31

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

kevin628
  • 3,458
  • 3
  • 30
  • 44
Pavel Voronin
  • 13,503
  • 7
  • 71
  • 137
  • Have you tried method `PrintMe` with `FormattableString` and not `IFormattable`? – Dovydas Šopa Jun 30 '16 at 09:44
  • 3
    Get the idea out of your head that `() => {}` is an `Action`. It's not. It's a lambda expression. It can be converted to an `Action`, or any other delegate type that has the correct signature (like `MethodInvoker` or `ThreadStart`). It can also be converted to an `Expression`, or any other `Expression` that has the correct signature. That's why it's ambiguous. `PrintMe(Action)` and `PrintMe(Expression)` are equally valid. – Dennis_E Jun 30 '16 at 09:46
  • @DovydasSopa Not yet. – Pavel Voronin Jun 30 '16 at 09:46
  • @Dennis_E Yes, and that's why I tried both. And I'd expect the same ambiguity for interpolated string. But it is not. – Pavel Voronin Jun 30 '16 at 09:47
  • I guess the original type is a special compiler type called "interpolated string" which is convertible to other types. Like with lambda expressions and method groups. So it's a special type not represented in CLR. – IS4 Jul 24 '16 at 00:04

3 Answers3

22

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
Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • Now we should explain how 3 and 4 work with overloading. It seams like compiler prefers to infer IFormattable, rather than a string. – Pavel Voronin Jun 30 '16 at 09:50
  • Please post a .NET Fiddle or Try Roslyn example for that @voroninp as it does not match my observations, see my edited answer. – Lasse V. Karlsen Jun 30 '16 at 09:54
  • It's the first code sample. I'd expect, that 'object message' would be selected for interpolated string rather than 'IFormattable message'. – Pavel Voronin Jun 30 '16 at 09:58
  • I mean that while string is preferred, when you do not have a string, but IFormattable, compiler infers the type of IFormattable, not of the string – Pavel Voronin Jun 30 '16 at 10:00
  • There are only two resulting types possible, either a `string`, or a `FormattableString`, and the compiler will use the former *if possible*, but if not possible it will see if it can use the latter, and if not even that, it will produce a compiler error, unable to convert or no overload or method, or something along those lines. – Lasse V. Karlsen Jun 30 '16 at 10:19
  • @voroninp I find the behavior for extension methods is normal. Check my answer. – Zein Makki Jun 30 '16 at 11:05
  • Referencing the source ( https://github.com/dotnet/roslyn/blob/56f605c41915317ccdb925f66974ee52282609e7/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_StringInterpolation.cs#L10) it creates a FormattableStringFactory.Create() call, followed by conversion if required. – Caramiriel Jul 02 '16 at 10:43
  • You are so right we really need the "official C# 6 standards document". I now linked (from my new answer here) a thread where a link to a preliminary version can be found (see "Linked" in the column at the right). You did a good job in listing all the relevant cases. That C# documentation concisely explains why everything works out exactly the way you observed in your answer. – Jeppe Stig Nielsen Jul 23 '16 at 18:32
  • If one wishes a list like the one you have in the end of the answer, one can remember that `string` implements a bunch of interfaces. For example `IComparable x = $"...";` is valid since a `string` is `IComparable`. In general, with method invocations, ordinary overload resolution is performed. This has many subtle properties but generally prefers more specific (for example `string`) overloads over less specific ones (like `object`). The type of `$"..."` is `string`, but `$"..."` has an __implicit conversion__ to `FormattableString` and its interface `IFormattable` as well. – Jeppe Stig Nielsen Jul 23 '16 at 18:46
9

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.

Zein Makki
  • 29,485
  • 6
  • 52
  • 63
4

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

Community
  • 1
  • 1
Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • If you were right, then extension method void Foo(this FormattableString str) would work. But it doesn't. Try it, and you'll get `CS1928 'string' does not contain a definition for 'Foo' and the best extension method overload 'FS.Foo(FormattableString)' has some invalid arguments`. – Pavel Voronin Jul 23 '16 at 17:22
  • @voroninp I am right. Overload resolution is complicated. Implicit conversions, however, are implicit conversions. If I make an extension method on `ushort`, and try using it with a `char`, as in `'j'.MyExtension()`, it also does not compile, even if there exists an implicit conversion from `char` to `ushort`. And you can try creating your own implicit conversion with `public static implicit operator ...` and observe the same. – Jeppe Stig Nielsen Jul 23 '16 at 17:26
  • I would not call it an implicit conversion. You cannot convert `string` to `FormattableString`. FormattableString needs more information than `string` contains. Compiler creates `FormattableString` instead of a simple interpolated string value depending on the context. – Pavel Voronin Jul 23 '16 at 17:35
  • @voroninp There is no implicit conversion from `string` to `FormattableString`. (1) The type of a string interpolation expression is `string`. (2) There exists an implicit conversion from a string interpolation expression (this is something that looks exactly like `$"..."` or `$@"..."` in the code) to the type `FormattableString`. That is all there is. – Jeppe Stig Nielsen Jul 23 '16 at 17:39
  • Well, yes, I agree with this wording, but, alas, there is no such type as `string interpolation expression`. As soon as we speak about expression it must have a type. The type of `string interpolation expression` is contextually dependent: compiler emits different code for this expression. The default type of this expression is `string`. Consider ternary expression `? :` its type depends only on operands, but you cannot tell the same about string interpolation. – Pavel Voronin Jul 23 '16 at 17:48
  • @voroninp I did not claim there was a "type" _string interpolation expression_. Instead a _string interpolation expression_ (actually called _interpolated string expression_ now that I check in the spec) is a part of a C# program. This particular expression has type `string`, but this expression also has the implicit conversion to `FormattableString`. If you say that all expressions in a C# program have a type, that is not quite exact. As you can see from my post, a lambda expression is an expression that does not have a type. Other expressions without a type are _method group_ and `null`. – Jeppe Stig Nielsen Jul 23 '16 at 18:10
  • Ok. We talk about the same, but I don't like the term 'implicit conversion'. It makes me think about implicit conversion operators which converts value of one type to the value of another type. And note, that bacause of not having defined type lambda expression does not allow using var. It requires you specify the type explicitly. In case of interpolation language designers decided for `string` type as default. – Pavel Voronin Jul 23 '16 at 18:22