csharplang/meetings/2014/LDM-2014-10-15.md
Nick Schonning 6cd82c21ed fix: MD033/no-inline-html
Inline HTML get swallowed in MD and HTML rendering
2019-05-25 01:31:46 -04:00

33 KiB

nameof operator: spec v5

The nameof(.) operator has the form nameof(expression). The expression must have a name, and may refer to either a single symbol or a method-group or a property-group. Depending on what the argument refers to, it can include static, instance and extension members.

This is v5 of the spec for the "nameof" operator. [v1, v2, v3, v4]. The key decisions and rationales are summarized below. Please let us know what you think!

Rationale

Question: why do we keep going back-and-forth on this feature?

Answer: I think we're converging on a design. It's how you do language design! (1) make a proposal, (2) spec it out to flush out corner cases and make sure you've understood all implications, (3) implement the spec to flush out more corner cases, (4) try it in practice, (5) if what you hear or learn at any stage raises concerns, goto 1.

  • v1/2 had the problem "Why can't I write nameof(this.p)? and why is it so hard to write analyzers?"
  • v3 had the problem "Why can't I write nameof(MyClass1.p)? and why is it so hard to name method-groups?"
  • v4 had the problem "What name should be returned for types? and how exactly does it relate to member lookup?"

This particular "nameof v5" proposal came from a combined VB + C# LDM on 2014.10.22. We went through the key decisions:

  1. Allow to dot an instance member off a type? Yes. Settled on the answer "yes", based on the evidence that v1/v2 had it and it worked nicely, and v3 lacked it and ended up with unacceptably ugly "default(T)" constructions.
  2. Allow to dot instance members off an instance? Yes. Settled on the answer "yes", based on the evidence that v1/v2 lacked it and it didn't work well enough when we used the CTP, primarily for the case "this.p"
  3. Allow to name method-groups? Yes. Settled on the answer "yes", based on the evidence that v1/v2 had it and it worked nicely, and v3 lacked it and ended up with unacceptably ugly constructions to select which method overload.
  4. Allow to unambiguously select a single overload? No. Settled on the answer "no" based on the evidence that v3 let you do this but it looked too confusing. I know people want it, and it would be a stepping stone to infoof, but at LDM we rejected these (good) reasons as not worth the pain. The pain is that the expressions look like they'll be executed, and it's unclear whether you're getting the nameof the method or the nameof the result of the invocation, and they're cumbersome to write.
  5. Allow to use nameof(other-nonexpression-types)? No. Settled on the answer "only nameof(expression)". I know people want other non-expression arguments, and v1/v2 had them, and it would be more elegant to just write nameof(List<>.Length) rather than having to specify a concrete type argument. But at LDM we rejected these (good) reasons as not worth the pain. The pain is that the language rules for member access in expressions are too different from those for member access in the argument to nameof(.), and indeed member access for StrongBox<>.Value.Length doesn't really exist. The effort to unify the two concepts of member access would be way disproportionate to the value of nameof. This principle, of sticking to existing language concepts, also explains why v5 uses the standard language notions of "member lookup", and hence you can't do nameof(x.y) to refer to both a method and a type of name "y" at the same time.
  6. Use source names or metadata names? Source. Settled on source names, based on the evidence... v1/v2/v3 were source names because that's how we started; then we heard feedback that people were interested in metadata names and v4 explored metadata names. But I think the experiment was a failure: it ended up looking like disallowing nameof on types was better than picking metadata names. At LDM, after heated debate, we settled on source names. For instance using foo=X.Y.Z; nameof(foo) will return "foo". Also string f<T>() => nameof(T) will return "T". There are pros and cons to this decision. In its favor, it keeps the rules of nameof very simple and predictable. In the case of nameof(member), it would be a hindrance in most (not all) bread-and-butter cases to give a fully qualified member name. Also it's convention that "System.Type.Name" refers to an unqualified name. Therefore also nameof(type) should be unqualified. If ever you want a fully-qualified type name you can use typeof(). If you want to use nameof on a type but also get generic type parameters or arguments then you can construct them yourself, e.g. nameof(List<int>) + "`2". Also the languages have no current notion of metadata name, and metadata name can change with obfuscation.
  7. Allow arbitrary expressions or just a subset? We want to try out the proposal "just a subset" because we're uneasy with full expressions. That's what v5 does. We haven't previously explored this avenue, and it deserves a try.
  8. Allow generic type arguments? Presumably 'yes' when naming a type since that's how expression binding already works. And presumably 'no' when naming a method-group since type arguments are used/inferred during overload resolution, and it would be confusing to also have to deal with that in nameof. [this item 8 was added after the initial v5 spec]

I should say, we're not looking for unanimous consensus -- neither amongst the language design team nor amongst the codeplex OSS community! We hear and respect that some people would like something closer to infoof, or would make different tradeoffs, or have different use-cases. On the language design team we're stewards of VB/C#: we have a responsibility to listen to and understand every opinion, and then use our own judgment to weigh up the tradeoffs and use-cases. I'm glad we get to do language design in the open like this. We've all been reading the comments on codeplex and elsewhere, our opinions have been swayed, we've learned about new scenarios and issues, and we'll end up with a better language design thanks to the transparency and openness. I'm actually posting these notes on codeplex as my staging ground for sharing them with the rest of the team, so the codeplex audience really does see everything "in the raw".

C# Syntax

expression: ... | nameof-expression

name-of-expression:
    nameof ( expression )

In addition to the syntax indicated by the grammar, there are some additional syntactic constraints: (1) the argument expression can only be made up out of simple-name, member-access, base-access, or "this", and (2) cannot be simply "this" or "base" on its own. These constraints ensure that the argument looks like it has a name, and doesn't look like it will be evaluated or have side effects. I found it easier to write the constraints in prose than in the grammar.

[clarification update] Note that member-access has three forms, E.I<A1...AK> and predefined-type.I<A1...AK> and qualified-alias-member.I. All three forms are allowed, although the first case E must only be made out of allowed forms of expression.

If the argument to nameof at its top level has an unacceptable form of expression, then it gives the error "This expression does not have a name". If the argument contains an unacceptable form of expression deeper within itself, then it gives the error "This sub-expression cannot be used as an argument to nameof".

It is helpful to list some things not allowed as the nameof argument:

    invocation-expression        e(args)
    assignment                   x += 15
    query-expression             from y in z select y
    lambda-expression            () => e
    conditional-expression       a ? b : c
    null-coalescing-expression   a?? b
    binary-expression            ||, &&, |, ^, &, ==, !=,
                                 <, >, <=, >=, is, as, <<,
                                 >>, +, -, *, /, %
    prefix-expression            +, -, !, ~, ++, --,
                                 *, &, (T)e
    postfix-expression           ++, --
    array-creation-expression    new C[…]
    object-creation-expression   new C(…)
    delegate-creation-expression new Action(…)
    anonymous-object-creation-expression new {…}
    typeof-expression            typeof(int)
    checked-expression           checked(…)
    unchecked-expression         unchecked(…)
    default-value-expression     default(…)
    anonymous-method-expression  delegate {…}
    pointer-member-access        e->x
    sizeof-expression            sizeof(int)
    literal                      "hello", 15
    parenthesized-expression     (x)
    element-access               e[i]
    base-access-indexed          base[i]
    await-expression             await e
    nameof-expression            nameof(e)
    vb-dictionary-lookup         e!foo 

Note that there are some types which are not counted as expressions by the C# grammar. These are not allowed as nameof arguments (since the nameof syntax only allows expressions for its argument). There's no need to spell out that the following things are not valid expressions, since that's already said by the language syntax, but here's a selection of some of the types that are not expressions:

    predefined-type              int, bool, float, object,
                                 dynamic, string
    nullable-type                Customer?
    array-type                   Customer[,]
    pointer-type                 Buffer*, void* 
    qualified-alias-member       A::B
    void                         void
    unbound-type-name            Dictionary<,>

Semantics

The nameof expression is a constant. In all cases, nameof(...) is evaluated at compile-time to produce a string. Its argument is not evaluated at runtime, and is considered unreachable code (however it does not emit an "unreachable code" warning).

Definite assignment. The same rules of definite assignment apply to nameof arguments as they do to all other unreachable expressions.

Name lookup. In the following sections we will be discussing member lookup. This is discussed in $7.4 of the C# spec, and is left unspecified in VB. To understand nameof it is useful to know that the existing member lookup rules in both languages either return a single type, or a single instance/static field, or a single instance/static event, or a property-group consisting of overloaded instance/static properties of the same name (VB), or a method-group consisting of overloaded instance/static/extension methods of the same name. Or, lookup might fail either because no symbol was found or because ambiguous conflicting symbols were found that didn't fall within the above list of possibilities.

Argument binding. The nameof argument refers to one or more symbols as follows.

nameof(simple-name), of the form I or I<A1...AK> The normal rules of simple name lookup $7.6.2 are used but with one difference...

  • The third bullet talks about member lookup of I in T with K type arguments. Its third sub-bullet says "Otherwise, the result is the same as a member access of the form T.I or T.I<A1...AK>. In this case, it is a binding-time error for the simple-name to refer to an instance member." For the sake of nameof(simple-name), this case does not constitute a binding-time error.

nameof(member-access), of the form E.I or E.I<A1...AK> The normal rules of expression binding are used to evaluate "E", with no changes. After E has been evaluated, then E.I<A1...AK> is evaluated as per the normal rules of member access $7.6.4 but with some differences...

  • The third bullet talks about member lookup of I in E. Its sub-bullets have rules for binding when I refers to static properties, fields and events. For the sake of nameof(member-access), each sub-bullet applies to instance properties, fields and events as well.
  • The fourth bullet talks about member lookup of I in T. Its sub-bullets have rules for binding when I refers to instance properties, fields and events. For the sake of nameof(member-access), each sub-bullet applies to static properties, fields and events as well.

nameof(base-access-named), of the form base.I or base.I<A1...AK> This is treated as nameof(B.I) or nameof(B.I<A1...AK> where B is the base class of the class or struct in which the construct occurs.

Result of nameof. The result of nameof is the identifier "I" with the standard identifier transformations. Note that, at the top level, every possible argument of nameof has "I<A1...AK>".

[update that was added after the initial v5 spec] If "I" binds to a method-group and the argument of nameof has generic type arguments at the top level, then it produces an error "Do not use generic type arguments to specify the name of methods". Likewise for property-groups.

The standard identifier transformations in C# are detailed in $2.4.2 of the C# spec: first any leading @ is removed, then Unicode escape sequences are transformed, and then any formatting-characters are removed. This of course still happens at compile-time. In VB, any surrounding [] is removed

Implementation

In C#, nameof is stored in a normal InvocationExpressionSyntax node with a single argument. That is because in C# 'nameof' is a contextual keyword, which will only become the "nameof" operator if it doesn't already bind to a programmatic symbol named "nameof". TO BE DECIDED: what does its "Symbol" bind to?

In VB, NameOf is a reserved keyword. It therefore has its own node:

Class NameOfExpressionSyntax : Inherits ExpressionSyntax
    Public ReadOnly Property Argument As ExpressionSyntax
End Class

TO BE DECIDED: Maybe VB should just be the same as C#. Or maybe C# should do the same as VB.

What is the return value from var r = semanticModel.GetSymbolInfo(argument)? In all cases, r.Candidates is the list of symbol. If there is only one symbol then it is in r.Symbol; otherwise r.Symbol is null and the reason is "ambiguity".

Analyzers and the IDE will just have to deal with this case.

IDE behavior

class C {
   [3 references] static void f(int i) {...nameof(f)...}
   [3 references] void f(string s) {...nameof(this.f)...}
   [3 references] void f(object o) {...nameof(C.f)...}
}
static class E {
   [2 references] public static void f(this C c, double d) {}
}

Highlight symbol from argument: When you set your cursor on an argument to nameof, it highlights all symbols that the argument bound to. In the above examples, the simple name "nameof(f)" binds to the three members inside C. The two member access "nameof(this.f)" and "nameof(C.f)" both bind to extension members as well.

Highlight symbol from declaration: When you set your cursor on any declaration of f, it highlights all nameof arguments that bind to that declaration. Setting your cursor on the extension declaration will highlight only "this.f" and "C.f". Setting your cursor on any member of C will highlight both all three nameof arguments.

Goto Definition: When you right-click on an argument to nameof in the above code and do GoToDef, it pops up a FindAllReferences dialog to let you chose which declaration. (If the nameof argument bound to only one symbol then it would go straight to that without the FAR dialog.)

Rename declaration: If you do a rename-refactor on one of the declarations of f in the above code, the rename will only rename this declaration (and will not rename any of the nameof arguments); the rename dialog will show informational text warning you about this. If you do a rename-refactor on the last remaining declaration of f, then the rename will also rename nameof arguments. Note that if you turn on the "Rename All Overloads" checkbox of rename-refactor, then it will end up renaming all arguments.

Rename argument: If you do a rename-refactor on one of the nameof arguments in the above code, the rename dialog will by default check the "Rename All Overloads" button.

Expand-reduce: The IDE is free to rename "nameof(p)" to "nameof(this.p)" if it needs to do so to remove ambiguity during a rename. This might make nameof now bind to more things than it used to...

Codelens: We've articulated the rules about what the argument of nameof binds to. The CodeLens reference counts above are a straightforward consequence of this.

Bread and butter cases

// Validate parameters 
void f(string s) {
    if (s == null) throw new ArgumentNullException(nameof(s));
}
// MVC Action links
<%= Html.ActionLink("Sign up",
             @typeof(UserController),
             @nameof(UserController.SignUp))
%>
// INotifyPropertyChanged
int p {
    get { return this._p; }
    set { this._p = value; PropertyChanged(this, new PropertyChangedEventArgs(nameof(this.p)); }
}
// also allowed: just nameof(p)
// XAML dependency property
public static DependencyProperty AgeProperty = DependencyProperty.Register(nameof(Age), typeof(int), typeof(C));
// Logging
void f(int i) {
    Log(nameof(f), "method entry");
}
// Attributes
[DebuggerDisplay("={" + nameof(getString) + "()}")]
class C {
    string getString() { ... }
}

Examples

void f(int x) {
   nameof(x)
}
// result "x": Parameter (simple name lookup)
int x=2; nameof(x)
// result "x": Local (simple name lookup)
const x=2; nameof(x)
// result "x": Constant (simple name lookup)
class C {
   int x;
   ... nameof(x)
}
// result "x": Member (simple name lookup)
class C {
   void f() {}
   nameof(f)
}
// result "f": Member (simple name lookup)
class C {
   void f() {}
   nameof(f())
}
// result error "This expression does not have a name"
class C {
   void f(){}
   void f(int i){}
   nameof(f)
}
// result "f": Method-group (simple name lookup)
Customer c; ... nameof(c.Age)
// result "Age": Property (member access)
Customer c; ... nameof(c._Age)
// result error "_Age is inaccessible due to its protection level: member access
nameof(Tuple.Create)
// result "Create": method-group (member access)
nameof(System.Tuple)
// result "Tuple": Type (member access). This binds to the non-generic Tuple class; not to all of the Tuple classes.
nameof(System.Exception)
// result "Exception": Type (member access)
nameof(List<int>)
// result "List": Type (simple name lookup)
nameof(List<>)
// result error "type expected": Unbound types are not valid expressions
nameof(List<int>.Length)
// result "Length": Member (Member access)
nameof(default(List<int>))
// result error "This expression doesn't have a name": Not one of the allowed forms of nameof
nameof(default(List<int>).Length)
// result error "This expression cannot be used for nameof": default isn't one of the allowed forms
nameof(int)
// result error "Invalid expression term 'int'": Not an expression. Note that 'int' is a keyword, not a name.
nameof(System.Int32)
// result "Int32": Type (member access)
using foo=System.Int32;
nameof(foo) 
// result "foo": Type (simple name lookup)
nameof(System.Globalization)
// result "Globalization": Namespace (member access)
nameof(x[2])
nameof("hello")
nameof(1+2)
// error "This expression does not have a name": Not one of the allowed forms of nameof
NameOf(a!Foo)
' error "This expression does not have a name": VB-specific. Not one of the allowed forms of NameOf.
NameOf(dict("Foo"))
' error "This expression does not have a name": VB-specific. This is a default property access, which is not one of the allowed forms.
NameOf(dict.Item("Foo"))
' error "This expression does not have a name": VB-specific. This is an index of a property, which is not one of the allowed forms.
NameOf(arr(2))
' error "This expression does not have a name": VB-specific. This is an array element index, which is not one of the allowed forms.
Dim x = Nothing
NameOf(x.ToString(2))
' error "This expression does not have a name": VB-specific. This resolves to .ToString()(2), which is not one of the allowed forms.
Dim o = Nothing
NameOf(o.Equals)
' result "Equals". Method-group. Warning "Access of static member of instance; instance will not be evaluated": VB-specific. VB allows access to static members off instances, but emits a warning.
[Foo(nameof(C))]
class C {}
// result "C": Nameof works fine in attributes, using the normal name lookup rules.
[Foo(nameof(D))]
class C { class D {} }
// result "D": Members of a class are in scope for attributes on that class
[Foo(nameof(f))]
class C { void f() {} }
// result "f": Members of a class are in scope for attributes on that class
[Foo(nameof(T))]
class C<T> {}
// result error "T is not defined": A class type parameter is not in scope in an attribute on that class
[Foo(nameof(T))] void f<T> { }
// result error "T not defined": A method type parameter is not in scope in an attribute on that method
void f([Attr(nameof(x))] int x) {}
// result error "x is not defined": A parameter is not in scope in an attribute on that parameter, or any parameter in the method
Function f()
  nameof(f)
End Function
' result "f": VB-specific. This is resolved as an expression which binds to the implicit function return variable
NameOf(New)
' result error "this expression does not have a name": VB-specific. Not one of the allowed forms of nameof. Note that New is not a name; it is a keyword used for construction.
Class C
  Dim x As Integer
  Dim s As String = NameOf(x)
End Class
' result "x": Field (simple name lookup)
class C {
   int x;
   string s = nameof(x);
}
// result "x". Field (simple name lookup)
class C {
   static int x;
   string s = nameof(x);
}
// result "x". Field (simple name lookup)
class C {
   int x;
   string s = nameof(C.x);
}
// result "x". Member (member access)
class C {
   int x;
   string s = nameof(default(C).x);
}
// result error "This expression isn't allowed in a nameof argument" - default.
struct S {
   int x;
   S() {var s = nameof(x); ...}
}
// result "x": Field access (simple name lookup). Nameof argument is considered unreachable, and so this doesn't violate definite assignment.
int x; ... nameof(x); x=1;
// result "x": Local access (simple name lookup). Nameof argument is unreachable, and so this doesn't violate definite assignment.
int x; nameof(f(ref x));
// result error "this expression does not have a name".
var @int=5; nameof(@int)
// result "int": C#-specific. Local (simple name lookup). The leading @ is removed.
nameof(m\u200c\u0065)
// result "me": C#-specific. The Unicode escapes are first resolved, and the formatting character \u200c is removed.
Dim [Sub]=5 : NameOf([Sub])
' result "Sub": VB-specific. Local (simple name lookup). The surrounding [.] is removed.
class C {
  class D {}
  class D<T> {}
  nameof(C.D)
}
// result "D" and binds to the non-generic form: member access only finds the type with the matching arity.
class C<T> where T:Exception {
  ... nameof(C<string>)
}
// result error: the type 'string' doesn't satisfy the constraints

String Interpolation for C#

An interpolated string is a way to construct a value of type String (or IFormattable) by writing the text of the string along with expressions that will fill in "holes" in the string. The compiler constructs a format string and a sequence of fill-in values from the interpolated string.

When it is treated as a value of type String, it is a shorthand for an invocation of

String.Format(string format, params object args[])

When it is converted to the type IFormattable, the result of the string interpolation is an object that stores a compiler-constructed format string along with an array storing the evaluated expressions. The object's implementation of

IFormattable.ToString(string format, IFormatProvider formatProvider)

is an invocation of

String.Format(IFormatProviders provider, String format, params object args[])

By taking advantage of the conversion from an interpolated string expression to IFormattable, the user can cause the formatting to take place later in a selected locale. See the section System.Runtime.CompilerServices.FormattedString for details.

Note: the converted interpolated string may have more "holes" in the format string than there were interpolated expression holes in the interpolated string. That is because some characters (such as "\{" "}") may be translated into a hole and a corresponding compiler-generated fill-in.

Lexical Grammar

An interpolated string is treated initially as a token with the following lexical grammar:

interpolated-string:
    $ " "
    $ " interpolated-string-literal-characters "

interpolated-string-literal-characters:
    interpolated-string-literal-part interpolated-string-literal-parts
    interpolated-string-literal-part

interpolated-string-literal-part:
    single-interpolated-string-literal-character
    simple-escape-sequence
    hexadecimal-escape-sequence
    unicode-escape-sequence
    interpolation

simple-escape-sequence:  one of
    \'  \"  \\  \0  \a  \b  \f  \n  \r  \t  \v  \{  \}

single-interpolated-string-literal-character:
    Any character except " (U+0022), \ (U+005C), { (U+007B) and new-line-character

interpolation:
    { interpolation-contents }

interpolation-contents:
    balanced-text
    balanced-text : interpolation-format

balanced-text:
    balanced-text-part
    balanced-text-part balanced-text

balanced-text-part
    Any character except ", (, [, {, /, \ and new-line-character
    ( balanced-text )
    { balanced-text }
    [ balanced-text ]
    regular-string-literal
    delimited-comment
    unicode-escape-sequence
    / after-slash

after-slash
    Any character except ", (, [, {, /, \, * and new-line-character
    ( balanced-text )
    { balanced-text }
    [ balanced-text ]
    regular-string-literal
    * delimited-comment-text[opt] asterisks /
    unicode-escape-sequence

interpolation-format:
    regular-string-literal
    literal-interpolation-format

literal-interpolation-format:
    interpolation-format-part
    interpolation-format-part literal-interpolation-format

interpolation-format-part
    Any character except ", :, \, } and new-line-character

With the additional restriction that a delimited-comment-text that is a balanced-text-part may not contain a new-line-character.

This lexical grammar is ambiguous in that it allows a colon appearing in interpolation-contents to be considered part of the balanced-text, or as the separator between the balanced-text and the interpolation-format. This ambiguity is resolved by considering it to be a separator between the balanced-text and interpolation-format.

Syntactic Grammar

An interpolated-string token is reclassified, and portions of it are reprocessed lexically and syntactically, during syntactic analysis as follows:

  • If the interpolated-string contains no interpolation, then it is reclassified as a regular-string-literal.
  • Otherwise
    • the portion of the interpolated-string before the first interpolation is reclassified as an interpolated-string-start terminal;
    • the portion of the interpolated-string after the last interpolation is reclassified as an interpolated-string-end terminal;
    • the portion of the interpolated-string between one interpolation and another interpolation is reclassified as an interpolated-string-mid terminal;
    • the balanced-text of each interpolation-contents is reprocessed according to the language's lexical grammar, yielding a sequence of terminals;
    • the colon in each interpolation-contents that contains an interpolation-format is classified as a colon terminal;
    • each interpolation-format is reclassified as a regular-string-literal terminal; and
    • the resulting sequence of terminals undergoes syntactic analysis as an interpolated-string-expression.
expression:
    interpolated-string-expression

interpolated-string-expression:
    interpolated-string-start interpolations interpolated-string-end

interpolations:
    single-interpolation
    single-interpolation interpolated-string-mid interpolations

single-interpolation:
    interpolation-start
    interpolation-start : regular-string-literal

interpolation-start:
    expression
    expression , expression

Semantics

An interpolated-string-expression has type string, but there is an implicit conversion from expression from an interpolated-string-expression to the type System.IFormattable. By the existing rules of the language (7.5.3.3 Better conversion from expression), the conversion to string is a better conversion from expression.

An interpolated-string-expression is translated into an intermediate format string and object array which capture the contents of the interpolated string using the semantics of Composite Formatting. If treated as a value of type string, the formatting is performed using string.Format(string format, params object[] args) or equivalent code. If it is converted to System.IFormattable, an object of type System.Runtime.CompilerServices.FormattedString is constructed using the format string and argument array, and that object is the value of the interpolated-string-expression.

The format string is constructed of the literal portions of the interpolated-string-start, interpolated-string-mid, and interpolated-string-end portions of the expression, with special treatment for { and } characters (see Notes).

The evaluation order needs to be specified.

The definite assignment rules need to be specified.

single-interpolation Semantics

This section should describe in detail the construction of a format item from a single-interpolation, and the corresponding element of the object array.

If an interpolation-start has a comma and a second expression, the second expression must evaluate to a compile-time constant of type int, which is used as the alignment of a format item.

If a single-interpolation has a colon and a regular-string-literal, then the string literal is used as the formatString of a format item.

Notes

The compiler is free to translate an interpolated string into a format string and object array where the number of objects in the object array is not the same as the number of interpolations in the interpolated-string-expression. In particular, the compiler may translate { and } characters into a fill-in in the format string and a corresponding string literal containing the character. For example, the interpolated string $"\{ {n} \}" may be translated to String.Format("{0} {1} {2}", "{", n, "}").

The compiler is free to use any overload of String.Format in the translated code, as long as doing so preserves the semantics of calling string.Format(string format, params object[] args).

Examples

The interpolated string

$"{hello}, {world}!"

is translated to

String.Format("{0}, {1}!", hello, world)

The interpolated string

$"Name = {myName}, hours = {DateTime.Now:hh}"

is translated to

String.Format("Name = {0}, hours = {1:hh}", myName, DateTime.Now)

The interpolated string

$"\{{6234:D}\}"

is translated to

String.Format("{0}{1:D}{2}", "{", 6234, "}")

For example, if you want to format something in the invariant locale, you can do so using this helper method

public static string INV(IFormattable formattable)
{
    return formattable.ToString(null, System.Globalization.CultureInfo.InvariantCulture);
}

and writing your interpolated strings this way

   string coordinates = INV("longitude={longitude}; latitude={latitude}");

System.Runtime.CompilerServices.FormattedString

The following platform class is used to translate an interpolated string to the type System.IFormattable.

namespace System.Runtime.CompilerServices 
{ 
    public class FormattedString : System.IFormattable 
    {
        private readonly String format;
        private readonly object[] args;
        public FormattedString(String format, params object[] args)
        {
            this.format = format;
            this.args = args;
        }
        string IFormattable.ToString(string ignored, IFormatProvider formatProvider)
        {
            return String.Format(formatProvider, format, args);
        }
    } 
} 

Issues

  1. As specified, an interpolated string with no interpolations cannot be converted to IFormattable because it is a string literal. It should have such a conversion.