Pull to refresh

Comments 21

Удивляет то, что интерполяция строки иногда компилируется в вызов 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).

Ну вот и не совсем понятно, почему он превращает именно в string.Format, а не в Concat. Конечно, явно есть случаи, когда так сделать нельзя, но кажется, что почти всегда интерполяцию вполне можно записать как сумму строк.

Потому что это краткая и более удобная форма вызова string.Format. Далее компилятор может заменить на конкатенацию, если ему так покажется правильней.

Не интересовался, если есть желание, посмотрите в исходниках Roslyn.

Но ToString() ведь все равно приведет к аллокации, возможно даже больше размером.

Тут штука вот в чëм: ToString в любом случае будет вызван. Либо у исходной переменной, либо у ссылки, полученной после упаковки. В статье же говорится о том, как упаковки избежать, имея исключительно вызов ToString.

!!x - это ссылка на x-овый generic parameter метода. То есть в DefaultInterpolatedStringHandler::AppendFormatted<int32>(!!0) - !!0 ссылается на int32.
Таким же образом, !x - это ссылка на x-овый generic parameter типа

Нет смысла смотреть на IL там, где JIT всё равно сделает своё чёрное дело и перетрясёт код до неузнаваемости.

Ну, на эту тему в статье есть раздел "Оптимизации времени выполнения". Или мб какие-то конкретные примеры из статьи работают не так, как описано?

Sign up to leave a comment.