なぜ代入ステートメントは値を返すのですか?


126

これは許可されています:

int a, b, c;
a = b = c = 16;

string s = null;
while ((s = "Hello") != null) ;

私の理解では、割り当てs = ”Hello”;によって“Hello”が割り当てられるだけですsが、操作は値を返しません。それが真の場合、何にも比較されない((s = "Hello") != null)ため、エラーが発生しますnull

代入ステートメントが値を返すことを許可する背後にある理由は何ですか?


44
参考までに、この動作はC#仕様で定義されています。「単純な割り当て式の結果は、左のオペランドに割り当てられた値です。結果は左のオペランドと同じ型で、常に値として分類されます。
Michael Petrotta

1
@Skurmedel私は投票者ではありません。あなたに同意します。しかし、おそらくそれは修辞的な質問であると考えられていたからでしょうか?
jsmith

パスカル/デルファイの割り当てでは、:=は何も返しません。私はそれが嫌いです。
Andrey

20
Cブランチの言語の標準。割り当ては式です。
Hans Passant

「割り当て...はsに「Hello」が割り当てられるだけで、操作は値を返さないはずです」-なぜですか?「割り当てステートメントが値を返すことを許可する理由は何ですか?」-自分の例が示すように、それは便利だからです。(まあ、あなたの2番目の例はばかげていますが、そうでwhile ((s = GetWord()) != null) Process(s);はありません)。
ジムバルター2015

回答:


160

私の理解では、割り当てs = "Hello"; 「Hello」がsに割り当てられるだけですが、操作は値を返しません。

あなたの理解は100%間違っています。なぜこの偽りのことを信じるのか説明できますか?

代入ステートメントが値を返すことを許可する背後にある理由は何ですか?

まず、代入ステートメントは値を生成しません。割り当ては値を生成します。割り当て式は正当なステートメントです。C#で有効なステートメントである式はほんの一握りです。式の待機、インスタンスの構築、インクリメント、デクリメント、呼び出し、および割り当て式は、ステートメントが期待される場所で使用できます。

C#には、なんらかの値を生成しない1種類の式しかありません。つまり、voidを返すものとして型指定されたものの呼び出しです。(または、同等の結果値が関連付けられていないタスクの待機。)他のすべての種類の式は、値、変数、参照、プロパティアクセス、イベントアクセスなどを生成します。

ステートメントとして正当であるすべての式は、それらの副作用に役立ちます。それがここでの重要な洞察であり、割り当ては式ではなくステートメントでなければならないという直感の原因はおそらくあると思います。理想的には、ステートメントごとに1つの副作用があり、式には副作用がないことが理想です。ある副作用コードが全く表現コンテキストで使用することができることビット奇数。

この機能を許可する理由は、(1)多くの場合便利であり、(2)Cのような言語では慣用的であるためです。

質問が懇願されたことに気づくかもしれません:なぜこれがCのような言語で慣用的であるのですか?

残念ながら、デニス・リッチーはもう質問できなくなりましたが、私の割り当てでは、ほとんどの場合、レジスターに割り当てらればかりの値が残されると思います。Cは非常に「機械に近い」種類の言語です。Cの設計に合わせて、基本的に「割り当てたばかりの値を使い続ける」ことを意味する言語機能があることはもっともらしいようです。この機能のコードジェネレーターを作成するのは非常に簡単です。割り当てられた値を格納したレジスタを使い続けるだけです。


1
値を返すものは何も認識していませんでした(インスタンスの構築も式と見なされます)
user437291

9
@ user437291:インスタンスの構築が式でない場合、構築されたオブジェクトを使用して何も実行できません。オブジェクトに割り当てることも、メソッドに渡すことも、メソッドを呼び出すこともできません。その上に。かなり役に立たないでしょうね。
ティムウィ

1
変数の変更は、実際には代入演算子の「単なる」副作用であり、式を作成すると、主な目的として値が生成されます。
mk12 2012

2
「あなたの理解は100%正しくありません。」-OPの英語の使用は最高ではありませんが、彼/彼女の「理解」は、どうあるべきについてであり、それを意見にしているので、「100%正しくない」可能性があるようなものではありません。 。一部の言語設計者は、OPの「すべき」に同意し、代入に価値がないか、そうでなければ副式であることを禁止します。これは=/ ==typo に対する過剰反応だと思い=ます。かっこで囲まれていない限り、の値の使用を禁止することで簡単に対処できます。たとえば、if ((x = y))またはif ((x = y) == true)許可されていif (x = y)ますが許可されていません。
ジムバルター2015

「値を返すものはすべて式と見なされることを認識していませんでした」-うーん...値を返すすべてのものは式と見なされます-2つは実質的に同義です。
ジムバルター2015

44

あなたは答えを提供していませんか?それはあなたが言及した種類の構成を正確に有効にすることです。

代入演算子のこのプロパティが使用される一般的なケースは、ファイルから行を読み取ることです...

string line;
while ((line = streamReader.ReadLine()) != null)
    // ...

1
+1これは、この構成の最も実用的な用途の1つである必要があります
Mike Burton

11
私は本当に単純なプロパティ初期化子でそれが本当に好きですが、読みやすくするために「シンプル」で線を引くように注意する必要がありますreturn _myBackingField ?? (_myBackingField = CalculateBackingField());。nullをチェックして代入するよりもはるかにチャフが少ないです。
トムメイフィールド

34

割り当て式の私のお気に入りの使用法は、遅延初期化されたプロパティ用です。

private string _name;
public string Name
{
    get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); }
}

5
これが、非常に多くの人々が??=構文を提案した理由です。
コンフィグレータ

27

たとえば、次の例のように、割り当てを連鎖させることができます。

a = b = c = 16;

別の方法として、結果を単一の式に割り当てて確認することができます。

while ((s = foo.getSomeString()) != null) { /* ... */ }

どちらも疑わしい理由である可能性がありますが、これらの構造を好む人は確かにいます。


5
+1チェーニングは重要な機能ではありませんが、多くの機能が機能しない場合に「機能する」ことを確認すると便利です。
Mike Burton

チェーンの場合も+1。私はいつもそれを、「式の戻り値」の自然な副作用ではなく、言語によって処理される特別なケースと考えていました。
Caleb Bell、

2
また、あなたが戻っに割り当てを使用することができます:return (HttpContext.Current.Items["x"] = myvar);
セルジュ・シュルツ

14

既に述べた理由(割り当ての連鎖、whileループ内での設定とテストなど)を除いて、ステートメントを適切に使用するには、usingこの機能が必要です。

using (Font font3 = new Font("Arial", 10.0f))
{
    // Use font3.
}

MSDNは、破棄された後もスコープ内に残るため、usingステートメントの外で使い捨てオブジェクトを宣言しないことをお勧めします(私がリンクしたMSDNの記事を参照)。


4
これは割り当てチェーンそのものではないと思います。
ケニー

1
@ケニー:ええと...いいえ、でも私もそうだとは言っていません。既に述べた理由(割り当ての連鎖を含む)を除いて、代入演算子が代入演算の結果を返すという事実がなければ、usingステートメントの使用ははるかに難しくなります。これは、割り当てチェーンとはまったく関係ありません。
MartinTörnwall、2009

1
しかし、Ericが前述したように、「割り当てステートメントは値を返しません」。これは実際には、usingステートメントの単なる言語構文です。これは、usingステートメントで "代入式"を使用できないことの証明です。
ケニー

@ケニー:謝罪、私の用語は明らかに間違っていた。言語が値を返す代入式を一般的にサポートしていなくても、usingステートメントを実装できることは理解しています。それは、私の目には、ひどく矛盾しています。
MartinTörnwall、2009

2
@kenny:実際には、定義と割り当てのステートメント、またはの任意の式を使用できますusing。これらのすべてが有効です:using (X x = new X())using (x = new X())using (x)。ただし、この例では、usingステートメントの内容は、値を返す代入にまったく依存しない特別な構文でありFont font3 = new Font("Arial", 10.0f)、式ではなく、式を必要とする場所では無効です。
コンフィギュレー

10

Eric Lippertが彼の回答で述べた特定のポイントについて詳しく説明し、他の誰にもまったく触れられていない特定の機会にスポットライトを当てたいと思います。エリックは言った:

[...]割り当ては、ほとんどの場合、レジスタに割り当てられたばかりの値を残します。

割り当ては常に、左側のオペランドに割り当てようとした値を残します。「ほぼ常に」ではない。しかし、ドキュメントでコメントされているこの問題を見つけられなかったので、私にはわかりません。理論的には、「残して」左のオペランドを再評価しない非常に効果的な実装手順ですが、効率的ですか?

これまでのところ、このスレッドの回答で構築されたすべての例で「効率的」です。しかし、getおよびsetアクセサーを使用するプロパティとインデクサーの場合は効率的ですか?どういたしまして。このコードを考えてみましょう:

class Test
{
    public bool MyProperty { get { return true; } set { ; } }
}

ここには、プライベート変数のラッパーではないプロパティがあります。彼が呼ばれるときはいつでも彼は真を返すでしょう、彼が値を設定しようとするときはいつでも彼は何もしません。したがって、この特性が評価されるときはいつでも、彼は真実であろう。しばらく様子を見てみましょう:

Test test = new Test();

if ((test.MyProperty = false) == true)
    Console.WriteLine("Please print this text.");

else
    Console.WriteLine("Unexpected!!");

それが印刷するものを推測しますか?印刷しUnexpected!!ます。結局のところ、setアクセサは実際に呼び出され、何も実行しません。しかし、その後、getアクセサーはまったく呼び出されません。false割り当ては、プロパティに割り当てようとした値を残すだけです。そして、このfalse値はifステートメントが評価するものです。

最後に、この問題を調査させた実際の例を挙げます。私のList<string>クラスがプライベート変数として持っているコレクション()の便利なラッパーであるインデクサーを作成しました。

インデクサーに送信されたパラメーターは文字列でした。これは、私のコレクションの値として扱われます。getアクセサーは、その値がリストに存在するかどうかにかかわらず、単にtrueまたはfalseを返します。したがって、getアクセサーは、List<T>.Containsメソッドを使用する別の方法でした。

文字列を引数としてインデクサーのセットアクセサーが呼び出され、右側のオペランドがboolであるtrue場合、そのパラメーターをリストに追加します。ただし、同じパラメーターがアクセサーに送信され、正しいオペランドがboolのfalse場合、代わりにリストから要素が削除されます。したがって、セットアクセサは、List<T>.Addおよびの両方の便利な代替手段として使用されましたList<T>.Remove

自分のロジックをゲートウェイとして実装してリストをラップする、きちんとしたコンパクトな「API」があると思いました。インデクサーの助けを借りて、私はいくつかのキーストロークのセットで多くのことを行うことができました。たとえば、リストに値を追加して、そこにあることを確認するにはどうすればよいですか?私はこれが必要な唯一のコード行だと思いました:

if (myObject["stringValue"] = true)
    ; // Set operation succeeded..!

しかし、前の例で示したように、値が本当にリストにあるかどうかを確認するはずのgetアクセサーは呼び出されていません。true値は常に背後に効果的に私は私のgetアクセサに実装されていたものは何でも、論理破壊残っていました。


非常に興味深い答え。そして、テスト駆動開発について前向きに考えるようになりました。
エリオット

1
これは興味深いですが、エリックの回答に対するコメントであり、OPの質問に対する回答ではありません。
ジムバルター2015

割り当て式が最初にターゲットプロパティの値を取得することを期待していません。変数が可変で、型が一致する場合、古い値が何であるかが重要なのはなぜですか?新しい値を設定するだけです。私はIFテストでの割り当てのファンではありません。割り当てが成功したためにtrueが返されますか、それともいくつかの値(たとえば、trueに強制されている正の整数)か、またはブール値を割り当てているためですか?実装に関係なく、ロジックはあいまいです。
ダボス

6

割り当てが値を返さなかった場合、行a = b = c = 16も機能しません。

のようなものを書くことがwhile ((s = readLine()) != null)できることも時々役立ちます。

したがって、割り当てに割り当てられた値を返させる理由は、それらを実行できるようにするためです。


誰も言及した欠点-なぜtrueを返すのではなく、よくある割り当てと比較のバグ 'if(something = 1){}'がコンパイルに失敗するために、割り当てがnullを返せないのか疑問に思いました。私は今それが…他の問題を引き起こすだろうとわかりました!
Jon

1
@Jonそのバグは=、括弧で囲まれていない式での発生を禁止または警告することで簡単に防ぐことができます(if / whileステートメント自体の括弧は数えません)。gccはそのような警告を出し、それによってこのクラスのバグを、それを使用してコンパイルされたC / C ++プログラムから本質的に排除しました。他のコンパイラ作成者がこれとgccの他のいくつかの優れたアイデアにほとんど注意を払っていないのは残念です。
ジムバルター2015

4

パーサーが構文を解釈する方法を誤解していると思います。割り当てが最初に評価され、結果がNULLと比較されます。つまり、ステートメントは次と同等です。

s = "Hello"; //s now contains the value "Hello"
(s != null) //returns true.

他の人が指摘したように、割り当ての結果は割り当てられた値です。持っていることの利点を想像するのは難しいと思います

((s = "Hello") != null)

そして

s = "Hello";
s != null;

同等ではない ...


あなたの2行が同等であるという実際的な意味では正しいですが、割り当てが値を返さないというステートメントは技術的に正しくありません。私の理解では、割り当ての結果は(C#仕様に従って)値であり、その意味では、1 + 2 + 3のような加算演算子と言うことと違いはありません
Steve Ellinger

3

主な理由は、C ++およびCとの(意図的な)類似性だと思います。割り当て演算子(および他の多くの言語構造)をC ++の対応物と同じように動作させることは、驚きが最も少ないという原則に従い、プログラマーは別のカーリーから来ています。ブラケット言語は、あまり考えずにそれらを使用できます。C ++プログラマにとって簡単に習得できることは、C#の主要な設計目標の1つでした。


2

投稿に含める2つの理由により、
1)できるのでa = b = c = 16
2)割り当てが成功したかどうかをテストできます if ((s = openSomeHandle()) != null)


1

'a ++'または 'printf( "foo")'が自己完結型のステートメントとして、またはより大きな式の一部として有用であるという事実は、Cが式の結果が中古。そのため、値を有効に「返す」可能性のある式も同様に行うことができるという一般的な概念があります。問題のすべての変数が正確に同じ型を持っていない場合、割り当てチェーンはCではわずかに「興味深い」可能性があり、C ++ではさらに興味深い可能性があります。そのような使用法はおそらく回避するのが最善です。


1

もう1つの優れた使用例として、私はいつもこれを使用しています。

var x = _myVariable ?? (_myVariable = GetVariable());
//for example: when used inside a loop, "GetVariable" will be called only once

0

私がここで答えに示していない追加の利点は、割り当ての構文が算術に基づいていることです。

ここx = y = b = c = 2 + 3で、算術においてCスタイルの言語とは異なる何かを意味します。そのアサーション演算で、我々は、状態すなわちxがy等に等しく、Cスタイルの言語では、命令のになり、それが実行された後、xがy等に等しいです。

これは、正当な理由がない限り、算術とコードの間にはまだ十分な関係があるため、算術の自然な動作を禁止することは意味をなさないためです。(Cスタイル言語が等号の使用から取ったもう1つのことは、等号比較のための==の使用です。ただし、右端の==が値を返すため、この種の連鎖は不可能です。)


@JimBalter 5年後、あなたは実際に反対するかもしれないのだろうか。
ジョンハンナ

@JimBalterいいえ、2番目のコメントは実際には反論であり、健全なものです。私の考えの反省は、あなたの最初のコメントがそうではなかったからです。算術演算を学習する際それでも、私たちは学ぶa = b = cことの手段abし、c同じものですが。Cスタイルの言語を学ぶときa = b = caそれbは後に学びcます。私の答え自体が言うように、セマンティクスには確かに違いがありますが、それでも子供の頃=、割り当てに使用する言語をプログラミングすることを最初に学びましたが、a = b = cこれを許可していませんでしたが、私には不合理に思えました…
Jon Hanna

…その言語=は等値比較にも使用されるため、避けられないので、そのため、Cスタイルの言語での意味a = b = cを意味する必要がありa = b == cます。算術のアナロジーを描くことができたので、Cで許可されたチェーニングははるかに直感的にわかりました。
ジョンハンナ

0

一連のものを更新し、変更があったかどうかを返す必要があるときに、割り当ての戻り値を使用するのが好きです。

bool hasChanged = false;

hasChanged |= thing.Property != (thing.Property = "Value");
hasChanged |= thing.AnotherProperty != (thing.AnotherProperty = 42);
hasChanged |= thing.OneMore != (thing.OneMore = "They get it");

return hasChanged;

注意してください。あなたはそれをこれに短縮できると思うかもしれません:

return thing.Property != (thing.Property = "Value") ||
    thing.AnotherProperty != (thing.AnotherProperty = 42) ||
    thing.OneMore != (thing.OneMore = "They get it");

ただし、最初のtrueが見つかると、orステートメントの評価が実際に停止します。この場合、最初の異なる値を割り当てると、後続の値の割り当てを停止します。

これをいじるにはhttps://dotnetfiddle.net/e05Rh8を参照してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.