Comments 21
Интерполяция строк и C#10: рекомендую перечитать хорошую статью https://habr.com/ru/articles/590069/ из которой можно выяснить, что в некоторых случаях даже конкатенация строк может не выполнятся.
Удивляет то, что интерполяция строки иногда компилируется в вызов String.Format. Почему так происходит, если конкатенация строк была бы куда выгоднее?
"Простая конкатенация" может быть сильно хуже форматирования в сложных случаях (ну там вставить тысячу полей в многомегабайтный текст). А откуда компилятору заранее знать насколько сложная там разбивка строки? Нет, он конечно может провести некоторый анализ - и это было сделано в процессе развития языка, но в старых версиях остался более "надёжный", но менее оптимальный подход.
Либо статья вводит в заблуждение, либо я делаю что-то не так, но у меня Visual Studio 2022 выдаёт совершенно другой IL код для _ = $"{str} {num}"
.
IL код большой и его я закинул под спойлер(ибо он слишком большой), но вот как бы это выглядело, если перевести обратно в C#:
[NullableContext(1)]
[CompilerGenerated]
internal static void <<Main>$>g__Foo|0_0(string str, int num)
{
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(1, 2);
defaultInterpolatedStringHandler.AppendFormatted(str);
defaultInterpolatedStringHandler.AppendLiteral(" ");
defaultInterpolatedStringHandler.AppendFormatted<int>(num);
defaultInterpolatedStringHandler.ToStringAndClear();
}
IL код
// Token: 0x06000007 RID: 7 RVA: 0x000020A0 File Offset: 0x000002A0
.method assembly hidebysig static
void '<<Main>$>g__Foo|0_0' (
string str,
int32 num
) cil managed
{
.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
01 00 01 00 00
)
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
)
// Header Size: 12 bytes
// Code Size: 50 (0x32) bytes
// LocalVarSig Token: 0x11000001 RID: 1
.maxstack 3
.locals init (
[0] valuetype [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler
)
/* (2,1)-(2,2) C:\Users\Professional\Projects\CSharp\test_cs\test_cs\Program.cs */
/* 0x000002AC 00 */ IL_0000: nop
/* (3,5)-(3,24) C:\Users\Professional\Projects\CSharp\test_cs\test_cs\Program.cs */
/* 0x000002AD 1200 */ IL_0001: ldloca.s V_0
/* 0x000002AF 17 */ IL_0003: ldc.i4.1
/* 0x000002B0 18 */ IL_0004: ldc.i4.2
/* 0x000002B1 280F00000A */ IL_0005: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::.ctor(int32, int32)
/* 0x000002B6 1200 */ IL_000A: ldloca.s V_0
/* 0x000002B8 02 */ IL_000C: ldarg.0
/* 0x000002B9 281000000A */ IL_000D: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendFormatted(string)
/* 0x000002BE 00 */ IL_0012: nop
/* 0x000002BF 1200 */ IL_0013: ldloca.s V_0
/* 0x000002C1 7201000070 */ IL_0015: ldstr " "
/* 0x000002C6 281100000A */ IL_001A: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendLiteral(string)
/* 0x000002CB 00 */ IL_001F: nop
/* 0x000002CC 1200 */ IL_0020: ldloca.s V_0
/* 0x000002CE 03 */ IL_0022: ldarg.1
/* 0x000002CF 280100002B */ IL_0023: call instance void [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0)
/* 0x000002D4 00 */ IL_0028: nop
/* 0x000002D5 1200 */ IL_0029: ldloca.s V_0
/* 0x000002D7 281300000A */ IL_002B: call instance string [System.Runtime]System.Runtime.CompilerServices.DefaultInterpolatedStringHandler::ToStringAndClear()
/* 0x000002DC 26 */ IL_0030: pop
/* (4,1)-(4,2) C:\Users\Professional\Projects\CSharp\test_cs\test_cs\Program.cs */
/* 0x000002DD 2A */ IL_0031: ret
} // end of method Program::'<<Main>$>g__Foo|0_0'
В статье описано, что это зависит от того, подо что вы компилируете код. У вас net6?
У вас net6?
Да, я компилировал под net6, а в статье описывается как это всё компилируется под net5. А в заключении ещё и описано то, какой код будет сгенерирован под net6
Не знаю, как так вышло, что я всё это проглядел. Прочитать статью внимательнее я времени не нашёл, но зато проверить всё самому — это запросто. Дурак я, в общем.
Потому что это виртуальная фича, она переводит строку в String.Format на этапе разбора. Я когда писал функцию string.Format для nanoFramework интерполяция сразу заработала. Таких фич у C# навалом. А компилятор уже потом оптимизирует string.Format.
Не могли бы вы поподробнее пояснить, что вы имеете ввиду? @ValeryIvanov спросил, почему компилятор превращает интерполяцию в string.Format, а не в обычное a + b + c. Что означает "компилятор уже потом оптимизирует string.Format"? Вы имеете ввиду JIT?
Это синтаксический сахар, в компилятор он уже попадает как string.Format. Таких вещей в C# сделано много, что бы не переписывать компилятор из-за каждой мелочи.
Вот выдержка из New-Language-Features-in-VB-14.md:
Note that, since it's shorthand for the specified call to String.Format, (1) string interpolation uses the current culture, and (2) it isn't a constant. However the compiler is at liberty to optimize string interpolation if it knows how String.Format will behave and if it can figure a faster way to do that (e.g. by avoiding boxing).
Но ToString() ведь все равно приведет к аллокации, возможно даже больше размером.
!!x
- это ссылка на x-овый generic parameter метода. То есть в DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0)
- !!0
ссылается на int32
.
Таким же образом, !x
- это ссылка на x-овый generic parameter типа
Нет смысла смотреть на IL там, где JIT всё равно сделает своё чёрное дело и перетрясёт код до неузнаваемости.
Всегда ли в C# есть упаковка при конкатенации со строкой и интерполяции?