てくメモ

trivial な notes

【C#】null の Span

連続したメモリ領域を表すSpan<T>は、nullを受けることもできる。


その場合、== nullで中身の参照のnullチェックができる。

int[]? array1 = null;
int[]? array2 = new int[] { 1, 2, 3 };
ReadOnlySpan<int> span1 = array1;
ReadOnlySpan<int> span2 = array2;

Console.WriteLine($"{span1 == null}: span1 == null");
Console.WriteLine($"{span2 == null}: span2 == null");
// True: span1 == null
// False: span2 == null

これは演算子オーバーロードによるものなので、is nullとは等価でない。

bool _ = span1 is null;  // ❌ コンパイルエラー


Span のオーバーロードされた==が何をやっているか。
オペランドと右オペランドの長さと参照の等価をみている。

null同士は等価になる。
参考として、長さをみていることで、スライスしていれば参照が同じでもfalseを返す。

int[]? array3 = null;
ReadOnlySpan<int> span3 = array3;
ReadOnlySpan<int> span4 = span2[..^1];

Console.WriteLine($"{span1 == span3}: span1 (null) == span3 (null)");
Console.WriteLine($"{span2 == span4}: span2 == span4 (span2[..^1])");

// True: span1 (null) == span3 (null)
// False: span2 == span4 (span2[..^1])


ところで、中身の参照がnullでも Span 自体はそうではない。
プロパティはアクセスでき、null 例外は出ない。

// Length に普通にアクセスできる
Console.WriteLine($"span1.Length: {span1.Length}");

// インデクサも Span のものなので
try
    { Console.WriteLine(span1[0]); }
catch(NullReferenceException) // こちらではなく
    { Console.WriteLine(nameof(NullReferenceException)); }
catch(IndexOutOfRangeException) // こちらが飛ぶ
    { Console.WriteLine(nameof(IndexOutOfRangeException)); }

// span1.Length: 0
// IndexOutOfRangeException


これにより、null 許容型を Span で扱うと null を吸収して処理しうる。(見方や場合によっては、処理してしまう、という捉え方もされるかもしれない)

static string toLower(ReadOnlySpan<char> str)
{
    Span<char> buf = stackalloc char[str.Length];
    str.ToLower(buf, CultureInfo.InvariantCulture);
    return new(buf);
}

string? nullStr = null;
Console.WriteLine(toLower(nullStr)); // 例外は出ない