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

What is the original type of interpolated string?

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

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

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

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

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

提示:

I am a System.String
I am a System.Runtime.CompilerServices.FormattableStringFactory+ConcreteFormattableString

如果您从第二种方法中删除注释,您将得到:

I am a string System.String
I am a string System.String

好的
可能是我不太了解重载决议,但是 C# spec 的 14.4.2 意味着首先定义了传递参数的类型,但是 lambda 又是如何工作的?

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

删除评论并...

CS0121 The call is ambiguous between the following methods or properties: 'UserQuery.PrintMe(Expression)' and '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);
    }
}

现在我变成这样了:

I am a System.String
I am a System.Runtime.CompilerServices.FormattableStringFactory+ConcreteFormattableString
I am a System.String
I am a System.String

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

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

  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
  3. 创建一个

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

所以,上面的例子展示了如果编译器知道目标类型会发生什么,在本例中是通过变量类型。如果我们调用一个没有重载且具有其中一种类型的方法,则会发生完全相同的 "magic"。

但是如果我们有重载会怎样?

考虑这个例子:

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

长话短说:

如果编译器找到带有 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 的扩展方法,您将遇到编译时错误。

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

字符串插值表达式$"..."的类型是string并且存在从字符串插值表达式$"..."到类型[=15的隐式转换=].

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

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

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

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

与像() => { }这样的lambda表达式的不同之处在于lambda表达式本身没有类型,它只有有隐式转换。有一个从 lambda 表达式 () => { } 到任何具有正确签名和 return 类型的委托类型 D 的隐式转换,加上一个到 System.Linq.Expressions.Expression<D> 类型的隐式转换,其中 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

为了完整起见,这里是,§7.6.2(权威)的措辞:

An interpolated string expression is classified as a value. If it is immediately converted to System.IFormattable or System.FormattableString with an implicit interpolated string conversion (§6.1.4), the interpolated string expression has that type. Otherwise, it has the type string.

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

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

An implicit interpolated string conversion permits an interpolated string expression (§7.6.2) to be converted to System.IFormattable or System.FormattableString (which implements System.IFormattable).