44

struct私は頻繁にs が不変であるべきだと読んでいます - それらは定義上そうではありませんか?

int不変だと思いますか?

int i = 0;
i = i + 123;

大丈夫そうです - 新しい を取得し、intに割り当て直しiます。これはどうですか?

i++;

わかりました、ショートカットと考えることができます。

i = i + 1;

はどうstruct Pointですか?

Point p = new Point(1, 2);
p.Offset(3, 4);

これは本当にポイントを変更し(1, 2)ますか? Point.Offset()新しいポイントを返すことで、次のショートカットと考えるべきではないでしょうか。

p = p.Offset(3, 4);

この考えの背景はこれです - アイデンティティのない値の型はどのようにして変更可能になるのでしょうか? 変更されたかどうかを判断するには、少なくとも 2 回確認する必要があります。しかし、アイデンティティなしでどうやってこれを行うことができますか?

refパラメータとボクシングを考慮して、これについての推論を複雑にしたくありません。p = p.Offset(3, 4);が不変性を表現するよりもはるかに優れていることも認識してp.Offset(3, 4);います。しかし、疑問が残ります - 定義上、値型は不変ではないのでしょうか?

アップデート

変数またはフィールドの可変性と、変数の値の可変性という、少なくとも 2 つの概念が関係していると思います。

public class Foo
{
    private Point point;
    private readonly Point readOnlyPoint;

    public Foo()
    {
        this.point = new Point(1, 2);
        this.readOnlyPoint = new Point(1, 2);
    }

    public void Bar()
    {
        this.point = new Point(1, 2);
        this.readOnlyPoint = new Point(1, 2); // Does not compile.

        this.point.Offset(3, 4); // Is now (4, 6).
        this.readOnlyPoint.Offset(3, 4); // Is still (1, 2).
    }
}

この例では、可変フィールドと不変フィールドを使用する必要があります。値型フィールドには値全体が含まれるため、不変フィールドに格納される値型も不変である必要があります。私はまだ結果に非常に驚いています.readonlyフィールドが変更されないままになるとは思っていませんでした.

変数 (定数を除く) は常に変更可能であるため、値型の変更可能性に対する制限はありません。


答えはそれほど単純ではないようですので、質問を言い換えます。

以下を考える。

public struct Foo
{
    public void DoStuff(whatEverArgumentsYouLike)
    {
        // Do what ever you like to do.
    }

    // Put in everything you like - fields, constants, methods, properties ...
}

の完全なバージョンFooと使用例 (refパラメーターとボックス化が含まれる場合があります) を提供していただけますか?

foo.DoStuff(whatEverArgumentsYouLike);

foo = foo.DoStuff(whatEverArgumentsYouLike);
4

12 に答える 12

55

オブジェクトが作成された後にその状態が変わらない場合、そのオブジェクトは不変です。

簡単な答え: いいえ、定義上、値の型は不変ではありません。構造体とクラスは両方とも、変更可能または不変のいずれかになります。4つの組み合わせすべてが可能です。構造体またはクラスに、読み取り専用ではないパブリック フィールド、setter を持つパブリック プロパティ、またはプライベート フィールドを設定するメソッドがある場合、その型の新しいインスタンスを作成せずに状態を変更できるため、変更可能です。


長い回答: まず第一に、不変性の問題は、フィールドまたはプロパティを持つ構造体またはクラスにのみ適用されます。最も基本的な型 (数値、文字列、および null) は、変更するもの (フィールド/プロパティ) がないため、本質的に不変です。A 5 is a 5 is a 5. 5 に対する操作は、別の不変値のみを返します。

などの変更可能な構造体を作成できますSystem.Drawing.Point。と の両方XY、構造体のフィールドを変更するセッターがあります。

Point p = new Point(0, 0);
p.X = 5;
// we modify the struct through property setter X
// still the same Point instance, but its state has changed
// it's property X is now 5

一部の人々は、値型が参照ではなく値 (したがってその名前) によって渡されるという事実と不変性を混同しているようです。

void Main()
{
    Point p1 = new Point(0, 0);
    SetX(p1, 5);
    Console.WriteLine(p1.ToString());
}

void SetX(Point p2, int value)
{
    p2.X = value;
}

この場合Console.WriteLine()は「 」と書きます{X=0,Y=0}のコピーである変更p1があったため、ここは変更されませんでした。これは、 が不変である(そうではない)ためではなく、値の型であるために発生します。SetX()p2p1p1

なぜ値型は不変でなければならないのですか? 理由はたくさんあります...この質問を参照してください。ほとんどの場合、可変値型があらゆる種類のそれほど明白ではないバグにつながるためです。上記の例では、プログラマーp1は. または、後で変更できる値でソートすることを想像してみてください。その後、ソートされたコレクションは期待どおりにソートされなくなります。同じことが辞書とハッシュにも当てはまります。The Fabulous Eric Lippert (ブログ) は、不変性と、不変性が C# の未来であると彼が信じる理由について、シリーズ全体を書いています。これは、読み取り専用変数を「変更」できる彼の例の 1 つです。(5, 0)SetX()


更新:あなたの例:

this.readOnlyPoint.Offset(3, 4); // Is still (1, 2).

読み取り専用変数の変更に関する投稿で Lippert が言及したものとまったく同じです。Offset(3,4)実際には a を変更しましたPointが、それは のコピーでありreadOnlyPoint、何にも割り当てられていないため、失われています。

可変値型が悪いのはそのためです。実際にはコピーを変更しているときに、何かを変更していると思わせてしまい予期しないバグにつながることがありますPointが不変の場合Offset()、 new を返すPoint必要があり、それを に割り当てることができませんでしたreadOnlyPoint。そして、「ああ、それは理由で読み取り専用です。なぜ私はそれを変更しようとしたのですか?コンパイラが今私を止めてくれて良かったです。」


更新: あなたの言い換えられた要求について... 私はあなたが何を得ているか知っていると思います. ある意味では、構造体を内部的に不変であると「考える」ことができます。構造体を変更することは、それを変更されたコピーで置き換えることと同じです。私が知っている限り、それはCLRがメモリ内で内部的に行うことでさえあるかもしれません。(これがフラッシュメモリの仕組みです。数バイトだけを編集することはできません。キロバイトのブロック全体をメモリに読み込み、必要ないくつかを変更し、ブロック全体を書き戻す必要があります。)ただし、それらが「内部的に不変」であったとしても"、これは実装の詳細であり、構造体 (必要に応じてインターフェイスまたは API) のユーザーである私たち開発者にとって、構造体は変更できます。その事実を無視して、「それらを不変と考える」ことはできません。

コメントで、「フィールドまたは変数の値を参照することはできません」と述べました。1 つのコピーを変更しても他の変数に影響を与えないように、すべての構造体変数には異なるコピーがあると想定しています。それは完全に真実ではありません。以下の場合、以下の行は置換できません...

interface IFoo { DoStuff(); }
struct Foo : IFoo { /* ... */ }

IFoo otherFoo = new Foo();
IFoo foo = otherFoo;
foo.DoStuff(whatEverArgumentsYouLike); // line #1
foo = foo.DoStuff(whatEverArgumentsYouLike); // line #2

行 #1 と #2 の結果は同じではありません...なぜですか? fooおよびFooの同じボックス化されたインスタンスotherFooを参照するためです。1 行目で変更された内容はすべてに反映されます。行 #2 は新しい値に置き換えられ、何もしません(新しいインスタンスを返し、それ自体を変更しないと仮定します)。foootherFoofoootherFooDoStuff()IFoofoo

Foo foo1 = new Foo(); // creates first instance
Foo foo2 = foo1; // create a copy (2nd instance)
IFoo foo3 = foo2; // no copy here! foo2 and foo3 refer to same instance

変更してもまたはfoo1には影響しません。変更は に反映されますが、 には反映されません。変更は に反映されますが、 には反映されません。foo2foo3foo2foo3foo1foo3foo2foo1

混乱しますか?不変の値型に固執すれば、それらのいずれかを変更する衝動を排除できます。


更新: 最初のコード サンプルのタイプミスを修正

于 2009-05-15T15:15:04.497 に答える
11

可変性と値型は、2 つの別個のものです。

型を値型として定義することは、ランタイムがランタイムへの参照ではなく値をコピーすることを示します。一方、可変性は実装に依存し、各クラスは必要に応じて実装できます。

于 2009-05-15T12:39:31.203 に答える
8

変更可能な構造体を作成できますが、値の型を不変にすることをお勧めします。

たとえば、DateTime は、操作を行うときに常に新しいインスタンスを作成します。ポイントはミュータブルで、変更することができます。

あなたの質問に答えるには:いいえ、それらは定義上不変ではありません。変更可能かどうかはケースによって異なります。たとえば、それらが辞書のキーとして機能する必要がある場合、それらは不変でなければなりません。

于 2009-05-15T12:36:50.023 に答える
5

ロジックを十分に進めれば、すべての型が不変になります。参照型を変更すると、何かを変更するのではなく、実際には同じアドレスに新しいオブジェクトを書き込んでいると主張できます。

または、どの言語でもすべてが変更可能であると主張することもできます。これは、以前はある目的で使用されていたメモリが別の目的で上書きされる場合があるためです。

十分な抽象化を行い、十分な言語機能を無視すれば、好きな結論にたどり着くことができます。

そして、それは要点を逃しています。.NET 仕様によると、値の型は変更可能です。変更できます。

int i = 0;
Console.WriteLine(i); // will print 0, so here, i is 0
++i;
Console.WriteLine(i); // will print 1, so here, i is 1

しかし、それはまだ同じです。変数iは一度だけ宣言されます。この宣言の後に発生することはすべて変更です。

不変変数を持つ関数型言語のようなものでは、これは合法ではありません。++i は不可能です。変数が宣言されると、その値は固定値になります。

.NET ではそうではありませんi。宣言された後に を変更するのを止めるものは何もありません。

もう少し考えた後、より良いかもしれない別の例を次に示します。

struct S {
  public S(int i) { this.i = i == 43 ? 0 : i; }
  private int i;
  public void set(int i) { 
    Console.WriteLine("Hello World");
    this.i = i;
  }
}

void Foo {
  var s = new S(42); // Create an instance of S, internally storing the value 42
  s.set(43); // What happens here?
}

最後の行では、あなたの論理によれば、実際に新しいオブジェクトを構築し、その値で古いオブジェクトを上書きすると言えます。しかし、それは不可能です!新しいオブジェクトを作成するには、コンパイラはi変数を 42 に設定する必要があります。ただし、これは非公開です! 値 43 を明示的に禁止する (代わりに 0 に設定する) ユーザー定義のコンストラクターを介してのみアクセスできます。次にset、厄介な副作用を持つメソッドを介してアクセスします。コンパイラには、好きな値で新しいオブジェクトを作成する方法がありません。s.i43 に設定できる唯一の方法は、 を呼び出して現在のオブジェクトを変更するset()ことです。コンパイラは、プログラムの動作を変更するため、それだけでは実行できません (コンソールに出力されます)。

したがって、すべての構造体が不変であるためには、コンパイラーは言語の規則をごまかして破る必要があります。そしてもちろん、ルールを破ることをいとわないのであれば、何でも証明できます。すべての整数も等しいこと、または新しいクラスを定義するとコンピューターが発火することを証明できます。言語のルール内にとどまっている限り、構造体は変更可能です。

于 2009-05-15T12:47:47.487 に答える
4

ref パラメータとボクシングを考慮して、これについての推論を複雑にしたくありません。p = p.Offset(3, 4);が不変性を表現するよりもはるかに優れ ていることも認識してp.Offset(3, 4);います。しかし、疑問が残ります - 定義上、値型は不変ではないのでしょうか?

では、あなたは現実の世界で実際に活動しているわけではありませんよね? 実際には、値型が関数間を移動するときに自分自身のコピーを作成する傾向は、不変性とうまく調和しますが、不変にしない限り、実際には不変ではありません。他のもののように。

于 2009-05-15T12:39:32.607 に答える
4

定義上、値型は不変ではありませんか?

いいえ、そうではありません。System.Drawing.Pointたとえば、構造体を見ると、Xプロパティにセッターとゲッターがあります。

ただし、すべての値の型は不変の API で定義する必要があると言うのは本当かもしれません。

于 2009-05-15T12:40:45.567 に答える
2

混乱は、値型のように振る舞うべき参照型がある場合、それを不変にすることをお勧めします。値型と参照型の主な違いの 1 つは、参照型の 1 つの名前によって行われた変更が、別の名前に現れる可能性があることです。これは値型では起こりません:

public class foo
{
    public int x;
}

public struct bar
{
    public int x;
}


public class MyClass
{
    public static void Main()
    {
        foo a = new foo();
        bar b = new bar();

        a.x = 1;
        b.x = 1;

        foo a2 = a;
        bar b2 = b;

        a.x = 2;
        b.x = 2;

        Console.WriteLine( "a2.x == {0}", a2.x);
        Console.WriteLine( "b2.x == {0}", b2.x);
    }
}

プロデュース:

a2.x == 2
b2.x == 1

ここで、値のセマンティクスを持ちたいが、実際には値の型にしたくない型がある場合 - 必要なストレージが多すぎるなどの理由で、不変性が一部であることを考慮する必要があります。デザイン。不変の参照型を使用すると、既存の参照を変更すると、既存の参照を変更するのではなく、新しいオブジェクトが生成されるため、保持している値を他の名前で変更できないという値型の動作が得られます。

もちろん、System.String クラスはそのような動作の代表的な例です。

于 2009-05-15T13:34:48.073 に答える
2

昨年、構造体を不変にしないことで発生する可能性のある問題に関するブログ投稿を書きました。

投稿の全文はここで読むことができます

これは、物事がひどくうまくいかない例です。

//Struct declaration:

struct MyStruct
{
  public int Value = 0;

  public void Update(int i) { Value = i; }
}

コードサンプル:

MyStruct[] list = new MyStruct[5];

for (int i=0;i<5;i++)
  Console.Write(list[i].Value + " ");
Console.WriteLine();

for (int i=0;i<5;i++)
  list[i].Update(i+1);

for (int i=0;i<5;i++)
  Console.Write(list[i].Value + " ");
Console.WriteLine();

このコードの出力は次のとおりです。

0 0 0 0 0
1 2 3 4 5

同じことをしましょう。ただし、配列をジェネリックに置き換えList<>ます。

List<MyStruct> list = new List<MyStruct>(new MyStruct[5]); 

for (int i=0;i<5;i++)
  Console.Write(list[i].Value + " ");
Console.WriteLine();

for (int i=0;i<5;i++)
  list[i].Update(i+1);

for (int i=0;i<5;i++)
  Console.Write(list[i].Value + " ");
Console.WriteLine();

出力は次のとおりです。

0 0 0 0 0
0 0 0 0 0

説明はとても簡単です。いいえ、それはボクシング/アンボクシングではありません...

配列から要素にアクセスする場合、ランタイムは配列要素を直接取得するため、Update() メソッドは配列項目自体で機能します。これは、配列内の構造体自体が更新されることを意味します。

2 番目の例では、ジェネリックを使用しましたList<>。特定の要素にアクセスするとどうなるでしょうか? さて、メソッドである indexer プロパティが呼び出されます。値の型は、メソッドによって返されるときに常にコピーされるため、まさにこれが行われます。リストのインデクサー メソッドは、内部配列から構造体を取得し、それを呼び出し元に返します。これは値の型に関するものであるため、コピーが作成され、そのコピーに対して Update() メソッドが呼び出されますが、もちろんリストの元の項目には影響しません。

つまり、いつコピーが作成されるかわからないため、構造体が不変であることを常に確認してください。ほとんどの場合、それは明らかですが、場合によっては本当に驚くことがあります...

于 2009-06-02T14:15:39.657 に答える
1

型が変更可能か不変かを定義するには、その「型」が何を参照しているかを定義する必要があります。参照型の格納場所が宣言されている場合、宣言は、別の場所に格納されているオブジェクトへの参照を保持するためのスペースを割り当てるだけです。宣言は、問題の実際のオブジェクトを作成しません。それにもかかわらず、特定の参照型について話すほとんどのコンテキストでは、参照を保持する格納場所についてではなく、その参照によって識別されるオブジェクトについて話します。オブジェクトへの参照を保持しているストレージの場所に書き込むことができるという事実は、オブジェクト自体が変更可能であることを意味するものではありません。

対照的に、値型の格納場所が宣言されている場合、システムは、その値型が保持するパブリックまたはプライベート フィールドごとに、その格納場所内にネストされた格納場所を割り当てます。値の型に関するすべては、そのストレージの場所に保持されます。fooタイプの変数Pointとその 2 つのフィールドを定義する場合、XYは、それぞれ 3 と 6 を保持します。Pointinの「インスタンス」をfieldsfooのペアとして定義すると、そのインスタンスは、 が変更可能な場合にのみ変更可能になります。のインスタンスをそれらのフィールドに保持される値として定義する場合(例: "3,6")、そのようなインスタンスは、定義上、不変です。これらのフィールドの 1 つを変更すると、fooPointPoint別のインスタンスを保持します。

値型の「インスタンス」は、フィールドが保持する値ではなく、フィールドであると考える方が役立つと思います。その定義により、変更可能なストレージの場所に格納され、デフォルト以外の値が存在する値型は、宣言方法に関係なく、常に変更可能になります。ステートメントは、フィールドとを使用しMyPoint = new Point(5,8)て の新しいインスタンスを構築し、そのフィールドの値を新しく作成された の値に置き換えることによって変更します。構造体がそのコンストラクターの外部でそのフィールドを変更する方法を提供しない場合でも、構造体型がインスタンスを保護して、そのすべてのフィールドが別のインスタンスの内容で上書きされないようにする方法はありません。PointX=5Y=8MyPointPoint

ちなみに、変更可能な構造体が他の手段では達成できないセマンティクスを達成できる簡単な例:myPoints[]が複数のスレッドにアクセス可能な単一要素の配列であると仮定し、20 個のスレッドが同時にコードを実行するとします。

Threading.Interlocked.Increment(myPoints[0].X);

ゼロmyPoints[0].Xに等しく開始し、20 個のスレッドが上記のコードを実行すると、同時かどうかにかかわらず、myPoints[0].X20 になります。上記のコードを模倣しようとすると、次のようになります。

myPoints[0] = new Point(myPoints[0].X + 1, myPoints[0].Y);

次に、myPoints[0].X別のスレッドがそれを読み取ってから改訂された値を書き戻すまでの間にいずれかのスレッドが読み取った場合、インクリメントの結果は失われます (その結果、myPoints[0].X1 から 20 の間の任意の値になる可能性があります)。

于 2012-06-09T20:35:09.767 に答える
1

いいえそうではありません。例:

Point p = new Point (3,4);
Point p2 = p;
p.moveTo (5,7);

この例moveTo()では、インプレース操作です。参照の背後に隠れる構造を変更しますp。を見てください。そのp2位置も変更されます。不変の構造でmoveTo()は、新しい構造を返す必要があります。

p = p.moveTo (5,7);

現在Pointは不変であり、コード内のどこにでも参照を作成しても、驚くことはありません。見てみましょうi

int i = 5;
int j = i;
i = 1;

これは違います。i不変ではない5です。そして、2 番目の代入は、 を含む構造体への参照をコピーしませんiが、 の内容をコピーしますi。そのため、舞台裏ではまったく異なることが起こります。メモリ内のアドレス (参照) のコピーだけではなく、変数の完全なコピーを取得します。

オブジェクトと同等のものは、コピー コンストラクターです。

Point p = new Point (3,4);
Point p2 = new Point (p);

ここでは、 の内部構造がp新しいオブジェクト/構造にコピーされ、p2それへの参照が含まれます。しかし、これは (上記の整数代入とは異なり) 非常にコストのかかる操作であるため、ほとんどのプログラミング言語が区別をつけています。

コンピュータがより強力になり、メモリが増えるにつれて、この区別はなくなります。これは、膨大な量のバグや問題を引き起こすためです. 次世代では、不変オブジェクトのみが存在し、すべての操作はトランザクションによって保護され、さらにintは本格的なオブジェクトになります。ガベージ コレクションと同様に、プログラムの安定性を大きく前進させるものであり、最初の数年間は多くの問題を引き起こしますが、信頼できるソフトウェアを作成できるようになります。今日のコンピューターは、これを行うには十分な速度ではありません。

于 2009-05-15T12:55:07.530 に答える
0

オブジェクト/構造体は、データを変更できないような方法で関数に渡され、返される構造体が構造体である場合、不変newです。古典的な例は

String s = "abc";

s.toLower();

「s」を置き換える新しい文字列が返されるようにtoLower関数が記述されている場合、それは不変ですが、関数が文字ごとに「s」内の文字を置き換え、「新しい文字列」を宣言しない場合、それは可変です。

于 2009-05-15T12:45:12.640 に答える