47

私はいくつかのプロジェクトの過程で、不変 (読み取り専用) オブジェクトと不変オブジェクト グラフを作成するためのパターンを開発しました。不変オブジェクトには、100% スレッドセーフであるという利点があるため、スレッド間で再利用できます。私の仕事では、このパターンを Web アプリケーションで、構成設定や、メモリにロードしてキャッシュするその他のオブジェクトによく使用します。キャッシュされたオブジェクトは、予期せず変更されないことを保証するために、常に不変である必要があります。

もちろん、次の例のように、不変オブジェクトを簡単に設計できます。

public class SampleElement
{
  private Guid id;
  private string name;

  public SampleElement(Guid id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public Guid Id
  {
    get { return id; }
  }

  public string Name
  {
    get { return name; }
  }
}

これは単純なクラスでは問題ありませんが、より複雑なクラスでは、コンストラクターを介してすべての値を渡すという概念は好きではありません。プロパティにセッターを設定することはより望ましく、新しいオブジェクトを作成するコードは読みやすくなります。

では、setter を使用して不変オブジェクトを作成するにはどうすればよいでしょうか。

さて、私のパターンでは、オブジェクトは最初から完全に変更可能であり、1 回のメソッド呼び出しでフリーズするまで続きます。オブジェクトが凍結されると、永久に不変のままになります。再び可変オブジェクトに変えることはできません。オブジェクトの変更可能なバージョンが必要な場合は、単純に複製します。

さて、いくつかのコードに進みます。次のコード スニペットでは、パターンを最も単純な形式に煮詰めようとしています。IElement は、すべての不変オブジェクトが最終的に実装する必要がある基本インターフェイスです。

public interface IElement : ICloneable
{
  bool IsReadOnly { get; }
  void MakeReadOnly();
}

Element クラスは、IElement インターフェイスのデフォルトの実装です。

public abstract class Element : IElement
{
  private bool immutable;

  public bool IsReadOnly
  {
    get { return immutable; }
  }

  public virtual void MakeReadOnly()
  {
    immutable = true;
  }

  protected virtual void FailIfImmutable()
  {
    if (immutable) throw new ImmutableElementException(this);
  }

  ...
}

上記の SampleElement クラスをリファクタリングして、不変オブジェクト パターンを実装しましょう。

public class SampleElement : Element
{
  private Guid id;
  private string name;

  public SampleElement() {}

  public Guid Id
  {
    get 
    { 
      return id; 
    }
    set
    {
      FailIfImmutable();
      id = value;
    }
  }

  public string Name
  {
    get 
    { 
      return name; 
    }
    set
    {
      FailIfImmutable();
      name = value;
    }
  }
}

MakeReadOnly() メソッドを呼び出してオブジェクトが不変としてマークされていない限り、Id プロパティと Name プロパティを変更できるようになりました。不変になると、setter を呼び出すと ImmutableElementException が発生します。

最後の注意: 完全なパターンは、ここに示すコード スニペットよりも複雑です。また、不変オブジェクトのコレクションと、不変オブジェクト グラフの完全なオブジェクト グラフのサポートも含まれています。完全なパターンでは、最も外側のオブジェクトで MakeReadOnly() メソッドを呼び出すことにより、オブジェクト グラフ全体を不変にすることができます。このパターンを使用してより大きなオブジェクト モデルの作成を開始すると、オブジェクトがリークするリスクが高まります。リーキー オブジェクトとは、オブジェクトに変更を加える前に FailIfImmutable() メソッドの呼び出しに失敗したオブジェクトです。リークをテストするために、単体テストで使用する一般的なリーク検出クラスも開発しました。リフレクションを使用して、すべてのプロパティとメソッドが ImmutableElementException を不変状態でスローするかどうかをテストします。つまり、TDD がここで使用されます。

私はこのパターンがとても好きになり、大きな利点を見つけました。ですから、私が知りたいのは、同様のパターンを使用している方がいるかどうかです。はいの場合、それを文書化した優れたリソースを知っていますか? 私は本質的に、潜在的な改善と、このトピックに関する既存の標準を探しています。

4

15 に答える 15

31

参考までに、2 番目のアプローチは「アイスキャンディーの不変性」と呼ばれます。

Eric Lippert は、不変性に関する一連のブログ エントリをここから始めています。私はまだCTP(C#4.0)を理解していますが、オプション/名前付きパラメータ(.ctorへの)がここで(読み取り専用フィールドにマップされた場合)何をするかは興味深いようです... [更新:私はブログを書いたこれについてはこちら]

参考までに、私はおそらくそれらのメソッドvirtualを作成しないでしょう - おそらくサブクラスがそれをフリーズ可能にできないようにしたくないでしょう。コードを追加できるようにしたい場合は、次のようなものをお勧めします。

[public|protected] void Freeze()
{
    if(!frozen)
    {
        frozen = true;
        OnFrozen();
    }
}
protected virtual void OnFrozen() {} // subclass can add code here.

また、AOP (PostSharp など) は、ThrowIfFrozen() チェックをすべて追加するための実行可能なオプションである可能性があります。

(用語/メソッド名を変更した場合はお詫びします-返信を作成するときに元の投稿が表示されないようにします)

于 2008-11-04T22:28:39.797 に答える
17

もう 1 つのオプションは、ある種の Builder クラスを作成することです。

たとえば、Java (および C# や他の多くの言語) では、文字列は不変です。String を作成するために複数の操作を実行する場合は、StringBuilder を使用します。これは変更可能であり、完了したら、最終的な String オブジェクトを返します。それ以来、それは不変です。

他のクラスでも同様のことができます。不変の要素があり、次に ElementBuilder があります。ビルダーが行うことは、設定したオプションを保存することだけです。それをファイナライズすると、不変の Element が構築されて返されます。

もう少しコードが増えますが、不変であるはずのクラスにセッターを配置するよりもすっきりしていると思います。

于 2008-11-04T21:59:32.073 に答える
10

変更のたびに新しいものを作成しなければならないという事実に最初は不快感を覚えた後、System.Drawing.Point数年前にこの概念を完全に受け入れました。実際、私は現在readonly、デフォルトですべてのフィールドを作成し、やむを得ない理由がある場合にのみ変更可能に変更しています。これは驚くほどまれです。

ただし、クロススレッドの問題についてはあまり気にしません (関連するコードを使用することはめったにありません)。セマンティックな表現力のおかげで、はるかに優れていると思います。不変性は、誤って使用するのが難しいインターフェイスの典型です。

于 2008-11-04T21:58:42.027 に答える
8

あなたはまだ状態を扱っているので、オブジェクトが不変になる前に並列化されていると、噛まれる可能性があります。

より機能的な方法は、各セッターでオブジェクトの新しいインスタンスを返すことです。または、変更可能なオブジェクトを作成し、それをコンストラクターに渡します。

于 2008-11-04T21:53:14.473 に答える
6

ドメイン駆動設計と呼ばれる (比較的) 新しいソフトウェア設計パラダイムは、エンティティ オブジェクトと値オブジェクトを区別します。

エンティティ オブジェクトは、従業員、クライアント、請求書など、永続的なデータ ストア内のキー駆動型オブジェクトにマップする必要があるものとして定義されます。オブジェクトのプロパティを変更すると、変更をどこかのデータ ストアに保存し、同じ「キー」を持つクラスのインスタンスが複数存在する場合は、それらを同期するか、データ ストアへの永続性を調整して、1 つのインスタンスの変更が他のインスタンスを上書きしないようにする必要があります。 . エンティティ オブジェクトのプロパティを変更するということは、オブジェクトに関する何かを変更していることを意味します。参照しているオブジェクトを変更するのではありません...

値オブジェクト otoh は、不変とみなすことができるオブジェクトであり、そのユーティリティはプロパティ値によって厳密に定義され、複数のインスタンスは、住所、電話番号、または車輪のように、何らかの方法で調整する必要はありません車、またはドキュメント内の文字... これらは完全にプロパティによって定義されます... テキスト エディター内の大文字の「A」オブジェクトは、ドキュメント全体で他の大文字の「A」オブジェクトと透過的に交換できます。他のすべての「A」と区別するためのキーは必要ありません。この意味では、「B」に変更すると不変です (電話番号オブジェクトの電話番号文字列を変更するのと同じように、変更できません)。変更可能なエンティティに関連付けられたデータを変更すると、ある値から別の値に切り替えられます...文字列の値を変更するときと同じように...

于 2008-11-04T22:27:30.847 に答える
5

@Cory Foy と @Charles Bretana による、エンティティと値の違いに関するポイントを拡張します。値オブジェクトは常に不変である必要がありますが、オブジェクトが自分自身を凍結したり、コードベースで任意に凍結できるようにしたりする必要はないと思います。それには本当に悪臭があり、オブジェクトがどこで凍結されたのか、なぜ凍結されたのか、そしてオブジェクトへの呼び出しの間に解凍から凍結に状態が変わる可能性があるという事実を追跡するのが難しくなるのではないかと心配しています.

これは、(変更可能な) エンティティを何かに渡して、それが変更されないようにしたい場合があると言っているわけではありません。

したがって、オブジェクト自体をフリーズする代わりに、ReadOnlyCollection< T > のセマンティクスをコピーする別の可能性があります。

List<int> list = new List<int> { 1, 2, 3};
ReadOnlyCollection<int> readOnlyList = list.AsReadOnly();

オブジェクトは、必要に応じて変更可能として機能し、必要に応じて変更不可にすることができます。

Add( T item)ReadOnlyCollection< T >は、インターフェイスにメソッドを持つ ICollection< T > も実装することに注意してください。bool IsReadOnly { get; }ただし、消費者が例外をスローするメソッドを呼び出す前に確認できるように、インターフェースにも定義されています。

違いは、IsReadOnly を false に設定できないことです。コレクションは読み取り専用であるかどうかに関係なく、コレクションの存続期間中は変更されません。

コンパイル時に C++ が提供する const-correctness があればいいのですが、それには独自の問題があり始めており、C# がそこに行かないことを嬉しく思います。


ICloneable - 私は次のことを参照するだけだと思いました:

ICloneable を実装しない

パブリック API で ICloneable を使用しないでください

Brad Abrams - 設計ガイドライン、マネージ コード、および .NET Framework

于 2008-11-04T23:48:19.280 に答える
4

これは重要な問題であり、それを解決するためのより直接的なフレームワーク/言語サポートを期待しています。あなたが持っているソリューションには、多くのボイラープレートが必要です。コード生成を使用して定型文の一部を自動化するのは簡単かもしれません。

フリーズ可能なプロパティをすべて含む部分クラスを生成します。このための再利用可能な T4 テンプレートを作成するのはかなり簡単です。

テンプレートはこれを入力として受け取ります:

  • 名前空間
  • クラス名
  • プロパティ名/型タプルのリスト

そして、以下を含む C# ファイルを出力します。

  • 名前空間宣言
  • 部分クラス
  • 対応する型、バッキング フィールド、ゲッター、FailIfFrozen メソッドを呼び出すセッターを含む各プロパティ

フリーズ可能なプロパティの AOP タグも機能しますが、より多くの依存関係が必要になりますが、T4 は新しいバージョンの Visual Studio に組み込まれています。

これによく似たもう 1 つのシナリオは、INotifyPropertyChangedインターフェイスです。その問題の解決策は、この問題にも適用できる可能性があります。

于 2010-11-03T21:05:54.483 に答える
4

System.String は、セッターと変更メソッドを持つ不変クラスの良い例ですが、各変更メソッドが新しいインスタンスを返すだけです。

于 2008-11-04T21:56:12.827 に答える
2

議論されていないあなたの特定の問題のための他の2つのオプション:

  1. プライベートプロパティセッターを呼び出すことができる独自のデシリアライザーを構築します。最初にデシリアライザーを構築するための努力ははるかに多くなりますが、それは物事をよりクリーンにします。コンパイラーは、セッターを呼び出そうとすることさえ防ぎ、クラスのコードは読みやすくなります。

  2. XElement(または他の種類のXMLオブジェクトモデル)を取得し、そこから自身を取り込むコンストラクターを各クラスに配置します。明らかに、クラスの数が増えると、これは解決策としてすぐに望ましくなくなります。

于 2009-06-23T20:23:35.333 に答える
2

これは Channel 9 の新しいビデオで、インタビューの 36:30 から Anders Hejlsberg が C# の不変性について話し始めています。彼は、ポプシクルの不変性の非常に優れた使用例を示し、これが現在どのように自分で実装する必要があるかを説明しています。C# の将来のバージョンで不変オブジェクト グラフを作成するためのより良いサポートについて検討する価値があると彼が言っているのを聞いて、私の耳には心地よく響きました。

専門家から専門家へ: Anders Hejlsberg - C# の未来

于 2009-03-08T09:37:26.633 に答える
2

オブジェクトを可変状態から不変状態に変更できるという考えは好きではありません。そのようなものは、私にとって設計のポイントを無効にしているようです。いつそれをする必要がありますか?VALUES を表すオブジェクトのみを不変にする必要があります

于 2008-11-04T21:56:37.523 に答える
2

要素のプロパティを単純化するためのヒント:自動プロパティを使用private setし、データ フィールドを明示的に宣言しないようにします。例えば

public class SampleElement {
  public SampleElement(Guid id, string name) {
    Id = id;
    Name = name;
  }

  public Guid Id {
    get; private set;
  }

  public string Name {
    get; private set;
  }
}
于 2008-11-05T12:37:53.470 に答える
2

サブクラス MutableThing と ImmutableThing を持つ抽象クラス ThingBase を持つのはどうですか? ThingBase は、保護された構造にすべてのデータを格納し、フィールドの読み取り専用パブリック プロパティとその構造の読み取り専用保護プロパティを提供します。また、ImmutableThing を返すオーバーライド可能な AsImmutable メソッドも提供します。

MutableThing は、読み取り/書き込みプロパティでプロパティをシャドウし、デフォルトのコンストラクターと ThingBase を受け入れるコンストラクターの両方を提供します。

不変のものは、単にそれ自体を返すように AsImmutable をオーバーライドする封印されたクラスになります。また、ThingBase を受け入れるコンストラクターも提供します。

于 2010-11-30T20:51:28.737 に答える