string插值问题

我想弄清楚为什么我的unit testing失败(下面的第三个断言):

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

是在variables的format string之后有3个引号,并且它从左到右开始转义,因此它将前2个引号作为格式string的一部分,将第三个引号作为最后一个引号。

所以它在o}转换o而且它不能插入它。

这应该工作

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

请注意,更简单的$"{date}}}" (即,3在没有 format string的variables之后curl)确实起作用,因为它认识到第一个curl引用是closures引用,而在break之后的格式说明符的解释正确的右括号标识。

为了certificate格式string像string一样被转义 ,请考虑以下内容

 $"{date:\x6f}" 

被视为

 $"{date:o}" 

最后,双转义引号是自定义date格式的一部分,所以编译器的行为是绝对合理的。 再一次,一个具体的例子

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

parsing是一个基于expression式语法规则的正式过程,不能仅仅通过一眼就能完成。

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

以确保这是预期的行为

就官方来源而言,我们应该参考msdn的内插string 。

内插string的结构是

 $ " <text> { <interpolation-expression> <optional-comma-field-width> <optional-colon-format> } <text> ... } " 

并且每个单个插值用语法forms化地定义

 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 literals C#语言规范5.0
  2. 您可以在任何可以使用string literal地方使用插入的string
  3. 要在插入的string中包含大括号( {} ),请使用两个花括号, {{}} =>即编译器以optional-colon-format 转义两个大括号
  4. 编译器将包含的插值expressions扫描为平衡文本,直到find逗号,冒号或closures大括号=>即冒号打破平衡文本以及closures大括号

为了清楚起见,这解释了$"{{{date}}}"之间的区别, date是一个expression ,所以它被标记化,直到第一个花括号与$"{{{date:o}}}" where date又是一个expression ,现在它被标记化直到第一个冒号,之后一个常规string文字开始,编译器恢复转义两个大括号等。

还有msdn的string格式化常见问题解答 ,在这里明确地处理了这种情况。

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

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

提供格式说明符时,string格式化将执行以下步骤:

确定说明符是否长于单个字符:如果是,则假定说明符是自定义格式。 自定义格式将使用适合的replace格式,但是如果它不知道如何处理某些字符,则会将其作为格式中的文本进行简单地写出。确定单个字符说明符是否是受支持的说明符(例如作为数字格式的“N”)。 如果是,则格式适当。 如果不是,则抛出一个ArgumnetException

当试图确定一个大括号是否应该被转义时,大括号只是按照他们收到的顺序来处理。 因此, {{{将前两个字符转义并打印文字{ ,第三个括号将开始格式化部分。 在此基础上,在}}}前两个大括号将被转义,因此一个文字}将被写入格式化string,然后最后一个大括号将被认为是结束一个格式化部分有了这个信息,我们现在可以弄清楚我们的{{{0:N}}}情况正在发生什么。 前两个花括号被转义,然后我们有一个格式化部分。 但是,在closures格式化部分之前,我们还要跳过结束的大括号。 因此,我们的格式部分实际上被解释为包含0:N} 。 现在,格式化程序查看格式说明符,并且它看到指定符N} 。 因此,它将其解释为自定义格式,既然N或}对于自定义数字格式都没有任何意义,这些字符只是简单地写出来,而不是被引用的variables的值。

问题似乎是插入一个括号,而使用string插值,你需要通过复制它来逃避它。 如果添加用于插值的括号本身,我们最终会得到一个三元括号,例如您在行中给出exception的那个:

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

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

然而,编译器将前两个视为已扫描的string字符,因此它将在插值分隔符之间插入一个string。 基本上编译器是这样做的:

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

你可以通过重新格式化这一行来解决这个问题:

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

这是使assert正常工作的最简单的方法。

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

在这种forms…

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

前两个右花括号被解释为一个文字大括号,第三个则是closures格式化expression式。

这不像插入string的语法限制那么多。 错误,如果有的话,格式化文本的输出应该是“o”而不是“o”。

在C,C#和C ++中,我们有运算符“+ =”而不是“= +”的原因是在forms= +中,在某些情况下,不能说“+”是运算符的一部分还是一元“ +”。