Last active
August 14, 2021 09:38
-
-
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以降で有効)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 は呼ばないみたいな分岐もできる。 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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