Skip to content

Instantly share code, notes, and snippets.

@ufcpp
Last active August 14, 2021 09:38
Show Gist options
  • Save ufcpp/233d33b0220215bb5bdf2a10e8a434bf to your computer and use it in GitHub Desktop.
Save ufcpp/233d33b0220215bb5bdf2a10e8a434bf to your computer and use it in GitHub Desktop.
Improved Interpolated Strings (C# 10.0 新機能。 .NET 6 Preview 7/VS 2020 Preview 3以降で有効)
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Globalization;
using System.Runtime.CompilerServices;
BenchmarkRunner.Run<StringInterpolationBenchmark>();
[MemoryDiagnoser]
public class StringInterpolationBenchmark
{
private const int N = 10;
private const int InitialBufferSize = 32;
[Benchmark]
public void OldStyle()
{
for (int a = 0; a < N; a++)
for (int b = 0; b < N; b++)
for (int c = 0; c < N; c++)
for (int d = 0; d < N; d++)
m(a, b, c, d);
// 昔の $"{a}.{b}.{c}.{d}" は string.Format に展開されてた。
// C# 10.0 以降でも、DefaultInterpolatedStringHandler がない(TargetFramework が .NET 5 以下とか)だとこれになる。
static string m(int a, int b, int c, int d) => string.Format("{0}.{1}.{2}.{3}", a, b, c, d);
}
[Benchmark]
public void Improved()
{
for (int a = 0; a < N; a++)
for (int b = 0; b < N; b++)
for (int c = 0; c < N; c++)
for (int d = 0; d < N; d++)
m(a, b, c, d);
static string m(int a, int b, int c, int d) => $"{a}.{b}.{c}.{d}";
}
private static readonly CultureInfo _currentCulture = CultureInfo.CurrentCulture;
private static readonly CultureInfo _invariantCulture = CultureInfo.InvariantCulture;
[Benchmark]
public void InvariantCulture()
{
for (int a = 0; a < N; a++)
for (int b = 0; b < N; b++)
for (int c = 0; c < N; c++)
for (int d = 0; d < N; d++)
m(a, b, c, d);
static string m(int a, int b, int c, int d) => string.Create(_invariantCulture, $"{a}.{b}.{c}.{d}");
}
[Benchmark]
public void InitialBuffer()
{
for (int a = 0; a < N; a++)
for (int b = 0; b < N; b++)
for (int c = 0; c < N; c++)
for (int d = 0; d < N; d++)
m(a, b, c, d);
static string m(int a, int b, int c, int d) => string.Create(_currentCulture, stackalloc char[InitialBufferSize], $"{a}.{b}.{c}.{d}");
}
[Benchmark]
public void InitialBufferInvariantCulture()
{
for (int a = 0; a < N; a++)
for (int b = 0; b < N; b++)
for (int c = 0; c < N; c++)
for (int d = 0; d < N; d++)
m(a, b, c, d);
static string m(int a, int b, int c, int d) => string.Create(_invariantCulture, stackalloc char[InitialBufferSize], $"{a}.{b}.{c}.{d}");
}
[Benchmark]
public void InitialBufferSkipLocalsInitInvariantCulture()
{
for (int a = 0; a < N; a++)
for (int b = 0; b < N; b++)
for (int c = 0; c < N; c++)
for (int d = 0; d < N; d++)
m(a, b, c, d);
[SkipLocalsInit]
static string m(int a, int b, int c, int d) => string.Create(_invariantCulture, stackalloc char[InitialBufferSize], $"{a}.{b}.{c}.{d}");
}
[Benchmark]
[SkipLocalsInit]
public void InitialSingleBufferInvariantCulture()
{
Span<char> buffer = stackalloc char[InitialBufferSize];
for (int a = 0; a < N; a++)
for (int b = 0; b < N; b++)
for (int c = 0; c < N; c++)
for (int d = 0; d < N; d++)
m(a, b, c, d, buffer);
string m(int a, int b, int c, int d, Span<char> buffer) => string.Create(_invariantCulture, buffer, $"{a}.{b}.{c}.{d}");
}
}
using System.Globalization;
var x = 1234;
var y = 1.234;
var z = new DateOnly(2001, 2, 3);
// 既存の構文のまま、コンパイル結果が変わるみたい。
// - DefaultInterpolatedStringHandler 型があるかどうかで分岐してそう
// - この場合、コンパイル結果は後述の DefaultInterpolatedStringHandler への代入 → ToStringAndClear と一緒。
// - 再コンパイル必須(再コンパイルしないと string.Format 呼び出しのまま)
// - ちなみに、CurrentCulture を参照
string s = $"{x} / {y} / {z}";
Console.WriteLine(s);
// DefaultInterpolatedStringHandler などを始めとする
// 所定のパターン (AppendLiteral, AppendFormatted メソッドを持ってる) を満たす型への代入すると、
// AppendLiteral, AppendFormatted メソッド呼び出しに展開される。
System.Runtime.CompilerServices.DefaultInterpolatedStringHandler h = $"{x} / {y} / {z}";
Console.WriteLine(h.ToStringAndClear());
// 上記コードは以下のようなコードとほぼ一緒。
h = new();
h.AppendFormatted(x);
h.AppendLiteral(" / ");
h.AppendFormatted(y);
h.AppendLiteral(" / ");
h.AppendFormatted(z);
Console.WriteLine(h.ToStringAndClear());
// ちなみに、 DefaultInterpolatedStringHandler は ArrayPool から配列を借り出してるので、
// 最後に ToStringAndClear を呼ばないと Pool への変換処理が掛からなくてまずい。
// この罠(マジでメモリリーク)があるので、DefaultInterpolatedStringHandler はあんまり直接触られたくはなさそう。
// ほとんどの場合、string s = $"" で事足りるし、細かいカスタマイズも下記の string.Create でできるはず。
// カルチャー指定。
// string.Create(IFormatProvider, DefaultInterpolatedStringHandler) が呼ばれてる。
var culture = new CultureInfo("fr-fr");
s = string.Create(culture, $"{x} / {y} / {z}");
Console.WriteLine(s);
// 初期バッファー渡す。
// string.Create(IFormatProvider, Span<char>, DefaultInterpolatedStringHandler) が呼ばれてる。
// 最速を目指すならその string.Create 多用することになるはず。
// (InvariantCulture と CurrentCulture でもパフォーマンス変わるかも?)
culture = CultureInfo.InvariantCulture;
s = string.Create(culture, stackalloc char[512], $"{x} / {y} / {z}");
Console.WriteLine(s);
// ↑の例で、IFormatProvider, Span<char> が DefaultInterpolatedStringHandler に渡すのに、
// InterpolatedStringHandlerArgument 属性が使われてる。
// DummyHandler の方が呼ばれる。
C.M($"{x} / {y} / {z}");
// これが string の方になるのはいいとして…
// (M(string) がないとコンパイル エラー。)
C.M("");
// この2つも string の方になる。
// 評価結果が const string になっちゃう $"" はただの string 扱い。
// (ただし、M(string) がないと M(DummyHandler) の方に行く。)
C.M($"");
C.M($"{"abc"}");
class C
{
public static void M(string _) => Console.WriteLine("string");
public static void M(DummyHandler _) => Console.WriteLine("DummyHandler");
}
// これが $"" を受け取れる最低ラインのパターン。
[System.Runtime.CompilerServices.InterpolatedStringHandler]
public struct DummyHandler
{
public DummyHandler(int literalLength, int formattedCount) { }
// 追加で、任意の引数を足して、InterpolatedStringHandlerArgument 属性を介して受け取れる。
// あと、末尾に out bool 引数を足せば「以降の AppendLiteral/AppendFormatted は一切呼ばない」みたいな分岐もできる。
public void AppendLiteral(string s) { }
public void AppendFormatted<T>(T x, int alignment = 0, string? format = null) { }
// alignment, format 引数はなくてもいい。
// これらの引数がない場合、単に $"{value: X, 4}" みたいなのがコンパイル エラーになるだけ。
// 戻り値を bool にして、false を返すとそれ以降の Append は呼ばないみたいな分岐もできる。
}
using System.Runtime.CompilerServices;
var a = 1;
var b = false;
var c = 1.2;
var d = "abc";
// これで
// a: 1
// b: False
// c: 1.2
// d: abc
// になる。
// 濫用気味なトリックとして params Dictionary<string, T> 代わりにも使えたり。
// (ORM の類でなら実用できるかも。)
write<object>($"{a}{b}{c}{d}");
// リテラル直渡ししたときが微妙な挙動だけども…
// 1: 1
// 2: 2
// a: 1
write<int>($"{1}{2}{a}");
static void write<TValue>(ParamsDictionaryHandler<TValue> handler)
{
var dic = handler.Dictionary;
foreach (var (key, value) in dic)
{
Console.WriteLine($"{key}: {value}");
}
}
[InterpolatedStringHandler]
public struct ParamsDictionaryHandler<T>
{
internal readonly Dictionary<string, T> Dictionary;
public ParamsDictionaryHandler(int _, int formattedCount) => Dictionary = new(formattedCount);
public void AppendFormatted(T value, [CallerArgumentExpression("value")] string? ex = null)
{
if (ex is null) return;
Dictionary[ex] = value;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment