【问题标题】:String interpolation issues字符串插值问题
【发布时间】:2017-06-27 18:37:03
【问题描述】:

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

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

【问题讨论】:

  • 看来(虽然我讨厌这样说)这是一个编译器错误。
  • @DavidG:可能是编译器错误,也可能是底层格式库中的错误,但我同意这里有些东西闻起来很糟糕。至少应该调查一下。
  • 这似乎与评估结束插值括号的方式有关。外括号上方的代码关闭了插值{{countdown|**{**date:o}}**}**,括号之间的空格使其计算为内括号{{countdown|**{**date:o**}**_}}
  • 请注意,问题不是由字符串插值引起的,而是在 string.Format 内部某处(例如 string.Format("{{{0:o}}}", date)
  • 看起来o 被解释为自定义日期时间格式的一部分。而且由于它不是有效的格式说明符,它只是被复制到输出。请参阅(自定义日期和时间格式字符串文档页面](msdn.microsoft.com/en-us/library/…)。

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


【解决方案1】:

这一行的问题

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

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

因此它将o 转换为o} 并且无法对其进行插值。

这应该可以工作

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

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

为了证明格式字符串像字符串一样被转义,考虑以下

$"{date:\x6f}"

被视为

$"{date:o}"

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

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

解析是一个基于表达式语法规则的形式化过程,不是看一眼就能完成的。

【讨论】:

  • 大括号的数量不是问题,因为$"{{{date:o}}}" 仍然会产生无效的输出。
  • @DavidG 3 curly quotes 在格式字符串 o do 之后产生无效输出,正如我在我的回答中所写的那样 - 现在是正确
  • 不错的分析。我很惊讶地得知词法分析器将格式说明符后面的}}} 视为转义的},然后是有意义的}。我天真地期望规则是“一旦你找到一个有意义的{,解析格式化的表达式直到你找到匹配的},然后恢复常规字符串词法分析。下次我看到尼尔我会问是什么导致这个有点令人惊讶的结果。
  • 另一个例子是Console.WriteLine(" {0} is rendered as {0:000}}000.000}",2000.4);,其中输出是2000,4 is rendered as 002}000,400,因此,一旦找到有意义的{,它就会解析直到找到匹配的}:,然后在后一种情况它恢复 } 转义
  • @user1892538 这是预期的行为吗?
【解决方案2】:

这是对我原来的回答的后续

确保这是预期的行为

就官方来源而言,我们应该参考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,根据paragraph 2.4.4.5 String literalsC# Language Specification 5.0
  2. 您可以在任何可以使用string literal 的地方使用内插字符串
  3. 要在插值字符串中包含大括号({}),请使用两个大括号 {{}} => 即编译器 转义 两个大括号optional-colon-format
  4. 编译器将包含的插值expressions 扫描为平衡文本,直到找到逗号、冒号或右大括号 => 即冒号打破平衡文本以及 一个大括号

为了清楚起见,这解释了$"{{{date}}}" 之间的区别,其中dateexpression,因此它在第一个花括号之前被标记化,而$"{{{date:o}}}" 其中date 又是expression 和现在它被标记直到第一个冒号,之后 常规字符串文字 开始,编译器继续转义两个花括号,等等......

还有来自 msdn 的 String Formatting FAQ,其中明确处理了这种情况。

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

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

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

确定说明符是否比单个字符长:如果是, 然后假设说明符是自定义格式。自定义格式 将为您的格式使用合适的替换,但如果它不知道 如何处理某些字符,它会简单地将其写为 格式中找到的文字 确定单个字符是否 说明符是受支持的说明符(例如数字的“N” 格式化)。如果是,则适当地格式化。如果没有,抛出一个 ArgumnetException

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

【讨论】:

  • 感谢您的详细跟进。
【解决方案3】:

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

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

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

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

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

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

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

【讨论】:

    【解决方案4】:

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

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

    以这种形式...

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

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

    这与其说是一个错误,不如说是对内插字符串的语法限制。如果存在错误,则可能是格式化文本的输出应该是“o}”而不是“o”。

    我们在 C、C# 和 C++ 中使用运算符“+=”而不是“=+”的原因是,在 =+ 形式中,在某些情况下,您无法判断“+”是运算符的一部分还是一元“+”。

    【讨论】:

      猜你喜欢
      • 2018-11-07
      • 1970-01-01
      • 2023-03-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多