はじめに
C# には空文字列の表現がいくつかあります。
const string ConstEmpty = "";
string readOnlyEmpty = string.Empty;
Assert.Equal(ConstEmpty, readOnlyEmpty);
今回はどれを使うのがいいか考えてみたいと思います。
-
string
の場合パフォーマンスはどれも同じ -
""
のほうがコード的には短い -
string.Empty
はデフォルト引数で使えない -
ReadOnlySpan<char>
も""
がよさげ
テストコード
テストコード
using System.Runtime.CompilerServices;
using Xunit;
public class __StringEmptyOrTest
{
[Fact]
void EmptyStringEquals()
{
const string ConstEmpty = "";
string readOnlyEmpty = string.Empty;
Assert.Equal(ConstEmpty, readOnlyEmpty);
ReadOnlySpan<char> constEmptySpan = "";
ReadOnlySpan<char> readOnlyEmptySpan = string.Empty;
ReadOnlySpan<char> collectionExpression = [];
ReadOnlySpan<char> defaultValue = default;
ReadOnlySpan<char> spanEmptyValue = ReadOnlySpan<char>.Empty;
ReadOnlySpan<char> nullStringValue = (string?)null;
Assert.Equal(constEmptySpan, readOnlyEmptySpan);
Assert.Equal(constEmptySpan, collectionExpression);
Assert.Equal(constEmptySpan, defaultValue);
Assert.Equal(constEmptySpan, spanEmptyValue);
Assert.Equal(constEmptySpan, nullStringValue);
}
[Fact]
void EmptyStringU8Equals()
{
ReadOnlySpan<byte> constEmptySpan = ""u8;
ReadOnlySpan<byte> collectionExpression = [];
ReadOnlySpan<byte> defaultValue = default;
ReadOnlySpan<byte> spanEmptyValue = ReadOnlySpan<byte>.Empty;
ReadOnlySpan<byte> nullByteArrayValue = (byte[]?)null;
Assert.Equal(constEmptySpan, collectionExpression);
Assert.Equal(constEmptySpan, spanEmptyValue);
Assert.Equal(constEmptySpan, defaultValue);
Assert.Equal(constEmptySpan, nullByteArrayValue);
}
static void StringPerformance(Performance p)
{
[MethodImpl(MethodImplOptions.NoInlining)]
int DummyFunc(string s) => s.Length + 1;
p.AddTest("\"\"", () =>
{
var count = 0;
for (int i = 0; i < 100000; i++)
count += DummyFunc("");
});
p.AddTest("string.Empty", () =>
{
var count = 0;
for (int i = 0; i < 100000; i++)
count += DummyFunc(string.Empty);
});
}
static void ReadOnlySpanPerformance(Performance p)
{
[MethodImpl(MethodImplOptions.NoInlining)]
int DummyFunc(ReadOnlySpan<char> s) => s.Length + 1;
p.AddTest("\"\"", () =>
{
var count = 0;
for (int i = 0; i < 100000; i++)
count += DummyFunc("");
});
p.AddTest("string.Empty", () =>
{
var count = 0;
for (int i = 0; i < 100000; i++)
count += DummyFunc("");
});
p.AddTest("[]", () =>
{
var count = 0;
for (int i = 0; i < 100000; i++)
count += DummyFunc([]);
});
p.AddTest("default", () =>
{
var count = 0;
for (int i = 0; i < 100000; i++)
count += DummyFunc(default);
});
p.AddTest("ReadOnlySpan.Empty", () =>
{
var count = 0;
for (int i = 0; i < 100000; i++)
count += DummyFunc(ReadOnlySpan<char>.Empty);
});
}
static void Utf8Performance(Performance p)
{
[MethodImpl(MethodImplOptions.NoInlining)]
int DummyFunc(ReadOnlySpan<byte> s) => s.Length + 1;
p.AddTest("\"\"u8", () =>
{
var count = 0;
for (int i = 0; i < 100000; i++)
count += DummyFunc(""u8);
});
p.AddTest("[]", () =>
{
var count = 0;
for (int i = 0; i < 100000; i++)
count += DummyFunc([]);
});
p.AddTest("default", () =>
{
var count = 0;
for (int i = 0; i < 100000; i++)
count += DummyFunc(default);
});
p.AddTest("ReadOnlySpan.Empty", () =>
{
var count = 0;
for (int i = 0; i < 100000; i++)
count += DummyFunc(ReadOnlySpan<byte>.Empty);
});
}
void StringDefault(string s = default!) { }
void StringNull(string s = null!) { }
void StringLiteral(string s = "") { }
// void StringLiteral(string s = string.Empty) { } // コンパイルエラー!
void SpanDefault(ReadOnlySpan<char> s = default) { }
void SpanNull(ReadOnlySpan<char> s = (string?)null) { }
void SpanLiteral(ReadOnlySpan<char> s = "") { }
// void SpanEmpty(ReadOnlySpan<char> s = ReadOnlySpan<char>.Empty) { } // コンパイルエラー!
void SpanU8Default(ReadOnlySpan<byte> s = default) { }
void SpanU8Null(ReadOnlySpan<byte> s = (byte[]?)null) { }
// void SpanU8Literal(ReadOnlySpan<byte> s = ""u8) { } // コンパイルエラー!
// void SpanU8Empty(ReadOnlySpan<byte> s = ReadOnlySpan<byte>.Empty) { } // コンパイルエラー!
[Fact]
void ReflectionAssignStringEmpty()
{
var info = typeof(string).GetField(nameof(string.Empty), System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)!;
Assert.Equal("", info.GetValue(null));
Assert.Throws<FieldAccessException>(() => info.SetValue(null, "NotEmptyString"));
}
[Fact]
void UnsafeAccessorAssignStringEmpty()
{
[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = nameof(string.Empty))]
static extern ref string GetEmptyString(string? _);
GetEmptyString(null) = "NotEmptyString";
// 実行時エラーにならず、値は書き換えられない
Assert.Equal("", string.Empty);
}
[Fact]
void UnsafeAssignStringEmpty()
{
ref var ptr = ref Unsafe.AsRef(in string.Empty);
ptr = "NotEmptyString";
Assert.Equal("NotEmptyString", ptr);
// 実行時エラーにならず、値は書き換えられない
Assert.Equal("", string.Empty);
}
private static readonly string MyEmpty = "";
[Fact]
unsafe void UnsafeAssignPointerStringEmpty()
{
#pragma warning disable CS8500
fixed (string* ptr = &string.Empty)
#pragma warning restore CS8500
{
*ptr = "NotEmptyString";
// string.Empty は書き換えられない
Assert.Equal("", string.Empty);
}
#pragma warning disable CS8500
fixed (string* ptr = &MyEmpty)
#pragma warning restore CS8500
{
*ptr = "NotEmptyString";
// ユーザー定義のフィールドは読み取り専用でも書き換えられる
Assert.Equal("NotEmptyString", MyEmpty);
}
}
}
file class MyRecord : System.ComponentModel.INotifyPropertyChanged
{
internal string Name { get => field; set => SetValue(ref field, value); } = "";
public event System.ComponentModel.PropertyChangedEventHandler?
PropertyChanged;
void SetValue<T>(ref T field, T value,
[System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
PropertyChanged?.Invoke(this,
new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
}
パフォーマンス比較
[MethodImpl(MethodImplOptions.NoInlining)]
int DummyFunc(string s) => s.Length + 1;
var count = 0;
for (int i = 0; i < 100000; i++)
count += DummyFunc("");
Test | Score | % | GC0 |
---|---|---|---|
x86 | |||
"" | 692 | 100.0% | 0 |
string.Empty | 649 | 93.8% | 0 |
x64 | |||
"" | 608 | 100.0% | 0 |
string.Empty | 605 | 99.5% | 0 |
実行環境: Windows11 x64 .NET Runtime 10.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。
-
x86 だと
""
はstring.Empty
より僅かにパフォーマンスがいいようです -
x64 だと
""
とstring.Empty
はパフォーマンス的に同じです - このへんは .NET10 の最適化の恩恵かもしれません。別のバージョンだと違う結果になりそうですが、多分違いは僅かです
- C# の文字列リテラルは、実行時に文字列ごとに唯一のインスタンスが生成されそのポインタが割り当てられます(うろ覚え)。つまり、リテラルも
string.Empty
も実行時は同じような挙動のはずです
デフォルト引数
例えばプロパティ通知パターンで ↓ のようなコードを書くとします。
void SetValue<T>(ref T field, T value,
[System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{
// 省略
}
デフォルト引数には const
(コンパイル時定数)しか使えないため、string propertyName = ""
となります。nullable 以前は = null
をよく使っていました。= string.Empty
はコンパイルエラーになります。
ReadOnlySpan<char>
等について
文字列を扱う場合パフォーマンスを考慮すると ReadOnlySpan<char>
をよく使います。パフォーマンスも見ていきましょう。
Test | Score | % | GC0 |
---|---|---|---|
x86 | |||
"" | 709 | 100.0% | 0 |
string.Empty | 714 | 100.7% | 0 |
[] | 572 | 80.7% | 0 |
default | 676 | 95.3% | 0 |
ReadOnlySpan.Empty | 677 | 95.5% | 0 |
x64 | |||
"" | 529 | 100.0% | 0 |
string.Empty | 523 | 98.9% | 0 |
[] | 530 | 100.2% | 0 |
default | 532 | 100.6% | 0 |
ReadOnlySpan.Empty | 521 | 98.5% | 0 |
-
x86 だと
""
のパフォーマンスがいいようです。default
や[]
がそうでもないのが意外です。計測誤差かもしれませんが、何回か繰り返してもこの傾向です - x64 だとどれも同じようなパフォーマンスです
UTF-8 文字列を扱う場合 ReadOnlySpan<byte>
を使うことになります。
Test | Score | % | GC0 |
---|---|---|---|
x86 | |||
""u8 | 695 | 100.0% | 0 |
[] | 698 | 100.4% | 0 |
default | 697 | 100.3% | 0 |
ReadOnlySpan.Empty | 704 | 101.3% | 0 |
x64 | |||
""u8 | 509 | 100.0% | 0 |
[] | 517 | 101.6% | 0 |
default | 514 | 101.0% | 0 |
ReadOnlySpan.Empty | 499 | 98.0% | 0 |
- こちらは x86 でもパフォーマンスに違いがありません
-
ReadOnlySpan<char>
よりスコアがいいため、最適化されている可能性がありそうです
ReadOnlySpan<char>
等で使用できるリテラル
void StringDefault(string s = default!) { }
void StringNull(string s = null!) { }
void StringLiteral(string s = "") { }
// void StringLiteral(string s = string.Empty) { } // コンパイルエラー!
void SpanDefault(ReadOnlySpan<char> s = default) { }
void SpanNull(ReadOnlySpan<char> s = (string?)null) { }
void SpanLiteral(ReadOnlySpan<char> s = "") { }
// void SpanEmpty(ReadOnlySpan<char> s = ReadOnlySpan<char>.Empty) { } // コンパイルエラー!
void SpanU8Default(ReadOnlySpan<byte> s = default) { }
void SpanU8Null(ReadOnlySpan<byte> s = (byte[]?)null) { }
// void SpanU8Literal(ReadOnlySpan<byte> s = ""u8) { } // コンパイルエラー!
// void SpanU8Empty(ReadOnlySpan<byte> s = ReadOnlySpan<byte>.Empty) { } // コンパイルエラー!
ReadOnlySpan<char> s = (string?)null
とか ReadOnlySpan<char> s = ""
は型変換を含むものの使えるようです。
string.Empty
は書き換えられない
string.Empty
は書き換えられないよう特別に保護されているようです。
かつて(.NET Framework 時代)はリフレクションで string.Empty
を書き換えられたという・・・
現在はかなりあれなコードでも書き換えられないようになっています。
#pragma warning disable CS8500
private static readonly string MyEmpty = "";
fixed (string* ptr = &string.Empty)
{
*ptr = "NotEmptyString";
// string.Empty は書き換えられない
Assert.Equal("", string.Empty);
}
fixed (string* ptr = &MyEmpty)
{
*ptr = "NotEmptyString";
// ユーザー定義のフィールドは読み取り専用でも書き換えられる
Assert.Equal("NotEmptyString", MyEmpty);
}
おわりに
個人的には ""
を使います。文字数が少ないためです。
コード中の文字列リテラルは避けたほうがいい風潮もありますが、空文字列ならいいでしょう。