せっかちさん向けに
using System.Collections;
public class NonEmptyEnumerable<T> : IEnumerable<T>
{
public T Head { get; }
public IEnumerable<T> Tail { get; }
public NonEmptyEnumerable(T head, params IEnumerable<T> tail)
{
Head = head;
Tail = tail;
}
public IEnumerator<T> GetEnumerator()
{
yield return Head;
foreach (var item in Tail)
yield return item;
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
}
params IEnumerable<T> tailで弾かれる人はparams T[] tailに変えればいいと思います。
概要
Nullable型の歴史
C#において、nullに悩まされて死にかけたところをNullable型として言語仕様を再度固めなおしたところで一命を取り留めた。
具体的には、C#8以前において、
A a = null;
a.B(); // 実行時エラー!(俗に言うぬるぽ、NullReferenceException)
というようなコードが散見された。nullが代入されているのにも関わらず、その状態で関数を呼び出しをしようとしてエラーが発生するものである。しかし、C#8以降においては、
A a = null; // コンパイル時に警告が発生し修正を促される
a.B();
というように警告が発生するようになり、
A a = new A();
a.B();
と書くことで、nullに対して不正な参照によるエラーが消えた。(本当にこれでいいのかは置いといて)
コレクション型に関して
ご存じの通り、よく使うであろうList<T>やT[]はIEnumerable<T>というインターフェースを継承しており、これは列挙可能な型であることを示している。
// 激キモステップ5人衆
IEnumerable<string> names = ["Catrina", "Hugh", "Sarah", "Emma", "Joe"];
// 一人目が標準出力越しに挨拶する。
Console.WriteLine($"Hi, my name is {names.First()}!");
先ほどのNullable型により、以下は対策された。
IEnumerable<string> names = null; // コンパイラがnull警察をしてキレる
nullじゃなきゃいいんだよな...?
IEnumerable<string> names = []; // コンパイラも文句は言うまい
ほなええか!
さーてと、names.First()を呼び出してと、、、
あ?
InvalidOperationException!?!?
そりゃそうだ、最初の要素すら存在しないもの。要素を取得したかっただけなのに実行時エラーが出ちゃった!
なら、どうすればいいのか?Emptyable型の仕様を待つしかないのか?;;
いや、そうする必要はない!
要素を1つ強制するコレクション型
ということで、冒頭のNonEmptyEnumerableクラスを使うと、
NonEmptyEnumerable<string> names = new(); // コンパイルエラー!コンストラクタを呼び出す引数が足りない的な
となり、コンパイルエラーが発生する。晴れて、以下のように修正することで、
NonEmptyEnumerable<string> names = new("Catrina");
安全にnames.First()が使えるようになった。
よくある代替案
どちらも、nullである状態および空である状態をコンパイルレベルで禁止するものであった。しかし、タイトルにある通り、許容した上で使用する度に対策する方式も存在する。というか、そちらの方がメジャーかも?
Nullable型について
先ほどまで広義的なNullable型について言及していたが、狭義的なNullable型を用いる。
A? a = null;
a.B(); // また警告
先ほどまでと違うのは、Aに?がついてnullが許容されるようになった点である。しかし、今度は使用するタイミングで警告が出るようになってしまった。
?演算子の場合
そこで、
a?.B();
とすることで、nullならば何もせずにスルーする挙動に変えることができる。また、
if (a != null) a.B();
と同義である。(ちゃんと調べてないけど)
??演算子の場合
(a ?? new A()).B();
というように、nullが参照される直前で代入してしまう。うーん。。。
コレクション型に関して
IEnumerable<string> names = [];
Console.WriteLine($"Hi, my name is {names.First()}!"); // 実行時エラー!
はどうすればいいのだろうか?
Any()の場合
まあ、愚直にやるなら、
if (names.Any()) Console.WriteLine($"Hi, my name is {names.First()}!");
とすることで、事前に空かどうかを確かめてから実行するかスルーする挙動に変える。
FirstOrDefault()の場合
Console.WriteLine($"Hi, my name is {names.FirstOrDefault("(none)")}!");
というように、空の場合のデフォルト値を設定できるメソッドを使うのもありかもしれない。うん、知らんけど。
あとがき
これは俗に言う型レベルプログラミングの第一歩であり、TypeScriptおよびIdrisなどに通じている人なら愚直な案としてパッって思いつきそうなクラスであるが、並々のプログラマには思いつかない、、、たぶん。。。
妄想で書くが、C#で篩型っぽく書くなら、
Refined<IEnumerable<T>, e => e.Any()>
だろうか。