0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】string.Empty か "" か

Last updated at Posted at 2025-05-11

はじめに

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

おわりに

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?