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:
- 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.
- 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"
- 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.
- 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.
- 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.
- 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". Alsostring 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. - 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.
- 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
- 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.