字符串插值问题
String interpolation issues
我想弄清楚为什么我的单元测试失败(下面的第三个断言):
var date = new DateTime(2017, 1, 1, 1, 0, 0);
var formatted = "{countdown|" + date.ToString("o") + "}";
//Works
Assert.AreEqual(date.ToString("o"), $"{date:o}");
//Works
Assert.AreEqual(formatted, $"{{countdown|{date.ToString("o")}}}");
//This one fails
Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");
AFAIK,这应该可以正常工作,但它似乎没有正确传递格式化参数,它在代码中显示为 {countdown|o}
。知道为什么会失败吗?
这一行的问题
Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");
是你在要转义的变量的 format string
之后有 3 个弯引号,它开始从左到右转义,因此它将前 2 个弯引号视为 的一部分格式字符串 和第三个大引号作为结束引号。
因此它在 o}
中转换 o
并且无法对其进行插值。
这应该有效
Assert.AreEqual(formatted, $"{{countdown|{date:o}"+"}");
请注意,更简单的 $"{date}}}"
(即变量 后的 3 个卷曲没有 a format string
)确实有效,因为它识别出第一个卷曲引号是结束一个,而 :
之后的格式说明符的解释打破了正确的右括号标识。
为了证明格式字符串像字符串一样被转义,考虑如下
$"{date:\x6f}"
被视为
$"{date:o}"
最后,双转义弯引号完全有可能是自定义日期格式的一部分,因此编译器的行为是绝对合理的。再次,一个具体的例子
$"{date:MMM}}dd}}yyy}" // it's a valid feb}09}2017
解析是一个基于表达式语法规则的形式化过程,不是看一眼就能搞定的
问题似乎是在使用字符串插值时插入括号,您需要通过复制它来转义它。如果您添加用于插值本身的括号,我们最终会得到一个三重括号,例如您在给您例外的行中的那个:
Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");
现在,如果我们观察“}}}”,我们可以注意到第一个括号包含了字符串插值,而最后两个是作为一个string-escaped 括号字符。
然而,编译器将前两个字符视为转义字符串字符,因此它会在插值定界符之间插入一个字符串。基本上编译器正在做这样的事情:
string str = "a string";
$"{str'}'}"; //this would obviously generate a compile error which is bypassed by this bug
您可以通过重新格式化该行来解决此问题:
Assert.AreEqual(formatted, $"{{countdown|{$"{date:o}"}}}");
这是 follow-up 对我原来的回答的顺序
to make sure this is the intended behavior
关于官方来源,我们应该参考msdn的Interpolated Strings。
内插字符串的结构是
$ " <text> { <interpolation-expression> <optional-comma-field-width> <optional-colon-format> } <text> ... } "
并且每个插值都使用语法
正式定义
single-interpolation:
interpolation-start
interpolation-start : regular-string-literal
interpolation-start:
expression
expression , expression
这里重要的是
optional-colon-format
被定义为 regular-string-literal
语法 => 即它可以包含一个 escape-sequence
,根据 C# Language Specification 5.0[的 paragraph 2.4.4.5 String literals
- 您可以在任何可以使用
string literal
的地方使用内插字符串
- 要在内插字符串中包含花括号(
{
或 }
),请使用两个花括号 {{
或 }}
=> 即编译器 转义 optional-colon-format
中的两个大括号
- 编译器将包含的插值
expressions
扫描为平衡文本,直到找到逗号、冒号或右花括号 => 即冒号 中断 平衡文本以及一个大括号
为了清楚起见,这解释了 $"{{{date}}}"
之间的区别,其中 date
是一个 expression
,因此它被标记化直到第一个大括号与 $"{{{date:o}}}"
之间date
又是一个 expression
,现在它被标记化到第一个冒号,之后是一个 常规字符串文字 开始,编译器继续转义两个花括号,等等...
还有来自 msdn 的 String Formatting FAQ,其中明确处理了这种情况。
int i = 42;
string s = String.Format(“{{{0:N}}}”, i); //prints ‘{N}’
The question is, why did this last attempt fail? There’s two things
you need to know in order to understand this result:
When providing a format specifier, string formatting takes these
steps:
Determine if the specifier is longer than a single character: if so,
then assume that the specifier is a custom format. A custom format
will use suitable replacements for your format, but if it doesn’t know
what to do with some character, it will simply write it out as a
literal found in the format Determine if the single character
specifier is a supported specifier (such as ‘N’ for number
formatting). If it is, then format appropriately. If not, throw an
ArgumnetException
When attempting to determine whether a curly bracket should be
escaped, the curly brackets are simply treated in the order they are
received. Therefore, {{{
will escape the first two characters and
print the literal {
, and the the third curly bracket will begin the
formatting section. On this basis, in }}}
the first two curly
brackets will be escaped, therefore a literal }
will be written to
the format string, and then the last curly bracket will be assumed to
be ending a formatting section With this information, we now can
figure out what’s occurring in our {{{0:N}}}
situation. The first
two curly brackets are escaped, and then we have a formatting section.
However, we then also escape the closing curly bracket, before closing
the formatting section. Therefore, our formatting section is actually
interpreted as containing 0:N}
. Now, the formatter looks at the
format specifier and it sees N}
for the specifier. It therefore
interprets this as a custom format, and since neither N or } mean
anything for a custom numeric format, these characters are simply
written out, rather than the value of the variable referenced.
这是使断言起作用的最简单方法...
Assert.AreEqual(formatted, "{" + $"countdown|{date:o}" + "}");
这种形式...
Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");
前两个右大括号被解释为文字右大括号,第三个被解释为结束格式化表达式。
与其说这是内插字符串的语法限制,不如说这是一个错误。错误(如果有的话)是格式化文本的输出可能应该是 "o}" 而不是 "o".
我们在 C、C# 和 C++ 中使用运算符“+=”而不是“=+”的原因是,在形式 =+ 中,在某些情况下您无法判断“+”是否是运算符或一元“+”。
我想弄清楚为什么我的单元测试失败(下面的第三个断言):
var date = new DateTime(2017, 1, 1, 1, 0, 0);
var formatted = "{countdown|" + date.ToString("o") + "}";
//Works
Assert.AreEqual(date.ToString("o"), $"{date:o}");
//Works
Assert.AreEqual(formatted, $"{{countdown|{date.ToString("o")}}}");
//This one fails
Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");
AFAIK,这应该可以正常工作,但它似乎没有正确传递格式化参数,它在代码中显示为 {countdown|o}
。知道为什么会失败吗?
这一行的问题
Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");
是你在要转义的变量的 format string
之后有 3 个弯引号,它开始从左到右转义,因此它将前 2 个弯引号视为 的一部分格式字符串 和第三个大引号作为结束引号。
因此它在 o}
中转换 o
并且无法对其进行插值。
这应该有效
Assert.AreEqual(formatted, $"{{countdown|{date:o}"+"}");
请注意,更简单的 $"{date}}}"
(即变量 后的 3 个卷曲没有 a format string
)确实有效,因为它识别出第一个卷曲引号是结束一个,而 :
之后的格式说明符的解释打破了正确的右括号标识。
为了证明格式字符串像字符串一样被转义,考虑如下
$"{date:\x6f}"
被视为
$"{date:o}"
最后,双转义弯引号完全有可能是自定义日期格式的一部分,因此编译器的行为是绝对合理的。再次,一个具体的例子
$"{date:MMM}}dd}}yyy}" // it's a valid feb}09}2017
解析是一个基于表达式语法规则的形式化过程,不是看一眼就能搞定的
问题似乎是在使用字符串插值时插入括号,您需要通过复制它来转义它。如果您添加用于插值本身的括号,我们最终会得到一个三重括号,例如您在给您例外的行中的那个:
Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");
现在,如果我们观察“}}}”,我们可以注意到第一个括号包含了字符串插值,而最后两个是作为一个string-escaped 括号字符。
然而,编译器将前两个字符视为转义字符串字符,因此它会在插值定界符之间插入一个字符串。基本上编译器正在做这样的事情:
string str = "a string";
$"{str'}'}"; //this would obviously generate a compile error which is bypassed by this bug
您可以通过重新格式化该行来解决此问题:
Assert.AreEqual(formatted, $"{{countdown|{$"{date:o}"}}}");
这是 follow-up 对我原来的回答的顺序
to make sure this is the intended behavior
关于官方来源,我们应该参考msdn的Interpolated Strings。
内插字符串的结构是
$ " <text> { <interpolation-expression> <optional-comma-field-width> <optional-colon-format> } <text> ... } "
并且每个插值都使用语法
正式定义single-interpolation:
interpolation-start
interpolation-start : regular-string-literal
interpolation-start:
expression
expression , expression
这里重要的是
optional-colon-format
被定义为regular-string-literal
语法 => 即它可以包含一个escape-sequence
,根据 C# Language Specification 5.0[的paragraph 2.4.4.5 String literals
- 您可以在任何可以使用
string literal
的地方使用内插字符串
- 要在内插字符串中包含花括号(
{
或}
),请使用两个花括号{{
或}}
=> 即编译器 转义optional-colon-format
中的两个大括号
- 编译器将包含的插值
expressions
扫描为平衡文本,直到找到逗号、冒号或右花括号 => 即冒号 中断 平衡文本以及一个大括号
为了清楚起见,这解释了 $"{{{date}}}"
之间的区别,其中 date
是一个 expression
,因此它被标记化直到第一个大括号与 $"{{{date:o}}}"
之间date
又是一个 expression
,现在它被标记化到第一个冒号,之后是一个 常规字符串文字 开始,编译器继续转义两个花括号,等等...
还有来自 msdn 的 String Formatting FAQ,其中明确处理了这种情况。
int i = 42;
string s = String.Format(“{{{0:N}}}”, i); //prints ‘{N}’
The question is, why did this last attempt fail? There’s two things you need to know in order to understand this result:
When providing a format specifier, string formatting takes these steps:
Determine if the specifier is longer than a single character: if so, then assume that the specifier is a custom format. A custom format will use suitable replacements for your format, but if it doesn’t know what to do with some character, it will simply write it out as a literal found in the format Determine if the single character specifier is a supported specifier (such as ‘N’ for number formatting). If it is, then format appropriately. If not, throw an
ArgumnetException
When attempting to determine whether a curly bracket should be escaped, the curly brackets are simply treated in the order they are received. Therefore,
{{{
will escape the first two characters and print the literal{
, and the the third curly bracket will begin the formatting section. On this basis, in}}}
the first two curly brackets will be escaped, therefore a literal}
will be written to the format string, and then the last curly bracket will be assumed to be ending a formatting section With this information, we now can figure out what’s occurring in our{{{0:N}}}
situation. The first two curly brackets are escaped, and then we have a formatting section. However, we then also escape the closing curly bracket, before closing the formatting section. Therefore, our formatting section is actually interpreted as containing0:N}
. Now, the formatter looks at the format specifier and it seesN}
for the specifier. It therefore interprets this as a custom format, and since neither N or } mean anything for a custom numeric format, these characters are simply written out, rather than the value of the variable referenced.
这是使断言起作用的最简单方法...
Assert.AreEqual(formatted, "{" + $"countdown|{date:o}" + "}");
这种形式...
Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");
前两个右大括号被解释为文字右大括号,第三个被解释为结束格式化表达式。
与其说这是内插字符串的语法限制,不如说这是一个错误。错误(如果有的话)是格式化文本的输出可能应该是 "o}" 而不是 "o".
我们在 C、C# 和 C++ 中使用运算符“+=”而不是“=+”的原因是,在形式 =+ 中,在某些情况下您无法判断“+”是否是运算符或一元“+”。