字符串插值问题

时间:2017-02-09 16:31:13

标签: c# c#-6.0 string-interpolation

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

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}代码。知道为什么会失败吗?

4 个答案:

答案 0 :(得分:22)

这一行的问题

Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");

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

因此它会转换o中的o},并且它无法插入它。

这应该有效

Assert.AreEqual(formatted, $"{{countdown|{date:o}"+"}");

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

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

$"{date:\x6f}"

被视为

$"{date:o}"

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

$"{date:MMM}}dd}}yyy}" // it's a valid feb}09}2017

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

答案 1 :(得分:6)

这是跟进到我原来的答案

  

确保这是预期的行为

就官方来源而言,我们应该参考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语法=&gt;即根据C# Language Specification 5.0
  2. escape-sequence,它可以包含paragraph 2.4.4.5 String literals
  3. 您可以在任何可以使用string literal
  4. 的地方使用插值字符串
  5. 要在插补字符串中包含大括号({}),请使用两个大括号{{}} =&gt;即编译器optional-colon-format
  6. 中转义两个大括号
  7. 编译器将包含的插值expressions扫描为平衡文本,直到找到逗号,冒号或关闭大括号=&gt;即结肠打破平衡文本以及一个紧密的大括号
  8. 为了清楚起见,这解释了$"{{{date}}}" date expression$"{{{date:o}}}"之间的差异,因此在第一次大括号与date expression之前标记为int i = 42; string s = String.Format(“{{{0:N}}}”, i); //prints ‘{N}’ 1}}再次是ArgumnetException,现在它被标记化,直到第一个冒号,之后常规字符串文字开始,编译器恢复转义两个花括号等...

    还有来自msdn的String Formatting FAQ,其中明确处理了此案例。

    {{{
      

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

         

    提供格式说明符时,字符串格式化会采用这些格式说明符   步骤进行:

         

    确定说明符是否长于单个字符:如果是,   然后假设说明符是自定义格式。自定义格式   将为您的格式使用合适的替代品,但如果它不知道   如何处理某些角色,它只会将其写成一个   以格式找到的文字确定单个字符   说明符是受支持的说明符(例如数字的“N”)   格式)。如果是,则适当格式化。如果没有,扔一个   {

         

    尝试确定是否应该使用花括号   转义后,大括号按照它们的顺序进行简单处理   接收。因此,}}}将转义前两个字符和   打印文字},第三个花括号将开始   格式化部分。在此基础上,在{{{0:N}}}前两个卷曲   括号将被转义,因此将写入文字0:N}   格式字符串,然后将假定最后一个大括号   结束格式化部分有了这些信息,我们现在可以   弄清楚N}情况下发生了什么。首先   两个花括号被转义,然后我们有一个格式化部分。   但是,在关闭之前,我们还会从结束的大括号中逃脱   格式化部分。因此,我们的格式化部分实际上是   解释为包含 let testText = SCNText(string: "ABCDEF", extrusionDepth: 0) testText.font = font let txtNd = SCNNode(geometry: testText) txtNd.position = SCNVector3(0,0,0) rootNode.addChildNode(txtNd) let bMax = txtNd.boundingBox.max let bMin = txtNd.boundingBox.min print("\(bMax)\n\(bMin)") 。现在,格式化程序看着   格式说明符,它看到说明符object Dummy { abstract class Foo { def execute: Unit } abstract class Bar { def execute: Unit } class FooFoo extends Foo { def execute: Unit = { println("Foo") } } class BarBar extends Bar { def execute: Unit = { println("Bar") } } def run(x: AnyRef): Unit = { x match { case f: Foo => f.execute case b: Bar => b.execute case _ => } } def main(args: Array[String]): Unit = { val any = new BarBar() run(any) } } 。因此   将此解释为自定义格式,因为N或}都不是   任何自定义数字格式,这些字符都很简单   写出来,而不是引用变量的值。

答案 2 :(得分:2)

问题似乎是在使用字符串插值时插入一个括号,你需要通过复制它来逃避它。如果你添加用于插值本身的括号,我们最终得到一个三重括号,例如你在行中提供例外的那个括号:

Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");

现在,如果我们观察&#34; }}}&#34; ,我们可以注意到第一个括号包含了字符串插值,而最后两个括号被视为字符串转义的括号字符。

然而,编译器将前两个视为字符串字符,因此它在插值分隔符之间插入一个字符串。基本上编译器正在做这样的事情:

string str = "a string";
$"{str'}'}"; //this would obviously generate a compile error which is bypassed by this bug

您可以通过重新格式化此行来解决此问题:

Assert.AreEqual(formatted, $"{{countdown|{$"{date:o}"}}}");

答案 3 :(得分:1)

这是让断言起作用的最简单方法......

Assert.AreEqual(formatted, "{" + $"countdown|{date:o}" + "}");

以这种形式......

Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");

前两个结束括号被解释为文字右括号,第三个用于关闭格式表达式。

这不是一个错误,而是对插值字符串的语法限制。错误(如果有的话)是格式化文本的输出应该是&#34; o}&#34;而不只是&#34; o&#34;。

我们有运营商&#34; + =&#34;而不是&#34; = +&#34;在C,C#和C ++中,形式= +你在某些情况下无法判断是否&#34; +&#34;是运营商或一元&#34; +&#34;的一部分。