字符串插值问题

Fra*_*erZ 41 c# string-interpolation c#-6.0

我试图找出我的单元测试失败的原因(下面的第三个断言):

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}}}");
Run Code Online (Sandbox Code Playgroud)

AFAIK,这应该可以正常工作,但看起来它没有正确传递格式化参数,它只显示为{countdown|o}代码.知道为什么会失败吗?

小智 22

这条线的问题

Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");
Run Code Online (Sandbox Code Playgroud)

就是format string要在要转义的变量之后有3个卷曲引号并且它从左向右开始转义,因此它将前2个卷曲引号视为格式字符串的一部分,将第三个引号引用作为结束引号.

所以它转换o进去o},它无法插入它.

这应该工作

Assert.AreEqual(formatted, $"{{countdown|{date:o}"+"}");
Run Code Online (Sandbox Code Playgroud)

请注意,更简单$"{date}}}"(即没有 a 的变量之后的3个卷曲format string)确实有效,因为它识别出第一个卷曲引用是结束引用,而在:断开正确的右括号标识之后对格式说明符的解释.

要证明格式字符串像字符串一样进行转义,请考虑以下内容

$"{date:\x6f}"
Run Code Online (Sandbox Code Playgroud)

被视为

$"{date:o}"
Run Code Online (Sandbox Code Playgroud)

最后,双转义卷曲引号完全有可能是自定义日期格式的一部分,因此编译器的行为绝对合理.再一次,一个具体的例子

$"{date:MMM}}dd}}yyy}" // it's a valid feb}09}2017
Run Code Online (Sandbox Code Playgroud)

解析是一个基于表达式语法规则的正式过程,不能仅仅通过浏览它来完成.

  • 很好的分析.我很惊讶地发现lexer将格式说明符后面的`}}}`视为转义的`}`后跟一个有意义的`}`.我天真地希望规则是"一旦你找到一个有意义的`{`,解析格式化的表达式,直到你找到匹配的`}`,然后恢复常规字符串lexing.下次我看到Neal我会问什么导致这个有些令人惊讶的结果. (2认同)

小智 6

这是我原来的答案的后续行动

确保这是预期的行为

就官方消息来源而言,我们应该参考msdn 的Interpolated Strings.

插值字符串的结构是

$ " <text> { <interpolation-expression> <optional-comma-field-width> <optional-colon-format> } <text> ... } "  
Run Code Online (Sandbox Code Playgroud)

并且每个插值都是用语法正式定义的

single-interpolation:  
    interpolation-start  
    interpolation-start : regular-string-literal  

interpolation-start:  
    expression  
    expression , expression  
Run Code Online (Sandbox Code Playgroud)

这里最重要的是

  1. optional-colon-format被定义为一个regular-string-literal语法=>即,其可以包含一个escape-sequence,根据paragraph 2.4.4.5 String literals所述的C#语言规范5.0
  2. 您可以在任何可以使用的地方使用插值字符串 string literal
  3. 要在插值字符串中包含花括号({}),请使用两个花括号,{{}}=>即编译器两个花括号中转义两个花括号optional-colon-format
  4. 编译器将包含的插值扫描expressions为平衡文本,直到找到逗号,冒号或关闭大括号=>即冒号破坏平衡文本以及关闭大括号

只是要清楚,这说明之间的区别$"{{{date}}}"在那里date是一个expression,所以它被标记化,直到第一个大括号对$"{{{date:o}}}"其中date又是一个expression,现在它被标记化,直到第一个冒号,在这之后,规则字符串的开始和编译器继续逃逸两个花括号等...

还有来自msdn 的字符串格式化常见问题解答,其中明确处理了此案例.

int i = 42;
string s = String.Format(“{{{0:N}}}”, i);   //prints ‘{N}’
Run Code Online (Sandbox Code Playgroud)

问题是,为什么最后一次尝试失败了?为了理解这个结果,您需要了解两件事:

提供格式说明符时,字符串格式化采取以下步骤:

确定说明符是否长于单个字符:如果是,则假定说明符是自定义格式.自定义格式将为您的格式使用合适的替换,但如果它不知道如何处理某些字符,它将简单地将其写为以格式确定的文字确定单个字符说明符是否是受支持的说明符(例如数字格式为'N').如果是,则适当格式化.如果没有,扔一个 ArgumnetException

在尝试确定是否应该转义大括号时,只需按接收顺序处理大括号.因此,{{{将转义前两个字符并打印文字{,第三个大括号将开始格式化部分.在此基础上,}}}前两个大括号将被转义,因此文字}将被写入格式字符串,然后最后一个大括号将被假定为结束格式化部分有了这些信息,我们现在可以弄清楚是什么发生在我们的{{{0:N}}}情况.前两个花括号被转义,然后我们有一个格式化部分.但是,在关闭格式化部分之前,我们还会转义结束的大括号.因此,我们的格式化部分实际上被解释为包含0:N}.现在,格式化程序查看格式说明符,并查看N}说明符.因此,它将此解释为自定义格式,并且由于N或}都不表示自定义数字格式的任何内容,因此这些字符只是写出来的,而不是引用的变量的值.