【C#】string.Empty か "" か

# はじめに

C# には**空文字列**の表現がいくつかあります。

```cs
const string ConstEmpty = "";
string readOnlyEmpty = string.Empty;
Assert.Equal(ConstEmpty, readOnlyEmpty);
```

今回はどれを使うのがいいか考えてみたいと思います。

- `string` の場合パフォーマンスはどれも同じ
- `""` のほうがコード的には短い
- `string.Empty` はデフォルト引数で使えない
- `ReadOnlySpan<char>` も `""` がよさげ

## テストコード

<details>
<summary>テストコード</summary>

```cs
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));
        }
    }
}
```

</details>

## パフォーマンス比較

```cs
[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` も実行時は同じような挙動のはずです

## デフォルト引数

例えば[プロパティ通知パターン](https://learn.microsoft.com/ja-jp/dotnet/api/system.componentmodel.inotifypropertychanged?view=net-10.0)で ↓ のようなコードを書くとします。

```cs
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>` 等で使用できるリテラル

```cs
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` を書き換えられたという・・・  
現在はかなりあれなコードでも書き換えられないようになっています。

```cs
#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);
}
```

## おわりに

個人的には `""` を使います。文字数が少ないためです。  
コード中の文字列リテラルは避けたほうがいい風潮もありますが、空文字列ならいいでしょう。