-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Discussion: Proposal to make FormattableString a struct #3981
Comments
To answer your question:
No, it was not considered. And I'm glad. I do not think the benefits of this proposal overcome its conceptual surface area. Would the language specification have special rules for each of A proposal like this would have to have been worked out in detail and implemented a few months ago to have any real chance of affecting the upcoming release. |
The proposal is for just one |
@justinvp Your proposal is for a change to the platform libraries. If you are proposing a language change, please spell it out. What would be the language specification for the interpolated string conversions? What conversions are defined by the language and what are their semantics? |
@gafter Exactly. I'm not proposing a language change. I haven't seen an up-to-date language spec for this feature, so I can't easily say whether there's an actual diff to how the spec is currently written, but the semantics would be the same as it is currently. When converting an interpolated-string-expression to The only "language change" is in the compiler implementations, because the compilers currently look for the protected override Conversion GetInterpolatedStringConversion(BoundInterpolatedString source, TypeSymbol destination, ref HashSet<DiagnosticInfo> useSiteDiagnostics)
{
// An interpolated string expression may be converted to the types
// System.IFormattable and System.FormattableString
return (destination == Compilation.GetWellKnownType(WellKnownType.System_IFormattable) ||
destination == Compilation.GetWellKnownType(WellKnownType.System_FormattableString))
? Conversion.InterpolatedString : Conversion.NoConversion;
}
Here's an example (it's the exact same process as right now): Assume the platform libraries look like this: namespace System
{
public static class FormattableString
{
public static string Invariant<TArguments>(FormattableString<TArguments> formattable) where TArguments : IFormattable
}
public struct FormattableString<TArguments> : IEquatable<FormattableString<TArguments>>, IFormattable
where TArguments : IFormattable
{
public FormattableString(string format, TArguments arguments);
public string Format { get; }
public TArguments Arguments { get; }
public override string ToString();
public string ToString(IFormatProvider formatProvider);
string IFormattable.ToString(string ignored, IFormatProvider formatProvider);
public bool Equals(FormattableString<TArguments> other);
public override bool Equals(object obj);
public override int GetHashCode();
}
}
namespace System.Runtime.CompilerServices
{
public static class FormattableStringFactory
{
public static FormattableString<Arguments> Create(string format, params object[] arguments);
public struct Arguments : IEquatable<Arguments>, IFormattable
{
public Arguments(object[] arguments);
public string ToString(string format, IFormatProvider formatProvider);
public bool Equals(Arguments other);
public override bool Equals(object obj);
public override int GetHashCode();
}
}
} If someone has code like this: float latitude = 47.644961f;
float longitude = -122.130690f;
string coords = FormattableString.Invariant($"({latitude},{longitude})"); The same exact language conversion lowering process is used to convert However, if optimized overloads of namespace System.Runtime.CompilerServices
{
public static class FormattableStringFactory
{
public static FormattableString<Arguments<T>> Create<T>(string format, T argument);
public static FormattableString<Arguments<T1, T2>> Create<T1, T2>(string format, T1 argument1, T2 argument2);
public static FormattableString<Arguments<T1, T2, T3>> Create<T1, T2, T3>(string format, T1 argument1, T2 argument2, T3 argument3);
public struct Arguments<T> : IEquatable<Arguments<T>>, IFormattable
{
public Arguments(T argument);
public string ToString(string format, IFormatProvider formatProvider);
public bool Equals(Arguments<T> other);
public override bool Equals(object obj);
public override int GetHashCode();
}
public struct Arguments<T1, T2> : IEquatable<Arguments<T1, T2>>, IFormattable
{
public Arguments(T1 argument1, T2 argument2);
public string ToString(string format, IFormatProvider formatProvider);
public bool Equals(Arguments<T1, T2> other);
public override bool Equals(object obj);
public override int GetHashCode();
}
public struct Arguments<T1, T2, T3> : IEquatable<Arguments<T1, T2, T3>>, IFormattable
{
public Arguments(T1 argument1, T2 argument2, T3 argument3);
public string ToString(string format, IFormatProvider formatProvider);
public bool Equals(Arguments<T1, T2, T3> other);
public override bool Equals(object obj);
public override int GetHashCode();
}
}
} Then the compiler would end up calling the |
You suggest an interpolated string conversion is available when there is a conversion to |
Perhaps you intend the conversion involve type inference to infer the type of And to get into the current release, we'd need it about 5-6 months ago. |
@AlexGhiondea, @jaredpar, @nguerrera, @ericstj:
I'd like to discuss a potential alternative approach to
FormattableString
that avoids the heap allocation associated with this type being a class and still allows for the same optimizations of the current design with minimal impact to the language/compiler. If there's a better place for this discussion, please let me know.Was a design along the lines of the proposal (below) considered, and if so, why was it rejected?
Background
Here's the current design of the new
System.FormattableString
andSystem.Runtime.CompilerServices.FormattableStringFactory
types:These types enable the new string interpolation language feature to format strings using the invariant culture:
Which compiles as:
The upside of this design is that it affords future optimizations that can avoid the argument array allocation and boxing when there are 1, 2, and 3 arguments (via private subclasses of
FormattableString
):The downside is that this always results in a heap allocation of
System.FormattableString
.This means that using the string interpolation feature to format with the invariant culture in this way will always be more costly than the default behavior of formatting with the current culture (assuming similar optimized overloads to
String.Format
).Proposal
Here's an alternative design that replaces the
FormattableString
class with aFormattableString<TArgument>
struct:Future optimized overloads would look like the following:
The huge upside: no GC heap allocation for the class when using string interpolation with the invariant culture.
This does require additional
Arguments
struct types. But these types could live in theSystem.Runtime.CompilerServices
namespace (and can be nested withinFormattableStringFactory
) where users wouldn't have to know or care about them. Users would use theInvariant
method with this approach in the exact same way as with the current design, with the benefit of no heap allocation.This approach fits well with apparent future related work:
System.Text.Formatting
StringBuilder.AppendFormat
in potential futureSystem.Text.Formatting
APIs that enables the use of the string interpolation language feature with these APIs without theFormattableString
GC heap allocationparams Arguments<T>
to avoid the params array allocationArguments
structs (specific toFormattableString<TArguments>
) be the same as theparams Arguments<T>
struct(s). I just wanted to call attention to the same motivation behind the feature (avoiding unnecessary allocations)Additional Benefit
As an added bonus, the proposed design enables a new scenario: "deferred string formatting" for APIs that want to avoid all allocations (even avoiding the allocation of the formatted string itself) if the formatting doesn't need to happen.
For example, logging APIs like NLog typically have methods that avoid calling
String.Format
and avoid allocating the arguments array and boxing when a log level isn't enabled:Usage:
The proposed design would allow the string interpolation language feature to be used in such APIs without any allocation penalty. Such methods would be similar to
FormattableString.Invariant
, with an implementation that could choose not to callToString
at all if the particular log level isn't enabled, in which case no allocations would occur:Usage:
Conclusion
I have an implementation of this approach and would be happy to submit a Pull Request (even if folks just want to see what the implementation looks like for discussion).
The compilers (and associated tests) would need to be updated to look for the generic
FormattableString<TArgument>
struct instead of the non-genericFormattableString
class and allow the result of theFormattableStringFactory.Create
call to be passed to generic methods likeFormattableString.Invariant
.The text was updated successfully, but these errors were encountered: