関数型C#:null不可の参照型

Functional C#シリーズの3番目の記事。





C#の非ゼロ参照型-現在のステータス



例を見てみましょう:



Customer customer = _repository.GetById(id); Console.WriteLine(customer.Name);
      
      





おなじみですね。 このコードにはどのような問題がありますか?



ここでの問題は、GetByIdメソッドがnullを返すことができるかどうかわからないことです。 メソッドが何らかのidに対してnullを返す場合、実行時にNullReferenceExceptionを取得するリスクがあります。 さらに悪いことに、顧客にnullが割り当てられてからこのオブジェクトを使用するまでにかなりの時間が経過する可能性があります。 そのようなコードはデバッグするのが難しい オブジェクトにnullが割り当てられた正確な場所を見つけるのは簡単ではありません。



フィードバックが速くなるほど、コードの問題を修正するのにかかる時間が短くなります。 もちろん、コンパイラは最速のフィードバックを与えることができます。 次のコードを記述して、コンパイラーにすべてのチェックを行わせるのはどれほど素晴らしいでしょうか?



 Customer! customer = _repository.GetById(id); Console.WriteLine(customer.Name);
      
      





これがタイプCustomerです! ゼロ以外のタイプ、つまり どのような状況でもオブジェクトをnullにできないタイプ。 またはさらに良い:



 Customer customer = _repository.GetById(id); Console.WriteLine(customer.Name);
      
      





つまり デフォルトですべての参照型をゼロ以外に設定し(現在は値型とまったく同じです)、正確にnull型が必要な場合は、次のように明示的に指定します。



 Customer? customer = _repository.GetById(id); Console.WriteLine(customer.Name);
      
      





残念ながら、ゼロ以外の参照型を言語レベルでC#に追加することはできません。 そのような決定は最初から行わなければなりません。さもないと、ほとんどすべての利用可能なコードが壊れます。 このトピックへのリンク: onetwo 。 C#の新しいバージョンでは、警告レベルで非ゼロの参照型が追加される可能性がありますが、この革新により、物事はスムーズに進みません。



また、nullの誤った使用に関連するエラーをコンパイラに強制的に検出させることはできませんが、回避策で問題を解決できます。 前の記事で終了したCustomerクラスのコードを見てみましょう。



 public class Customer { public CustomerName Name { get; private set; } public Email Email { get; private set; } public Customer(CustomerName name, Email email) { if (name == null) throw new ArgumentNullException(“name”); if (email == null) throw new ArgumentNullException(“email”); Name = name; Email = email; } public void ChangeName(CustomerName name) { if (name == null) throw new ArgumentNullException(“name”); Name = name; } public void ChangeEmail(Email email) { if (email == null) throw new ArgumentNullException(“email”); Email = email; } }
      
      





電子メールとカスタママーに関連するすべての検証を別のクラスに移動しましたが、現金小切手では何もできませんでした。 あなたが見ることができるように、これらは唯一の残りのチェックです。



nullチェックを削除する



どうすればそれらを取り除くことができますか?



ILリライタの助けを借りて。 この目的のために特別に作成されたNuGetパッケージNullGuard.Fodyを使用できます:コードにnullチェックを追加し、nullが入力パラメーターとして返されるか、メソッドの結果として返される場合、クラスに例外をスローさせる



使用を開始するには、NullGuard.Fodyパッケージをインストールし、属性でアセンブリをマークします



 [assembly: NullGuard(ValidationFlags.All)]
      
      





これ以降、アセンブリ内のすべてのメソッドとプロパティは、すべての受信および送信パラメーターに対してnull検証を自動的に受け取ります。 Customerクラスは次のように記述できます。



 public class Customer { public CustomerName Name { get; private set; } public Email Email { get; private set; } public Customer(CustomerName name, Email email) { Name = name; Email = email; } public void ChangeName(CustomerName name) { Name = name; } public void ChangeEmail(Email email) { Email = email; } }
      
      





さらに簡単:



 public class Customer { public CustomerName Name { get; set; } public Email Email { get; set; } public Customer(CustomerName name, Email email) { Name = name; Email = email; } }
      
      





ILリライタのおかげで出力が得られます。



 public class Customer { private CustomerName _name; public CustomerName Name { get { CustomerName customerName = _name; if (customerName == null) throw new InvalidOperationException(); return customerName; } set { if (value == null) throw new ArgumentNullException(); _name = value; } } private Email _email; public Email Email { get { Email email = _email; if (email == null) throw new InvalidOperationException(); return email; } set { if (value == null) throw new ArgumentNullException(); _email = value; } } public Customer(CustomerName name, Email email) { if (name == null) throw new ArgumentNullException(“name”, “[NullGuard] name is null.”); if (email == null) throw new ArgumentNullException(“email”, “[NullGuard] email is null.”); Name = name; Email = email; } }
      
      





ご覧のとおり、検証は手動で作成したものと同等ですが、戻り値の検証もここに追加されますが、これも非常に便利です。



nullを使用するにはどうすればよいですか?



nullが必要な場合はどうなりますか? Maybe構造を使用できます。



 public struct Maybe<T> { private readonly T _value; public T Value { get { Contracts.Require(HasValue); return _value; } } public bool HasValue { get { return _value != null; } } public bool HasNoValue { get { return !HasValue; } } private Maybe([AllowNull] T value) { _value = value; } public static implicit operator Maybe<T>([AllowNull] T value) { return new Maybe<T>(value); } }
      
      





たぶん、入ってくる値はAllowNull属性でマークされています。 これは、これらの特定のパラメータに対してnullチェックを追加しないことをリライタに示します。



Maybeを使用して、次のコードを記述できます。



 Maybe<Customer> customer = _repository.GetById(id);
      
      





そして今、コードを読むとき、GetByIdメソッドがnullを返すことができることが明らかになります。 そのセマンティクスを理解するためにメソッドコードを調べる必要はありません。



さらに、誤ってnull型と非ゼロ型を混同することはできません。そのようなコードはコンパイラエラーにつながります。



 Maybe<Customer> customer = _repository.GetById(id); ProcessCustomer(customer); // Compiler error private void ProcessCustomer(Customer customer) { // Method body }
      
      





もちろん、すべてのアセンブリがリライタを使用して変更する意味があるわけではありません。 たとえば、WFPを使用したアセンブリでこのようなルールを適用することは、おそらくあまり考えられません。なぜなら、その中のシステムコンポーネントが多すぎると、本質的にNULL可能になるからです。 そのような状況では、nullのチェックは意味をなしません。 あなたはまだこれらの税金のほとんどで何もできません。



ドメインアセンブリに関しては、この方法でアップグレードする価値があります。 さらに、このアプローチから最も恩恵を受けるのはドメインクラスです。



おわりに



説明したアプローチの利点:





シリーズの他の記事





記事の英語版: Functional C#:null不可の参照型



All Articles