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

时间:2016-06-30 09:30:09

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

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

如果您从第二种方法中删除评论,则会获得:

  

我是一个字符串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");
}

删除评论和...

  

CS0121以下方法或之间的呼叫不明确   属性:'UserQuery.PrintMe(Expression)'和   'UserQuery.PrintMe(动作)'

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

更新

为了使事情变得更糟,我已经检查了扩展方法的这种行为:

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

3 个答案:

答案 0 :(得分:20)

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

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

  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

  6. 所以我们可以总结编译器魔术如下:

    1. 如果我们只使用通过调用string创建的String.Format即可,那么就这样做
    2. 如果没有,请使用FormattableString,然后通过FormattableStringFactory.Create
    3. 创建一个

      由于我们还没有执行C#6标准文件,除了仔细阅读github存储库,问题和讨论之外,其具体规则尚不清楚(至少对我而言,请证明我错了!)。

      因此,上面的例子说明了如果编译器知道目标类型会发生什么,在这种情况下通过变量类型。如果我们调用一个没有重载的单一方法,那就是其中一种类型,完全相同的&#34;魔法&#34;会发生。

      但如果我们有超载会怎样?

      考虑这个例子:

      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

答案 1 :(得分:9)

长话短说:

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

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

如果您使用PrintMe参数对方法string发表评论,则会生成以下代码:

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()(因为字符串是{{{ 1}})。所以生成的代码是:

object

注意如果删除string.Format("{0}", "Hello World").PrintMe(); 的扩展方法,则会出现编译时错误。

答案 2 :(得分:2)

让我们不要让事情变得过于复杂。

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

其余的只是普通的C#重载决议。

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

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

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

() => { }之类的lambda表达式的区别在于lambda表达式本身没有类型,具有隐式转换。有一个从lambda表达式() => { }到具有正确签名和返回类型的任何委托类型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

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

  

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

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

他们提到§6.1.4的小节是§6.1 Implict conversions 的一部分,并且内容为:

  

隐式插值字符串转换允许插值   要转换为System.IFormattable的字符串表达式(第7.6.2节)   或System.FormattableString(实施   System.IFormattable)。