字符串插值问题

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  

这里重要的是

  1. optional-colon-format 被定义为 regular-string-literal 语法 => 即它可以包含一个 escape-sequence,根据 C# Language Specification 5.0[的 paragraph 2.4.4.5 String literals
  2. 您可以在任何可以使用 string literal
  3. 的地方使用内插字符串
  4. 要在内插字符串中包含花括号({}),请使用两个花括号 {{}} => 即编译器 转义 optional-colon-format
  5. 中的两个大括号
  6. 编译器将包含的插值 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++ 中使用运算符“+=”而不是“=+”的原因是,在形式 =+ 中,在某些情况下您无法判断“+”是否是运算符或一元“+”。