简体   繁体   中英

What is the original type of interpolated string?

MSDN docs contain the section about implicit conversions:

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

From the first string it follows that original type of interpolated string is string . Ok, I can understand it, but then… I realize that string does not implement IFormattable . So it looks like some magic from the compiler similar to what it does with lambdas.

Now guess the output of this code:

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

Hint:

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

If you remove comments from the second method you'll get:

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

Ok
May be I do not understand well overloading resolution, but 14.4.2 of C# spec implies that the type of the passed parameter is defined first, but again how do then lambdas work?

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

Remove comments and...

CS0121 The call is ambiguous between the following methods or properties: 'UserQuery.PrintMe(Expression)' and 'UserQuery.PrintMe(Action)'

So I do not understand compiler's behavior here.

Update:

To make things worse I've checked this behavior for extension methods:

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

Now I have it like that:

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

The new interpolated string syntax is part compiler magic and part runtime classes.

Let's go through all the scenarios and see what is actually happening.

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

    This gets compiled as this:

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

    See Try Roslyn for details.

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

    This gets compiled as this:

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

    See Try Roslyn for details.

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

    This gets compiled as this:

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

    See Try Roslyn for details.

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

    This gets compiled as this:

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

    See Try Roslyn for details.

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

    This gets compiled as this:

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

    See Try Roslyn for details.

So we can summarize the compiler magic as follows:

  1. If we can get by with just using string , created with a call to String.Format , then do that
  2. If not, use FormattableString , and create one via FormattableStringFactory.Create

Since we do not yet have an officiel C# 6 standards document, other than perusing the github repositories, issues, and discussions, the exact rules for this is not known (at least not to me, please prove me wrong!).

So, the above examples shows what happens if the compiler knows the target type, in this case through the variable type. If we call a single method, with no overloads, that has one of those types, the exact same "magic" will happen.

But what happens if we have overloads?

Consider this example:

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

When executing this example we get this output:

string

So clearly string is still preferred, even when multiple options are available.

See this .NET fiddle for details.

Note that .NET Fiddle for some reason does not allow me to use FormattableString directly, but if I run the same code, with that overload present, in LINQPad , I still get string as the output.

If I then remove the string overload I get FormattableString , and then if I remove that I get IFormattable , so with overloads I can then observe that the rules are, and here we stop with the first overload that has:

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

Long story short:

If the compiler finds a method PrintMe with string parameter it generates this code:

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

If you comment the method PrintMe with string parameter, it generates this code:

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

Then, the part of method overload decision is pretty easy i guess.

this.PrintMe("Hello World"); choose object parameter method, since "Hello World" can't be implicitly converted to IFormattable .

So, What is the original type of interpolated string?

This is based on the compiler's decision:

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

Generates (as a best option):

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

And:

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

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

Generates (in order to match the method signature):

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

For extension methods:

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

The compiler resolves $"{"Hello World"}" first, which leads to a string as a best decision and then checks if there is a method PrintMe() found (It is found since string is an object ). So the generated code is:

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

Note that if you remove the extension method for object , you'll get a compile-time error.

Let us not make things overly complicated.

The type of a string interpolation expression $"..." is string and there is an implicit conversion from a string interpolation expression $"..." to the type System.FormattableString .

The rest is just ordinary C# overload resolution.

If an overload is chosen that does not need the implicit conversion to System.FormattableString , a plain string is created (in practice this is implemented with the string.Format method). If the implicit conversion is needed, some concrete instance of the abstract class System.FormattableString is created (in practice with a FormattableStringFactory.Create method though that is an implementation detail).

You do not need method overloads to see these two basic cases. Just do:

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

The difference with a lambda expression like () => { } is that the lambda expression does not have a type in itself, it only has implicit conversions. There is one implicit conversion from the lambda expression () => { } to any delegate type D that has the right signature and return type, plus one implicit conversion to the System.Linq.Expressions.Expression<D> type where D is that delegate type.

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

For completeness, here is the wording from the likely C# Language Specification 6.0 , §7.6.2 (authoritative):

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 .

So implicit interpolated string conversion is the official designation for the implicit conversion I am talking about.

The subsection they mention §6.1.4 is a part of §6.1 Implict conversions , and reads:

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 ).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM