- 関数型C#:不変性
- 機能的なC#:プリミティブな強迫観念
- 関数型C#:null不可の参照型
- 機能的なC#:バグの処理
不変性
エンタープライズ開発の世界における最大の問題は、複雑性との戦いです。 コードの読みやすさは、多かれ少なかれ複雑なプロジェクトを書くときに最初に達成すべきことです。 これがないと、コードを理解し、これに基づいてインテリジェントな意思決定を行う能力が著しく損なわれます。
可変オブジェクトはコードを読むときに役立ちますか? 例を見てみましょう:
// Create search criteria var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10); // Search customers IReadOnlyCollection<Customer> customers = Search(queryObject); // Adjust criteria if nothing found if (customers.Count == 0) AdjustSearchCriteria(queryObject, name); // Is queryObject changed here? Search(queryObject);
queryObjectは、カスタムを再度検索するまでに変更されましたか? たぶんはい。 またはそうでないかもしれません。 このオブジェクトがAdjustSearchCriteriaメソッドを使用して変更されているかどうかによります。 調べるには、このメソッドの内部を調べる必要があります。そのシグネチャは十分な情報を提供しません。
これを次のコードと比較してください。
// Create search criteria var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10); // Search customers IReadOnlyCollection<Customer> customers = Search(queryObject); if (customers.Count == 0) { // Adjust criteria if nothing found QueryObject<Customer> newQueryObject = AdjustSearchCriteria(queryObject, name); Search(newQueryObject); }
この例では、AdjustSearchCriteriaが新しい基準オブジェクトを作成し、後で新しい検索に使用することは明らかです。
可変データ構造の問題は何ですか?
- あるメソッドから別のメソッドに送信されたデータが変更されるかどうかわからない場合、コードについて考えることは困難です。
- スタックを数レベル下に移動する必要がある場合、プログラムの進行に合わせて行うのは困難です。
- マルチスレッドアプリケーションの場合、コードの理解とデバッグは何度も複雑になります。
不変型を作成する方法
C#の将来のバージョンでは、不変キーワードが表示される可能性があります。 その助けを借りて、単にその署名を見ることによって、型が不変かどうかを理解することが可能になります。 それまでは、持っているものを使用する必要があります。
比較的単純なクラスがある場合は、不変にすることを検討してください。 このガイドラインは、 値オブジェクトの概念と相関しています。
たとえば、販売されている多数の製品を記述するProductPileクラスを取り上げます。
public class ProductPile { public string ProductName { get; set; } public int Amount { get; set; } public decimal Price { get; set; } }
不変にするには、そのプロパティを読み取り専用としてマークし、コンストラクターを追加します。
public class ProductPile { public string ProductName { get; private set; } public int Amount { get; private set; } public decimal Price { get; private set; } public ProductPile(string productName, int amount, decimal price) { Contracts.Require(!string.IsNullOrWhiteSpace(productName)); Contracts.Require(amount >= 0); Contracts.Require(price > 0); ProductName = productName; Amount = amount; Price = price; } }
ここで、製品の1つを販売するたびにAmountプロパティを1つずつ減らす必要があるとします。 既存のオブジェクトを変更する代わりに、既存のオブジェクトに基づいて新しいオブジェクトを作成できます。
public class ProductPile { public string ProductName { get; private set; } public int Amount { get; private set; } public decimal Price { get; private set; } public ProductPile(string productName, int amount, decimal price) { Contracts.Require(!string.IsNullOrWhiteSpace(productName)); Contracts.Require(amount >= 0); Contracts.Require(price > 0); ProductName = productName; Amount = amount; Price = price; } public ProductPile SubtractOne() { return new ProductPile(ProductName, Amount – 1, Price); } }
これにより何が得られますか?
- 不変の型を持つため、コンストラクターでそのコントラクトを一度だけ検証する必要があります。 その後、オブジェクトが正しい状態にあることを完全に確認できます。
- 不変型のオブジェクトはスレッドセーフです。
- コードの可読性が向上する理由は メソッドが動作している変数が変更されていないことを確認するために、スタックを深く調べる必要はもうありません。
制限事項
もちろん、すべての良い習慣には代償があります。 小さなクラスは不変性を最大限に活用しますが、このアプローチは大きな型の場合に常に適用できるとは限りません。
まず、不変性には潜在的なパフォーマンスの問題が伴います。 オブジェクトが非常に大きい場合は、変更ごとにコピーを作成する必要があります。
良い例は、 不変のコレクションです。 作成者は、潜在的なパフォーマンスの問題を考慮し、コレクションの状態を変更できる特別なBuilderクラスを追加しました。 コレクションが必要な状態になった後、変更不可能に変換することでコレクションをファイナライズできます。
var builder = ImmutableList.CreateBuilder<string>(); builder.Add(“1”); // Adds item to the existing object ImmutableList<string> list = builder.ToImmutable(); ImmutableList<string> list2 = list.Add(“2”); // Creates a new object with 2 items
おわりに
ほとんどの場合、不変型(特にかなり単純な場合)はコードを改善します。
シリーズの他の記事:
- 関数型C#:不変性
- 機能的なC#:プリミティブな強迫観念
- 関数型C#:null不可の参照型
- 機能的なC#:バグの処理
記事の英語版: Functional C#:不変性