問題
私たちは、C#などの言語について、静的かつ静的に型付けされたものについて話すことに慣れています。 もちろん、これは真実であり、多くの場合、ある言語エンティティに対して示すタイプは、そのタイプのアイデアをうまく表現しています 。 しかし、習慣から外れて(「誰もがこれを行う」)、「宣言された型」の「望ましい型」の完全に正しくない表現に我慢する場合の広範な例があります。 最も顕著なのは、代替手段がないヌル値を備えた参照型です。
アクティブな開発の年の現在のプロジェクトでは、NullReferenceExceptionはありませんでした。 これは、以下に説明する手法を適用した結果であると合理的に信じることができます。
コードを考えてみましょう:
public interface IUserRepo { User Get(int id); User Find(int id); }
このインターフェイスには追加のコメントが必要です。「Getは常にnullではないが、オブジェクトが見つからない場合は例外をスローします。 また、検索ではなく検索はnull "を返します。 これらのメソッドの作成者が暗示している「望ましい」戻り値の型は異なります。「必須ユーザー」と「未定ユーザー」です。 そして、「宣言された」型はまったく同じです。 言語がこの違いを明確に表現することを強制しない場合、これは私たちが自分のイニシアチブでそれを行うことができない、すべきではないことを意味しません。
解決策
関数型言語、たとえばF#には、標準型FSharpOption <T>があります。これは、任意の型のコンテナーを表すだけで、Tの値が1つあるか、存在しない場合があります。 関数型言語に精通しているさまざまなコーディングスタイルの支持者を含め、使いやすいようにこのタイプからどのような可能性を持ちたいか考えてみましょう。
この架空のタイプを考えると、次の形式でリポジトリを書き換えることができます。
public interface IUserRepo { User Get(int id); Maybe<User> Find(int id); }
最初のメソッドがまだnullを返すことができるように、すぐに予約してください。 これを言語レベルで禁止する簡単な方法はありません。 ただし、これは少なくとも開発チームの合意レベルで行うことができます。 そのような事業の成功は人々にかかっています。 私のプロジェクトでは、このような合意が受け入れられ、順調に監視されています。
もちろん、さらに進んで、ソースコードにnullキーワードが存在するかどうかをアセンブリプロセスでチェックすることもできます(このルールに指定された例外があります)。 しかし、これはまだ必要ありませんでした。内部の規律で十分です。
一般に、たとえば、PostSharpなどのAOPソリューションを介してContract.Ensure(Contract.Result <T>()!= Null)をすべての適切なメソッドに強制するなど、さらに先へ進むことができます。規律が低いと、不運なヌルを返すことはできません。
インターフェイスの新しいバージョンは、Findがオブジェクトを見つけられない可能性があることを明示的に宣言します。その場合、Maybe <User> .Nothingを返します。 この場合、結果のnullをチェックすることを忘れることはできません。 このようなリポジトリの使用についてさらに考えます。
// null var user = repo.Find(userId); // User, Maybe<User> var userName = user.Name; // , Maybe Name var maybeUser = repo.Find(userId); // , string userName; if (maybeUser.HasValue) // { var user = maybeUser.Value; userName = user.Name; } else userName = "unknown";
このコードは、nullチェックを使用して記述する場合と似ていますが、ifの条件が少し異なっているように見えます。 ただし、このようなチェックを絶えず繰り返すと、最初にコードが乱雑になり、操作の本質がわかりにくくなり、次に、開発者が退屈になります。 したがって、ほとんどの標準操作に既製のメソッドを使用すると非常に便利です。 以前の流なスタイルのコードは次のとおりです。
string userName = repo.Find(userId).Select(u => u.Name).OrElse("unknown");
関数型言語と表記法に近い人のために、完全に「関数型」スタイルをサポートできます。
string userName = (from user in repo.Find(userId) select user.Name).OrElse("unknown");
または、例はより複雑です:
( from roleAProfile in provider.FindProfile(userId, type: "A") from roleBProfile in provider.FindProfile(userId, type: "B") from roleCProfile in provider.FindProfile(userId, type: "C") where roleAProfile.IsActive() && roleCProfile.IsPremium() let user = repo.GetUser(userId) select user ).Do(HonorAsActiveUser);
その命令的な同等物で:
var maybeProfileA = provider.FindProfile(userId, type: "A"); if (maybeProfileA.HasValue) { var profileA = maybeProfileA.Value; var maybeProfileB = provider.FindProfile(userId, type: "B"); if (maybeProfileB.HasValue) { var profileB = maybeProfileB.Value; var maybeProfileC = provider.FindProfile(userId, type: "C"); if (maybeProfileC.HasValue) { var profileC = maybeProfileC.Value; if (profileA.IsActive() && profileC.IsPremium()) { var user = repo.GetUser(userId); HonorAsActiveUser(user); } } } }
また、少なくとも次の形式では、Maybe <T>とそのかなり近い相対物であるIEnumerable <T>との統合が必要です。
var admin = users.MaybeFirst(u => u.IsAdmin); // FirstOrDefault(u => u.IsAdmin); Console.WriteLine("Admin is {0}", admin.Select(a => a.Name).OrElse("not found"));
上記の「夢」から、Maybeタイプに何が欲しいのかは明らかです
- 価値存在情報へのアクセス
- そして、利用可能な場合、値自体に
- ストリーミングコールスタイルの便利なメソッド(または拡張メソッド)のセット
- LINQ式の構文のサポート
- IEnumerable <T>および他のコンポーネントとの統合。連携する場合、多くの場合、価値が不足する状況があります。
プロジェクトにすばやく含めるためにNugetが提供できるソリューションを検討し、上記の基準に従って比較します。
Nugetパッケージ名とタイプタイプ | ハスバリュー | 価値 | フルエンタピ | LINQサポート | IEnumerableとの統合 | ノートとソースコード |
---|---|---|---|---|---|---|
オプション、クラス | そこにある | いいえ、パターンマッチングのみ | 最小 | いや | いや | github.com/tejacques/Option |
Strilanc.Value.May、構造体 | そこにある | いいえ、パターンマッチングのみ | 金持ち | そこにある | そこにある | 5月にnullを有効な値として受け入れます
github.com/Strilanc/May |
オプション、構造体 | そこにある | そこにある | 平均的 | そこにある | そこにある | どちらのタイプも利用可能です。
github.com/davidsidlinger/options |
Nevernullクラス | そこにある | そこにある | 平均的 | いや | いや | github.com/Bomret/NeverNull |
機能的、たぶん、構造体 | そこにある | そこにある | 金持ち | そこにある | そこにある | github.com/AndreyTsvetkov/Functional.Maybe |
たぶんタイプなし | - | - | 最小 | いや | - | 拡張メソッドはプレーンなnullで動作します
github.com/hazzik/Maybe upd:同様のアプローチと詳細な説明を含むmezastelのスクリーンキャストです: www.techdays.ru/videos/4448.html |
WeeGems.Options、構造体 | そこにある | そこにある | 最小 | いや | いや | 他の機能ユーティリティもあります:メモ化、機能の部分使用
bitbucket.org/MattDavey/weegems |
私のプロジェクトがそのパッケージを成長させたのはたまたま起こった、それは上記の一つです。
この表から、最も「軽量」で低侵襲なソリューションはhazzikからである可能性があり、APIを変更する必要はありませんが、同じifsを取り除くためにいくつかの拡張メソッドを追加するだけです。 しかし、悲しいかな、それは忘れっぽいプログラマがNullReferenceExceptionを受け取ることを保護しません。
最も豊富なパッケージは、Strilanc.Value.Maybe(特にここでは、著者が(null).ToMaybe()がMaybe.Nothingと同じではないことを決定した理由)、Functional.Maybe、Optionsです。
味を選択してください。 一般に、私はもちろん、Microsoftの標準ソリューションと
UPD:同志aikixdは反対の記事を書きました。