[C#]`string` and `char` constants are not embedded as literals in string interpolation
Version Used:
main (10 Jan 2024) in sharplab
Steps to Reproduce:
using System;
public class C {
public string M() {
const string a = "S";
const int b = 1;
const char c = 'C';
return $"foo{a}bar{c}{b}";
}
}
.NET: https://sharplab.io/#v2:EYLgtghglgdgNAExAagD4AEBMBGAsAKHQGYACLEgYRIG8CT6zT1sAGEgWQAoBKGuhgQGMA9jADOAFzKsSEEgF4SAIgDKSgNz8B9EeKmwpwBSWyb82hrsklBACwgAnG8YDkFF2Yv10AdhIASJQAzYWFqCABfYEdqQQjqYAiNLRIIggigA
.NET Framework: https://sharplab.io/#v2:EYLgHgbALANAJiA1AHwAICYCMBYAUKgZgAIMiBhIgbzyNpONUwAYiBZACgEoqa6+BjAPYA7AM4AXEsyIBDIgF4iAIgDKSgNy8+tIWMkBLYZOAKimTbm11dEovwAWMgE53TAcjJuLV2qgDsRAAkSgBmgoKUMgC+wM6U/FGUwFEaWkRReFFAA=
Imports System
Public Class C
Public Function M() As string
const a = "S"
const b = 1
const c = "C"c
return $"foo{a}bar{c}{b}"
End Function
End Class
https://sharplab.io/#v2:EYLgtghglgdgNAGxAN2HAJiA1AHwJJgAOA9gE4AuAzgAQDKAnpeQKZgCwAUAAoCuwCUAMbUAwggiUaI6p2pzqvfkOoAxHjEHkoxGNQCyACgCUcgII0mpWAHNZ8+4J1NqEagF5qAIlqe79uY4wzsDu1ACMfv6BzsIeniKegpH2pMzkPKS6ACSeAGbExADeEAC+wBCkhYIlhcAlvhz2AKIw6KrqmtownC1tYhKUnEA
Diagnostic Id:
N/A
Expected Behavior:
.NET:
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue | DebuggableAttribute.DebuggingModes.DisableOptimizations)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
public class C
{
[NullableContext(1)]
public string M()
{
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(8, 1);
defaultInterpolatedStringHandler.AppendLiteral("fooSbarC");
defaultInterpolatedStringHandler.AppendFormatted(1);
return defaultInterpolatedStringHandler.ToStringAndClear();
}
}
.NET Framework:
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
using Microsoft.CodeAnalysis;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public readonly int Version;
public RefSafetyRulesAttribute(int P_0)
{
Version = P_0;
}
}
}
public class C
{
public string M()
{
return string.Format("fooSbarC{0}", 1);
}
}
VB:
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue | DebuggableAttribute.DebuggingModes.DisableOptimizations)]
[assembly: AssemblyVersion("0.0.0.0")]
public class C
{
public string M()
{
return string.Format("fooSbarC{0}", 1);
}
}
Actual Behavior:
.NET:
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue | DebuggableAttribute.DebuggingModes.DisableOptimizations)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
public class C
{
[NullableContext(1)]
public string M()
{
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(6, 3);
defaultInterpolatedStringHandler.AppendLiteral("foo");
defaultInterpolatedStringHandler.AppendFormatted("S");
defaultInterpolatedStringHandler.AppendLiteral("bar");
defaultInterpolatedStringHandler.AppendFormatted('C');
defaultInterpolatedStringHandler.AppendFormatted(1);
return defaultInterpolatedStringHandler.ToStringAndClear();
}
}
.NET Framework:
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;
using Microsoft.CodeAnalysis;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public readonly int Version;
public RefSafetyRulesAttribute(int P_0)
{
Version = P_0;
}
}
}
public class C
{
public string M()
{
return string.Format("foo{0}bar{1}{2}", "S", 'C', 1);
}
}
VB:
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue | DebuggableAttribute.DebuggingModes.DisableOptimizations)]
[assembly: AssemblyVersion("0.0.0.0")]
public class C
{
public string M()
{
return string.Format("foo{0}bar{1}{2}", "S", 'C', 1);
}
}
In contrast to this, the string concatenation operator (C#: + / VB: &) handles these types of constants well.
This is one of the ways in which interpolated strings is inferior to the concatenation operator.
Marking for compiler team to assign a buddy for the PR contribution.
@333fred pretty sure this is "By Design" but want to let you confirm.
No, this is not by design. We could adjust this behavior as an optimization.
- Should this optimization be suppressed in some cases, e.g. in lambda? (See https://github.com/dotnet/roslyn/pull/27411)
- Should we accept
const string = $"foo {'b'}ar";in C# 12 or only future versions?- If this should be accepted only in future C#, do we provide this optimization for char constants without accepting the above statement?
- That's not about lambdas in general, it's about expression trees. And yes, expression trees need to reflect the code as written.
- This would not be a language feature and should not be tied to language version. It should be entirely invisible to compiled code; it should just get marginally faster with no observable side effects.
It's about expression trees. And yes, expression trees need to reflect the code as written.
I managed to consider them in my PR but it looks like some code is not sufficient quality.
This would not be a language feature and should not be tied to language version. It should be entirely invisible to compiled code; it should just get marginally faster with no observable side effects.
We will not have to wait for the next version of .NET in the fix for C#. This change will be appreciated in VB too because of the existences of vbLf, ChrW (especially e.g. ChrW(&H201C) & ChrW(&H201D)), and NameOf, but it can be postponed to the next version of .NET to be tested more thoroughly in preview versions.
vblang's issue: https://github.com/dotnet/vblang/issues/369
but it looks like some code is not sufficient quality.
Fixed. I do not think that I have to make additional changes except for appending additional tests now.
My current PR for C# only aims at only interpolated strings that return string values (not other types of handlers or formattables).
If the code satisfies one of the following condition, I think it should be as is:
- Third party (not in-BCL) custom handlers not in
System.orIFormattableother thanFormattableString - The constructor argument for
IFormatProvider(2nd one) of BCL handlers is a class implementingICustomFormatter FormattableStringother than an argument ofFormattableString.Invariant(or.CurrentCulture)- In other forms it is difficult to detect calls for
.GetArgument(s)
- In other forms it is difficult to detect calls for
- In an expression tree
Should we add a new attribute to mark handlers as those that treat string and character variables in the same way as string literals?
I've forgotten to paste the link to an alive PR here: https://github.com/dotnet/roslyn/pull/72308