【问题标题】:What is the original type of interpolated string?内插字符串的原始类型是什么?
【发布时间】:2016-06-30 09:30:08
【问题描述】:

MSDN docs 包含关于隐式转换的部分:

var s = $"hello, {name}";
System.IFormattable s = $"Hello, {name}";
System.FormattableString s = $"Hello, {name}";

从第一个字符串开始,内插字符串的原始类型是string。好的,我可以理解,但是……我意识到字符串没有实现IFormattable。所以它看起来像是来自编译器的一些魔法,类似于它对 lambdas 所做的事情。

现在猜猜这段代码的输出:

void Main()
{
    PrintMe("Hello World");
    PrintMe($"{ "Hello World"}");
}

void PrintMe(object message)
{
    Console.WriteLine("I am a " + message.GetType().FullName);
}

//void PrintMe(string message)
//{
//  Console.WriteLine("I am a string " + message.GetType().FullName);
//}

void PrintMe(IFormattable message)
{
    Console.WriteLine("I am a " + message.GetType().FullName);
}

提示:

我是 System.String
我是 System.Runtime.CompilerServices.FormattableStringFactory+ConcreteFormattableString

如果你从第二种方法中移除 cmets,你会得到:

我是一个字符串 System.String
我是字符串 System.String

好的
可能是我不太了解重载分辨率,但是C# spec 的 14.4.2 暗示首先定义了传递参数的类型,但是 lambdas 又是如何工作的?

void Main()
{
    PrintMe(() => {});
    PrintMe(() => {});
}

void PrintMe(object doIt)
{
    Console.WriteLine("I am an object");
}

//void PrintMe(Expression<Action> doIt)
//{
//  Console.WriteLine("I am an Expression");
//}

void PrintMe(Action doIt)
{
    Console.WriteLine("I am a Delegate");
}

删除 cmets 和...

CS0121 调用在以下方法之间不明确或 属性:'UserQuery.PrintMe(Expression)' 和 'UserQuery.PrintMe(Action)'

所以我不明白这里编译器的行为。

更新:

更糟糕的是,我检查了扩展方法的这种行为:

void Main()
{
    PrintMe("Hello World");
    PrintMe($"{"Hello World"}");

    "Hello World".PrintMe();
    $"{"Hello World"}".PrintMe();
}

void PrintMe(object message)
{
    Console.WriteLine("I am a " + message.GetType().FullName);
}

void PrintMe(IFormattable message)
{
    Console.WriteLine("I am a " + message.GetType().FullName);
}

public static class Extensions
{
    public static void PrintMe(this object message)
    {
        Console.WriteLine("I am a " + message.GetType().FullName);
    }

    public static void PrintMe(this IFormattable message)
    {
        Console.WriteLine("I am a " + message.GetType().FullName);
    }
}

现在我有这样的:

我是一个 System.String
我是 System.Runtime.CompilerServices.FormattableStringFactory+ConcreteFormattableString
我是 System.String
我是 System.String

【问题讨论】:

  • 您尝试过PrintMe 与FormattableString 而不是IFormattable 的方法吗?
  • 摆脱() =&gt; {} 是Action 的想法。它不是。这是一个 lambda 表达式。它可以转换为Action,或任何其他具有正确签名的委托类型(如MethodInvoker 或ThreadStart)。它也可以转换为Expression&lt;Action&gt;,或任何其他具有正确签名的Expression&lt;TDelegate&gt;。这就是为什么它是模棱两可的。 PrintMe(Action) 和 PrintMe(Expression&lt;Action&gt;) 同样有效。
  • @DovydasSopa 还没有。
  • @Dennis_E 是的,这就是我尝试两者的原因。而且我希望插值字符串有同样的歧义。但事实并非如此。
  • 我猜原始类型是一种特殊的编译器类型,称为“插值字符串”,可以转换为其他类型。就像 lambda 表达式和方法组一样。所以它是 CLR 中没有表示的特殊类型。

标签: c# string type-inference overloading string-interpolation


【解决方案1】:

新的内插字符串语法部分是编译器魔法,部分是运行时类。

让我们浏览所有场景,看看实际发生了什么。

  1. var s = $"{DateTime.Now}";

    编译如下:

    string s = string.Format("{0}", DateTime.Now);
    

    详情请见Try Roslyn。

  2. string s = $"{DateTime.Now}";

    编译如下:

    string s = string.Format("{0}", DateTime.Now);
    

    详情请见Try Roslyn。

  3. object s = $"{DateTime.Now}";

    编译如下:

    object s = string.Format("{0}", DateTime.Now);
    

    详情请见Try Roslyn。

  4. IFormattable s = $"{DateTime.Now}";

    编译如下:

    IFormattable s = FormattableStringFactory.Create("{0}", new object[] {
        DateTime.Now
    });
    

    详情请见Try Roslyn。

  5. FormattableString s = $"{DateTime.Now}";

    编译如下:

    FormattableString s = FormattableStringFactory.Create("{0}", new object[] {
        DateTime.Now
    });
    

    详情请见Try Roslyn。

所以我们可以将编译器的魔力总结如下:

  1. 如果我们只使用 string(通过调用 String.Format 创建)就可以解决问题,那么就这样做
  2. 如果没有,请使用FormattableString,并通过FormattableStringFactory.Create 创建一个

由于我们还没有正式的 C# 6 标准文档,除了仔细阅读 github 存储库、问题和讨论之外,不知道具体规则(至少对我来说不是,请证明我错了!)。

所以,上面的例子展示了如果编译器知道目标类型会发生什么,在这种情况下是通过变量类型。如果我们调用具有其中一种类型的单一方法,没有重载,就会发生完全相同的“魔法”。

但是如果我们有重载会发生什么?

考虑这个例子:

using System;

public class Program
{
    public static void Main()
    {
        Test($"{DateTime.Now}");
    }

    public static void Test(object o) { Console.WriteLine("object"); }
    public static void Test(string o) { Console.WriteLine("string"); }
    public static void Test(IFormattable o) { Console.WriteLine("IFormattable"); }
    // public static void Test(FormattableString o) { Console.WriteLine("FormattableString"); }
}

执行此示例时,我们会得到以下输出:

string

很明显,string 仍然是首选,即使有多个选项可用。

详情请见this .NET fiddle。

请注意,.NET Fiddle 出于某种原因不允许我直接使用 FormattableString,但如果我运行相同的代码, 存在重载,在 LINQPad 中,我仍然会得到string 作为输出。

如果我然后删除string 重载,我得到FormattableString,然后如果我删除它,我得到IFormattable,所以通过重载我可以观察到规则是,这里我们停止第一个重载有:

  1. string
  2. FormattableString
  3. IFormattable
  4. object

【讨论】:

  • 现在我们应该解释 3 和 4 如何处理重载。看起来编译器更喜欢推断 IFormattable,而不是字符串。
  • 请为 @voroninp 发布一个 .NET Fiddle 或尝试 Roslyn 示例,因为它与我的观察不符,请参阅我编辑的答案。
  • 这是第一个代码示例。我希望,将为插值字符串选择“对象消息”而不是“IFormattable 消息”。
  • 我的意思是虽然首选字符串,但当您没有字符串但有 IFormattable 时,编译器会推断 IFormattable 的类型,而不是字符串的类型
  • 只有两种可能的结果类型,string 或 FormattableString,编译器将使用前者如果可能,但如果不可能的话将查看它是否可以使用后者,如果不能,它将产生编译器错误,无法转换或没有重载或方法,或类似的东西。
【解决方案2】:

长话短说:

如果编译器找到带有string 参数的方法PrintMe,它会生成以下代码:

this.PrintMe("Hello World");
this.PrintMe(string.Format("{0}", "Hello World"));

如果您使用string 参数注释方法PrintMe,它会生成以下代码:

this.PrintMe("Hello World");
this.PrintMe(FormattableStringFactory.Create("{0}", new object[] {"Hello World"}));

然后,我猜方法重载决策的部分很容易。

this.PrintMe("Hello World");选择object参数方法,因为"Hello World"不能隐式转换为IFormattable。

那么,内插字符串的原始类型是什么?

这是基于编译器的决定:

var s1 = $"{ "Hello World"}";

生成(作为最佳选择):

string s1 = string.Format("{0}", "Hello World");

还有:

void PrintMe(IFormattable message)
{
    Console.WriteLine("I am a " + message.GetType().FullName);
}

PrintMe($"{ "Hello World"}");

生成(为了匹配方法签名):

this.PrintMe(FormattableStringFactory.Create("{0}", new object[] {"Hello World"}));

对于扩展方法:

$"{"Hello World"}".PrintMe();

public static class Extensions
{
    public static void PrintMe(this object message)
    {
        Console.WriteLine("I am a " + message.GetType().FullName);
    }

    public static void PrintMe(this IFormattable message)
    {
            Console.WriteLine("I am a " + message.GetType().FullName);
    }
}

编译器首先解析$"{"Hello World"}",这导致string作为最佳决策,然后检查是否找到方法PrintMe()(因为字符串是object,所以找到了方法)。所以生成的代码是:

string.Format("{0}", "Hello World").PrintMe();

注意,如果您删除 object 的扩展方法,您将收到编译时错误。

【讨论】:

  • 我个人认为这种行为是在脚上开枪的好方法。
【解决方案3】:

我们不要把事情弄得太复杂。

字符串插值表达式$"..."的类型是string,并且存在从字符串插值表达式$"..."到类型System.FormattableString的隐式转换。

剩下的只是普通的 C# 重载解析。

如果选择了不需要需要隐式转换为System.FormattableString 的重载,则会创建一个纯字符串(实际上这是通过string.Format 方法实现的)。如果需要隐式转换,则创建抽象类System.FormattableString 的一些具体实例(实际上使用FormattableStringFactory.Create 方法,尽管这是一个实现细节)。

您不需要方法重载来查看这两种基本情况。做吧:

var a = $"...";               // string
FormattableString b = $"..."; // the implicit conversion 

与 () =&gt; { } 这样的 lambda 表达式的区别在于 lambda 表达式本身没有类型,它只有有隐式转换。有一个从 lambda 表达式 () =&gt; { } 到具有正确签名和返回类型的任何委托类型 D 的隐式转换,以及一个到 System.Linq.Expressions.Expression&lt;D&gt; 类型的隐式转换,其中 D 是该委托类型。

var p = () => {};                                // BAD, compile-time error
Action q = () => {};                             // OK, one implicit conversion
SomeAppropriateDelType r = () => {};             // OK, another implicit conversion
Expression<Action> s  = () => {};                // OK, another implicit conversion
Expression<SomeAppropriateDelType> t = () => {}; // OK, another implicit conversion

为了完整起见,以下是likely C# Language Specification 6.0,§7.6.2(权威)的措辞:

内插字符串表达式被归类为一个值。如果是 立即转换为System.IFormattable 或 System.FormattableString 带有隐式插值字符串 转换(第 6.1.4 节),内插字符串表达式具有该类型。 否则,它的类型为string。

所以隐式插值字符串转换是我所说的隐式转换的官方名称。

他们提到 §6.1.4 的小节是 §6.1 隐式转换的一部分,内容如下:

隐式插值字符串转换允许 插值 字符串表达式 (§7.6.2) 被转换为System.IFormattable 或System.FormattableString(实现 System.IFormattable)。

【讨论】:

  • 如果你是对的,那么扩展方法 void Foo(this FormattableString str) 将起作用。但事实并非如此。试试吧,你会得到CS1928 'string' does not contain a definition for 'Foo' and the best extension method overload 'FS.Foo(FormattableString)' has some invalid arguments。
  • @voroninp 我是对的。重载解析很复杂。然而,隐式转换是隐式转换。如果我在ushort 上创建一个扩展方法,并尝试将它与char 一起使用,就像在'j'.MyExtension() 中一样,即使存在从char 到ushort 的隐式转换,它也不会编译。您可以尝试使用 public static implicit operator ... 创建自己的隐式转换并观察相同的结果。
  • 我不会称之为隐式转换。您无法将 string 转换为 FormattableString。 FormattableString 需要比string 包含的更多信息。编译器根据上下文创建FormattableString,而不是简单的内插字符串值。
  • @voroninp 没有从string 到FormattableString 的隐式转换。 (1) 字符串插值表达式的类型为string。 (2) 存在从字符串插值表达式(这与代码中的$"..." 或$@"..." 完全相同)到类型FormattableString 的隐式转换。这就是全部。
  • 嗯,是的,我同意这个措辞,但是,唉,没有像string interpolation expression 这样的类型。一旦我们谈到表达式,它就必须有一个类型。 string interpolation expression 的类型取决于上下文:编译器为此表达式发出不同的代码。此表达式的默认类型是string。考虑三元表达式? :,它的类型仅取决于操作数,但对于字符串插值,您不能说相同。
猜你喜欢
  • 2019-11-04
  • 1970-01-01
  • 1970-01-01
  • 2011-01-06
  • 2022-10-09
  • 2012-08-25
相关资源
最近更新 更多