59

不変性に関する記事をいくつか読んだことがありますが、まだその概念にうまく従っていません。

最近ここに不変性についてのスレッドを立てましたが、それ自体が話題なので、今専用のスレッドを立てています。

過去のスレッドで、不変性とはオブジェクトを読み取り専用にし、可視性を低くするプロセスであると考えたことを述べました。別のメンバーは、それとは実際には何の関係もないと言いました. このページ(シリーズの一部) は、不変のクラス/構造体の例を使用し、読み取り専用およびその他の概念を使用してそれをロックします。

この例の場合の状態の定義は正確には何ですか? 状態は、私があまり理解していない概念です。

設計ガイドラインの観点から、不変クラスはユーザー入力を受け入れず、実際には値を返すだけのクラスでなければなりませんか?

私の理解では、情報を返すだけのオブジェクトは不変で「ロックダウン」する必要がありますよね?そのため、その 1 つのメソッドを使用して専用クラスで現在の時刻を返したい場合は、参照型を使用する必要があります。これは型の参照として機能するため、不変性の恩恵を受けます。

4

15 に答える 15

58

不変性とは?

  • 不変性は、主にオブジェクト (文字列、配列、カスタム Animal クラス) に適用されます。
  • 通常、クラスの不変バージョンがある場合、可変バージョンも使用できます。たとえば、Objective-C と Cocoa は NSString クラス (不変) と NSMutableString クラスの両方を定義します。
  • オブジェクトが不変の場合、作成後に変更することはできません (基本的に読み取り専用)。「コンストラクターだけがオブジェクトを変更できる」と考えることができます。

これは、ユーザー入力とは直接関係ありません。コードでさえ、不変オブジェクトの値を変更することはできません。ただし、いつでも新しい不変オブジェクトを作成して、それを置き換えることができます。疑似コードの例を次に示します。多くの言語では、以下で行ったようにコンストラクターを使用する代わりに簡単に実行できることに注意してください。ただしmyString = "hello";、わかりやすくするためにコンストラクターを含めました。

String myString = new ImmutableString("hello");
myString.appendString(" world"); // Can't do this
myString.setValue("hello world"); // Can't do this
myString = new ImmutableString("hello world"); // OK

あなたは「情報を返すだけのオブジェクト」に言及しています。これは、自動的に不変性の良い候補になるわけではありません。不変オブジェクトは、構築されたときと同じ値を常に返す傾向があるため、現在の時刻は頻繁に変化するため、理想的ではないと言いがちです。ただし、特定のタイムスタンプで作成され、常にその 1 つのタイムスタンプを将来返す MomentOfTime クラスを持つことができます。

不変性の利点

  • オブジェクトを別の関数/メソッドに渡す場合、関数が戻った後にそのオブジェクトが同じ値を持つかどうかを心配する必要はありません。例えば:

    String myString = "HeLLo WoRLd";
    String lowercasedString = lowercase(myString);
    print myString + " was converted to " + lowercasedString;
    

    lowercase()小文字バージョンを作成していたため、 myStringの実装が変更された場合はどうなるでしょうか? 3 行目では、必要な結果が得られません。もちろん、良いlowercase()関数はこれを行いませんが、myString が不変であれば、この事実は保証されます。そのため、不変オブジェクトは、適切なオブジェクト指向プログラミング プラクティスを実施するのに役立ちます。

  • 不変オブジェクトをスレッドセーフにする方が簡単です

  • クラスの実装を簡素化する可能性があります (クラスを書いている場合は便利です)。

オブジェクトのすべてのインスタンス変数を取り、それらの値を紙に書き留めるとしたら、それがその時点でのそのオブジェクトの状態になります。プログラムの状態は、特定の瞬間におけるすべてのオブジェクトの状態です。状態は時間とともに急速に変化します。プログラムは、実行を継続するために状態を変更する必要があります。

ただし、不変オブジェクトは、時間の経過とともに状態が固定されます。いったん作成されると、不変オブジェクトの状態は変更されませんが、プログラム全体の状態は変更される可能性があります。これにより、何が起こっているかを追跡しやすくなります (上記の他の利点も参照してください)。

于 2009-03-08T00:00:35.990 に答える
23

不変性

簡単に言えば、初期化後に変更されない場合、メモリは不変です。

C、Java、C# などの命令型言語で記述されたプログラムは、メモリ内データを自由に操作できます。いったん取っておかれた物理メモリの領域は、プログラムの実行中いつでも、実行スレッドによって全体または一部が変更される可能性があります。実際、命令型言語はこのようなプログラミング方法を奨励しています。

この方法でプログラムを作成することは、シングルスレッド アプリケーションでは非常に成功しています。しかし、最新のアプリケーション開発が 1 つのプロセス内で複数の同時実行スレッドに移行するにつれて、潜在的な問題と複雑さの世界が導入されています。

実行スレッドが 1 つしかない場合、この 1 つのスレッドがメモリ内のすべてのデータを「所有」しているため、自由に操作できると考えることができます。ただし、複数の実行スレッドが関与する場合、所有権の暗黙の概念はありません。

代わりに、この負担はプログラマーにかかっており、プログラマーはメモリ内構造がすべてのリーダーに対して一貫した状態であることを保証するために多大な労力を費やす必要があります。ロック構造は、別のスレッドによって更新されている間、あるスレッドがデータを参照できないように注意して使用する必要があります。この調整がなければ、スレッドは更新の途中にあるデータを消費することは避けられません。このような状況の結果は予測不可能であり、しばしば壊滅的です。さらに、コード内でロックを正しく機能させることは非常に困難であり、下手をするとパフォーマンスが低下したり、最悪の場合、デッドロックが発生して実行が回復不能になる可能性があります。

不変のデータ構造を使用すると、複雑なロックをコードに導入する必要がなくなります。プログラムの存続期間中にメモリのセクションが変更されないことが保証されている場合、複数のリーダーがメモリに同時にアクセスできます。その特定のデータを一貫性のない状態で観察することはできません。

Lisp、Haskell、Erlang、F#、Clojure などの多くの関数型プログラミング言語は、その性質上、不変のデータ構造を推奨しています。ますます複雑化するマルチスレッド アプリケーション開発や多数のコンピュータを使用するコンピュータ アーキテクチャに移行するにつれて、彼らへの関心が再び高まっているのはこのためです。

アプリケーションの状態は、特定の時点でのすべてのメモリと CPU レジスタの内容と簡単に考えることができます。

論理的には、プログラムの状態は次の 2 つに分けられます。

  1. ヒープの状態
  2. 実行中の各スレッドのスタックの状態

C# や Java などのマネージド環境では、あるスレッドが別のスレッドのメモリにアクセスすることはできません。したがって、各スレッドはそのスタックの状態を「所有」します。structスタックは、値型 ( ) のローカル変数とパラメーター、およびオブジェクトへの参照を保持していると考えることができます。これらの値は、外部スレッドから分離されています。

ただし、ヒープ上のデータはすべてのスレッド間で共有可能であるため、同時アクセスを制御するには注意が必要です。すべての参照型 ( class) オブジェクト インスタンスはヒープに格納されます。

OOP では、クラスのインスタンスの状態はそのフィールドによって決定されます。これらのフィールドはヒープに格納されるため、すべてのスレッドからアクセスできます。コンストラクターの完了後にフィールドを変更できるメソッドをクラスが定義している場合、そのクラスは可変です (不変ではありません)。フィールドを変更できない場合、型は不変です。readonlyすべての C# /Javafinalフィールドを持つクラスが必ずしも不変であるとは限らないことに注意することが重要です。これらの構造は、参照が変更できないことを保証しますが、参照されるオブジェクトは変更できません。たとえば、フィールドにはオブジェクトのリストへの変更不可能な参照がある場合がありますが、リストの実際の内容はいつでも変更できます。

型を真に不変であると定義することにより、その状態は凍結されていると見なすことができるため、その型は複数のスレッドから安全にアクセスできます。

実際には、すべての型を不変として定義するのは不便な場合があります。不変型の値を変更するには、かなりの量のメモリ コピーが必要になる場合があります。一部の言語は、このプロセスを他の言語よりも簡単にしますが、どちらの方法でも、CPU は余分な作業を行うことになります。メモリのコピーに費やされた時間がロックの競合の影響を上回るかどうかを判断するには、多くの要因が影響します。

リストやツリーなどの不変データ構造の開発については、多くの研究が行われています。このような構造、たとえばリストを使用する場合、「追加」操作は、新しいアイテムが追加された新しいリストへの参照を返します。前のリストへの参照には変更は見られず、データの一貫したビューが引き続き表示されます。

于 2009-03-08T00:11:49.490 に答える
7

不変のものは決して変わらない。可変なものは変化する可能性があります。可変なものは変化します。不変のものは変化しているように見えますが、実際には新しい可変のものを作成します。

たとえば、ここに Clojure のマップがあります

(def imap {1 "1" 2 "2"})
(conj imap [3 "3"])
(println imap)

最初の行は、新しい不変の Clojure マップを作成します。2 行目は、3 と "3" をマップに結合します。これは古いマップを変更しているように見えるかもしれませんが、実際には3 つの「3」が追加された新しいマップを返しています。これは、不変性の代表的な例です。これが変更可能なマップであった場合、同じ古いマップ3 "3" を直接追加するだけです。3行目は地図を印刷します

{3 "3", 1 "1", 2 "2"}

不変性は、コードをクリーンで安全に保つのに役立ちます。これと他の理由により、関数型プログラミング言語は不変性とステートフル性の低下に傾倒する傾向があります。

于 2009-03-07T23:11:28.557 に答える
3

良い質問。

マルチスレッド。すべての型が不変の場合、競合状態は存在せず、コードで必要な数のスレッドを安全にスローできます。

明らかに、複雑な計算を保存する可変性がなければ、それほど多くのことを達成することはできないため、通常、機能するビジネス ソフトウェアを作成するには、ある程度の可変性が必要です。ただし、トランザクショナルなものなど、どこに不変性があるべきかを認識することは価値があります。

哲学の詳細については、関数型プログラミングと純粋性の概念を参照してください。コレクションや静的に利用可能なオブジェクトなどの参照を介して利用できるようにするのではなく、コール スタック (メソッドに渡すパラメーター) に保存するほど、プログラムはより純粋になり、競合状態が発生しにくくなります。最近のマルチコアの増加により、このトピックはより重要になっています。

また、不変性はプログラムの可能性の量を減らし、潜在的な複雑さとバグの可能性を減らします。

于 2009-03-07T23:22:59.717 に答える
2

もう一つ付け加えさせてください。上記のすべてに加えて、次の不変性も必要です。

于 2009-03-07T23:49:19.200 に答える
1

不変性の理由

  1. エラーが発生しにくく、より安全です。

  2. 不変クラスは、可変クラスよりも設計、実装、および使用が容易です。

  3. 不変オブジェクトはスレッドセーフであるため、同期の問題はありません。

  4. 不変オブジェクトは、通常、一度作成されると変更されないため、適切な Map キーと Set 要素です。

  5. 不変性により、コードの記述、使用、および推論が容易になります (クラスの不変条件は一度確立され、その後は変更されません)。

  6. オブジェクト間の競合がないため、不変性によりプログラムの並列化が容易になります。

  7. 例外があっても、プログラムの内部状態は一貫しています。

  8. 不変オブジェクトへの参照は、変更されないためキャッシュできます (つまり、ハッシュでは高速な操作が提供されます)。

より詳細な回答については、私のブログを参照してください:
http://javaexplorer03.blogspot.in/2015/07/minimize-mutability.html

于 2016-12-24T04:04:25.333 に答える
1

「……なんで気にしなきゃいけないの?」

実際の例は、文字列の反復連結です。たとえば、.NET では次のようになります。

string SlowStringAppend(string [] files)
{
    // Declare an string
    string result="";

    for (int i=0;i<files.length;i++)
    {
        // result is a completely new string equal to itself plus the content of the new
        // file
        result = result + File.ReadAllText(files[i]);
    }

    return result;
}    

string EfficientStringAppend(string [] files)
{
    // Stringbuilder manages a internal data buffer that will only be expanded when absolutely necessary
    StringBuilder result=new SringBuilder();

    for (int i=0;i<files.length;i++)
    {
        // The pre-allocated buffer (result) is appended to with the new string 
        // and only expands when necessary.  It doubles in size each expansion
        // so need for allocations become less common as it grows in size. 
        result.Append(File.ReadAllText(files[i]));
    }

    return result.ToString();
}

残念ながら、最初の (遅い) 関数を使用するアプローチは、今でも一般的に使用されています。不変性を理解すると、StringBuilder を使用することが非常に重要である理由が非常に明確になります。

于 2009-03-08T03:19:40.817 に答える
1

不変オブジェクトを変更することはできないため、それを置き換える必要があります....「変更するには」。つまり、交換してから廃棄します。この意味での「置換」とは、ポインターを (古い値の) あるメモリー位置から別の (新しい値の) メモリー位置に変更することを意味します。

そうすることで、追加のメモリを使用することに注意してください。一部は古い値用で、一部は新しい値用です。また、次のようなコードを見て混乱する人もいます。

string mystring = "inital value";
mystring = "new value";
System.Console.WriteLine(mystring); // Outputs "new value";

「しかし、私はそれを変更しています。白黒で見てください! mystring は '新しい値' を出力します... 私はそれを変更できないと言ったと思いましたか?!!」

しかし、実際には、この新しいメモリの割り当てが起こっているのです。つまり、mystring は現在、別のメモリ アドレスと空間を指しています。この意味での「不変」は、mystring の値を参照するのではなく、変数 mystring がその値を格納するために使用するメモリを参照しています。

特定の言語では、古い値を格納しているメモリを手動でクリーンアップする必要があります。つまり、プログラマが明示的に解放する必要があります。.....そして、忘れずにそうしてください。他の言語では、これは言語の自動機能、つまり .Net のガベージ コレクションです。

これが実際に re:memory の使用を吹き飛ばす場所の 1 つは、非常に反復的なループであり、特に Ashs の投稿のように文字列を使用しています。反復ループで HTML ページを作成していて、常に次の HTML ブロックを最後に追加していたとします。これは、大容量のサーバーで実行していました。この「新しい値のメモリ」の一定の割り当ては、「古い値のメモリ」が適切にクリーンアップされていない場合、すぐに高価になり、最終的には致命的になります。

もう 1 つの問題は、ガベージ コレクション (GC) などの処理がすぐに行われると考える人がいるということです。しかし、そうではありません。より多くのアイドル期間中にガベージ コレクションが発生するように設定するなど、さまざまな最適化が行われます。そのため、メモリが破棄されたとマークされてからガベージ コレクタによって実際に解放されるまでの間に、かなりの遅延が発生する可能性があります.

GC がメモリを使い果たす前に動作する機会を得られない場合、自動ガベージ コレクションを持たない他の言語のように、物事が失敗することはありません。代わりに、GC は最も優先度の高いプロセスとして開始され、破棄されたメモリを解放します。タイミングがどんなに悪くても、クリーンアップ中にブロッキング プロセスになります。明らかに、これはクールではありません。

したがって、基本的には、これらのことを念頭に置いてコーディングし、使用している言語のドキュメントを調べて、このリスクを回避/軽減できるベスト プラクティス/パターンを調べる必要があります。

Ashs の投稿のように、.Net および文字列では、文字列の値を常に変更する必要がある場合は、不変の文字列クラスではなく、可変の StringBuilder クラスを使用することをお勧めします。

他の言語/タイプにも同様に独自の回避策があります。

于 2009-03-08T04:48:34.987 に答える
1

物事を不変にすることで、多くのよくある間違いを防ぐことができます。

たとえば、生徒は自分の生徒に # 変更を加えるべきではありません。変数を設定する方法を提供しない場合 (そしてそれを const、または final、または言語がサポートするものにする)、コンパイル時にそれを強制できます。

物事が可変であり、それらを渡すときにそれらを変更したくない場合は、渡す防御コピーを作成する必要があります。次に、呼び出すメソッド/関数がアイテムのコピーを変更すると、元のアイテムは変更されません。

物事を不変にするということは、防御的なコピーを作成するために覚えておく必要がない (または時間/記憶を取る必要がない) ことを意味します。

実際に取り組み、作成した各変数について考えると、変数の大部分 (通常は 90 ~ 95%) は、値が与えられると変更されないことがわかります。これを行うと、プログラムが追跡しやすくなり、バグの数が減ります。

状態に関する質問に答えるために、状態は「オブジェクト」(クラスまたは構造体)の変数が持つ値です。人の「オブジェクト」状態を取り上げると、目の色、髪の色、髪の長さなどになります...それらのいくつか(目の色など)は変化しませんが、髪の長さなどは変化します.

于 2009-03-07T23:12:55.123 に答える
0

不変オブジェクトによって提供される潜在的なパフォーマンス上の利点の例は、WPF API で利用できます。多くの WPF 型の共通の基本クラスはFreezable.

いくつかの WPF の例は、ロックとコピーが不要なため、オブジェクトをフリーズ (実行時に不変にする) すると、アプリケーションのパフォーマンスが大幅に向上することを示唆しています。

個人的には、私が最も頻繁に使用する言語である C# で、不変性の概念をより簡単に表現できればと思います。readonlyフィールドに使用できる修飾子があります。readonly読み取り専用タイプの読み取り専用フィールドのみを持つタイプに対してのみ許可されるタイプの修飾子も見たいと思います。基本的に、これは、構築時にすべての状態を注入する必要があり、オブジェクト グラフ全体が凍結されることを意味します。このメタデータが CLR に固有のものであれば、GC のガベージ分析を最適化するために簡単に使用できると思います。

于 2009-03-08T14:06:06.363 に答える
0

申し訳ありませんが、不変性によって競合状態が防止されるのはなぜですか (この例では、読み取り後に書き込みが発生する危険があります)。

shared v = Integer(3)
v = Integer(v.value() + 1) # in parallel
于 2009-04-09T08:33:25.563 に答える