字符串插值问题

40

我正在试图弄清楚为什么我的单元测试失败(以下是第三个断言):

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}}}");

据我所知,这应该能正常工作,但是看起来它没有正确传递格式参数,代码中只显示为{countdown|o}。有任何想法这是为什么失败了吗?


5
@DavidG: 可能是编译器的错误,或者是底层格式化库的错误,但我同意这里有些不对劲。至少应该调查一下。 - Eric Lippert
似乎与插值结束括号的计算方式有关。在上面的代码中,外部括号关闭了插值 {{countdown|**{**date:o}}**},括号之间的空格使其计算为内部括号 {{countdown|**{**date:o**}**_}} - Equalsk
我认为你的意思是 Assert.AreEqual(formatted, $"{{{$"countdown|{date:o}"}}}"); - Mikko Viitala
1
看起来 o 被解释为自定义日期时间格式的一部分。由于它不是有效的格式说明符,因此它只是被复制到输出中。请参阅(自定义日期和时间格式字符串文档页面)。 - Leonid Vasilev
我的赏金说这可能是一个错误。 - Blue
显示剩余6条评论
4个回答

22

这行代码存在问题

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

问题在于您在要转义的变量的格式字符串之后有3个花括号,它从左到右开始转义,因此它将前两个花括号视为格式字符串的一部分,第三个花括号作为结束符。

所以它将o转换成o},然后无法插值。

这应该可以解决问题。

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

请注意,更简单的$"{date}}}"(即变量后面没有格式字符串,只有3个大括号)也可以工作,因为它会识别第一个大括号是闭合符号,而在:之后的格式说明符的解释会破坏正确的闭合括号识别。 为了证明格式字符串像字符串一样被转义,考虑以下内容。
$"{date:\x6f}"

被视为

$"{date:o}"

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

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

解析是一种基于表达式语法规则的正式过程,不能仅凭眼观来完成。


1
@DavidG 在格式字符串 o 后使用 3 个花括号 将会产生无效的输出,正如我在我的答案中所写 - 现在已经 正确 - user6996876
2
不错的分析。我很惊讶地了解到,词法分析器将跟在格式说明符后面的 }}} 视为转义的 },然后是有意义的 }。我原本天真地期望规则是“一旦找到有意义的 {,就解析格式化表达式,直到找到匹配的 },然后恢复正常的字符串词法分析。下次我见到 Neal 时,我会问问他是什么导致了这个有些令人惊讶的结果。 - Eric Lippert
@user1892538 这是预期的行为吗? - Blue
@FrankerZ 是的,但无论哪种情况...让我引用大约6年前的这个评论吧... - user6996876
@user1892538 很好的观察。我很惊讶为什么这还没有被接受为答案。 - Chetan
显示剩余2条评论

6
这是对我的原始回答的跟进,以确保这是预期的行为。
就官方来源而言,我们应该参考来自 MSDN 的插值字符串
插值字符串的结构是:
$ " <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. 要在插值字符串中包括花括号 ({}),请使用两个花括号,{{}},即编译器会在 optional-colon-format 中转义两个花括号
  4. 编译器扫描包含的插值 expressions 作为平衡文本,直到找到逗号、冒号或关闭花括号,即冒号会 打断 平衡文本,以及 关闭花括号
只是为了明确,这里解释了$"{{{date}}}"$"{{{date:o}}}"之间的区别,其中date是一个expression,因此它会被标记化直到第一个花括号,而date再次是一个expression,现在它被标记化直到第一个冒号,之后开始一个regular string literal,编译器继续转义两个花括号等等...同时还有来自msdn的String Formatting FAQ,其中明确处理了这种情况。
int i = 42;
string s = String.Format(“{{{0:N}}}”, i);   //prints ‘{N}’

问题是,为什么这最后一次尝试失败了?要理解这个结果,您需要知道两件事情: 提供格式说明符时,字符串格式化采取以下步骤: 确定说明符是否超过单个字符:如果是,则假定说明符是自定义格式。自定义格式将使用适当的替换来进行格式化,但如果它不知道如何处理某些字符,则会将其简单地写出为在格式中找到的文字 确定单个字符说明符是否为受支持的说明符(例如数字格式化的“N”)。如果是,则适当地进行格式化。如果不是,则抛出ArgumentException 在尝试确定花括号是否应该被转义时,花括号只是按接收顺序简单地处理。因此,{{{将转义前两个字符并打印原始{,第三个花括号将开始格式化部分。基于此,在}}}中,前两个花括号将被转义,因此会将文本}写入格式字符串,然后最后一个花括号将被认为是结束格式化部分。有了这些信息,我们现在可以弄清楚在{{{0:N}}}情况下发生了什么。首先转义前两个花括号,然后我们有一个格式化部分。然而,在关闭格式化部分之前,我们还转义了闭合花括号。因此,我们的格式化部分实际上被解释为包含0:N}。现在,格式化程序查看格式说明符,并看到说明符N}。因此,它将其解释为自定义格式,由于N或}对于自定义数字格式没有任何意义,因此这些字符将被简单地写出,而不是引用的变量的值。

感谢您的详细跟进。 - Blue

2
问题似乎是在使用字符串插值时插入括号,需要通过复制来进行转义。如果加入用于插值的括号本身,则最终结果将变成三个括号,就像出现异常的那一行一样。
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}"}}}");

1

这是让断言生效的最简单方法...

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

在这个表单中...
Assert.AreEqual(formatted, $"{{countdown|{date:o}}}");

前两个右括号被解释为字面上的右括号,第三个右括号则表示结束格式化表达式。

这并不是一个 bug,而是插值字符串语法的限制。如果有 bug 的话,那就是格式化文本的输出应该是 "o}" 而不仅仅是 "o"。

C、C# 和 C++ 中为什么有 "+=" 运算符而没有 "=+" 是因为在形式上 "=+" 中有时无法确定 "+" 是运算符的一部分还是一元 "+"。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接