24

注:以下のコードは本質的に無意味であり、説明のみを目的としていることに注意してください。

割り当ての右側は、その値が左側の変数に割り当てられる前に常に評価される必要があり、およびなどのインクリメント操作は常に評価の直後に実行されるという事実に基づいて++--次のことは期待できません。動作するコード:

string[] newArray1 = new[] {"1", "2", "3", "4"};
string[] newArray2 = new string[4];

int IndTmp = 0;

foreach (string TmpString in newArray1)
{
    newArray2[IndTmp] = newArray1[IndTmp++];
}

むしろ、 をスローするところまで、、などnewArray1[0]に割り当てられることを期待します。代わりに、驚いたことに、例外をスローするバージョンはnewArray2[1]newArray1[1]newArray[2]System.IndexOutOfBoundsException

string[] newArray1 = new[] {"1", "2", "3", "4"};
string[] newArray2 = new string[4];

int IndTmp = 0;

foreach (string TmpString in newArray1)
{
    newArray2[IndTmp++] = newArray1[IndTmp];
}

私の理解では、コンパイラは最初に RHS を評価し、それを LHS に割り当ててからインクリメントするため、これは予期しない動作です。それとも本当に期待されていて、明らかに何かが欠けていますか?

4

6 に答える 6

21

ILDasm は時々あなたの親友になることができます ;-)

両方のメソッドをコンパイルし、結果の IL (アセンブリ言語) を比較しました。

当然のことながら、重要な詳細はループにあります。最初のメソッドは、次のようにコンパイルして実行します。

Code         Description                  Stack
ldloc.1      Load ref to newArray2        newArray2
ldloc.2      Load value of IndTmp         newArray2,0
ldloc.0      Load ref to newArray1        newArray2,0,newArray1
ldloc.2      Load value of IndTmp         newArray2,0,newArray1,0
dup          Duplicate top of stack       newArray2,0,newArray1,0,0
ldc.i4.1     Load 1                       newArray2,0,newArray1,0,0,1
add          Add top 2 values on stack    newArray2,0,newArray1,0,1
stloc.2      Update IndTmp                newArray2,0,newArray1,0     <-- IndTmp is 1
ldelem.ref   Load array element           newArray2,0,"1"
stelem.ref   Store array element          <empty>                     
                                                  <-- newArray2[0] = "1"

これは、newArray1 の各要素に対して繰り返されます。重要な点は、IndTmp がインクリメントされる前に、ソース配列内の要素の位置がスタックにプッシュされていることです。

これを 2 番目の方法と比較します。

Code         Description                  Stack
ldloc.1      Load ref to newArray2        newArray2
ldloc.2      Load value of IndTmp         newArray2,0
dup          Duplicate top of stack       newArray2,0,0
ldc.i4.1     Load 1                       newArray2,0,0,1
add          Add top 2 values on stack    newArray2,0,1
stloc.2      Update IndTmp                newArray2,0     <-- IndTmp is 1
ldloc.0      Load ref to newArray1        newArray2,0,newArray1
ldloc.2      Load value of IndTmp         newArray2,0,newArray1,1
ldelem.ref   Load array element           newArray2,0,"2"
stelem.ref   Store array element          <empty>                     
                                                  <-- newArray2[0] = "2"

ここでは、ソース配列内の要素の位置がスタックにプッシュされる前に IndTmp がインクリメントされるため、動作が異なります (およびその後の例外)。

完全を期すために、それを比較してみましょう

newArray2[IndTmp] = newArray1[++IndTmp];

Code         Description                  Stack
ldloc.1      Load ref to newArray2        newArray2
ldloc.2      Load IndTmp                  newArray2,0
ldloc.0      Load ref to newArray1        newArray2,0,newArray1
ldloc.2      Load IndTmp                  newArray2,0,newArray1,0
ldc.i4.1     Load 1                       newArray2,0,newArray1,0,1
add          Add top 2 values on stack    newArray2,0,newArray1,1
dup          Duplicate top stack entry    newArray2,0,newArray1,1,1
stloc.2      Update IndTmp                newArray2,0,newArray1,1  <-- IndTmp is 1
ldelem.ref   Load array element           newArray2,0,"2"
stelem.ref   Store array element          <empty>                     
                                                  <-- newArray2[0] = "2"

ここでは、IndTmp が更新される前に、インクリメントの結果がスタックにプッシュされています (そして配列インデックスになります)。

要約すると、割り当てのターゲットが最初に評価され、次にソースが評価されるようです。

本当に考えさせられる質問のOPに賛成です!

于 2011-07-02T18:51:56.700 に答える
18

これは、Eric Lippert によると C# 言語で明確に定義されており、簡単に説明できます。

  1. 参照して記憶する必要がある最初の左順序式のものが評価され、副作用が考慮されます
  2. 次に、正しい順序の式が行われます

注: コードの実際の実行はこのようにならない可能性があります。覚えておくべき重要なことは、コンパイラはこれと同等のコードを作成する必要があるということです。

コードの 2 番目の部分は次のようになります:</p>

  1. 左側:
    1. newArray2が評価され、結果が記憶されます (つまり、後で副作用によって変更された場合に備えて、格納したい配列への参照が記憶されます)。
    2. IndTemp評価され、結果が記憶される
    3. IndTemp1増加
  2. 右側:
    1. newArray1評価され、結果が記憶される
    2. IndTempが評価され、結果が記憶されます (ただし、ここでは 1 です)
    3. 配列項目は、ステップ 2.2 のインデックスでステップ 2.1 の配列にインデックスを付けることによって取得されます。
  3. 左側に戻る
    1. 配列項目は、ステップ 1.2 のインデックスでステップ 1.1 の配列にインデックス付けすることによって格納されます。

ご覧のとおり、2 回目のIndTemp評価 (RHS) では、値は既に 1 増加していますが、増加前の値が 0 だったことを記憶しているため、これは LHS には影響しません。

コードの最初の部分では、順序が少し異なります。

  1. 左側:
    1. newArray2評価され、結果が記憶される
    2. IndTemp評価され、結果が記憶される
  2. 右側:
    1. newArray1評価され、結果が記憶される
    2. IndTempが評価され、結果が記憶されます (ただし、ここでは 1 です)
    3. IndTemp1増加
    4. 配列項目は、ステップ 2.2 のインデックスでステップ 2.1 の配列にインデックスを付けることによって取得されます。
  3. 左側に戻る
    1. 配列項目は、ステップ 1.2 のインデックスでステップ 1.1 の配列にインデックス付けすることによって格納されます。

この場合、ステップ 2.3 での変数の増加は現在のループ反復に影響を与えないため、常に index から indexNにコピーしNますが、2 番目のコードでは常に index から indexN+1にコピーしますN

Eric はPrecedence vs order, reduxというタイトルのブログ エントリを読んでください。

これは、基本的に変数をクラスのプロパティに変換し、カスタムの「配列」コレクションを実装したことを示すコードです。これらはすべて、何が起こっているかをコンソールにダンプするだけです。

void Main()
{
    Console.WriteLine("first piece of code:");
    Context c = new Context();
    c.newArray2[c.IndTemp] = c.newArray1[c.IndTemp++];

    Console.WriteLine();

    Console.WriteLine("second piece of code:");
    c = new Context();
    c.newArray2[c.IndTemp++] = c.newArray1[c.IndTemp];
}

class Context
{
    private Collection _newArray1 = new Collection("newArray1");
    private Collection _newArray2 = new Collection("newArray2");
    private int _IndTemp;

    public Collection newArray1
    {
        get
        {
            Console.WriteLine("  reading newArray1");
            return _newArray1;
        }
    }

    public Collection newArray2
    {
        get
        {
            Console.WriteLine("  reading newArray2");
            return _newArray2;
        }
    }

    public int IndTemp
    {
        get
        {
            Console.WriteLine("  reading IndTemp (=" + _IndTemp + ")");
            return _IndTemp;
        }

        set
        {
            Console.WriteLine("  setting IndTemp to " + value);
            _IndTemp = value;
        }
    }
}

class Collection
{
    private string _name;

    public Collection(string name)
    {
        _name = name;
    }

    public int this[int index]
    {
        get
        {
            Console.WriteLine("  reading " + _name + "[" + index + "]");
            return 0;
        }

        set
        {
            Console.WriteLine("  writing " + _name + "[" + index + "]");
        }
    }
}

出力は次のとおりです。

first piece of code:
  reading newArray2
  reading IndTemp (=0)
  reading newArray1
  reading IndTemp (=0)
  setting IndTemp to 1
  reading newArray1[0]
  writing newArray2[0]

second piece of code:
  reading newArray2
  reading IndTemp (=0)
  setting IndTemp to 1
  reading newArray1
  reading IndTemp (=1)
  reading newArray1[1]
  writing newArray2[0]
于 2011-07-02T20:16:25.870 に答える
13
newArray2[IndTmp] = newArray1[IndTmp++];

最初に変数を割り当て、次に変数をインクリメントします。

  1. newArray2[0] = newArray1[0]
  2. インクリメント
  3. newArray2[1] = newArray1[1]
  4. インクリメント

等々。

RHS ++ 演算子はすぐにインクリメントしますが、インクリメントされる前の値を返します。配列のインデックスに使用される値は、RHS ++ 演算子によって返される値であるため、インクリメントされていない値です。

あなたが説明するもの (スローされた例外) は、LHS ++ の結果になります。

newArray2[IndTmp] = newArray1[++IndTmp]; //throws exception
于 2011-07-02T18:08:13.787 に答える
12

エラーがどこにあるかを正確に確認することは有益です。

割り当ての右側は、その値が左側の変数に割り当てられる前に常に評価される必要があります

正しい。明らかに、代入の副作用は、代入される値が計算されるまで発生しません。

++ や -- などのインクリメント操作は、常に評価の直後に実行されます

ほぼ正しい。「評価」が何を意味するのか明確ではありません-何の評価ですか?元の値、インクリメントされた値、または式の値? これについて考える最も簡単な方法は、元の値が計算され、次に増加した値が計算され、次に副作用が発生するということです。最終的な値は、演算子が前置または後置のどちらであったかに応じて、元の値または増加した値のいずれかが選択されることです。しかし、あなたの基本的な前提はかなり良いです: 最終値が決定された直後にインクリメントの副作用が発生し、最終値が生成されます。

つまり、左辺の副作用は右辺の評価の後に生じるという、これら 2 つの正しい前提から誤った結論を下しているように思われます。しかし、これらの 2 つの前提には、この結論を意味するものは何もありません! あなたはその結論をどこからともなく導き出しました。

3 番目の正しい前提を述べた場合は、より明確になります。

左側の変数に関連付けられた格納場所、割り当てが行われる前にわかっている必要があります。

明らかにこれは真実です。割り当てを行う前に、 2 つのことを知っておく必要があります。どの値が割り当てられているか、どのメモリ位置が変更されているかです。これら 2 つのことを同時に理解することはできません。最初にそれらの 1 つを把握する必要があります。左側にあるもの (変数) を最初に C# で把握します。ストレージが配置されている場所を特定することで副作用が発生する場合、その副作用は、変数に割り当てられている値という 2 番目のことを特定する前に生成されます。

つまり、C# では、変数への割り当てにおける評価の順序は次のようになります。

  • 左辺の副作用が発生し、変数が生成されます
  • 右辺の副作用が発生し、が生成されます
  • 値は暗黙のうちに左辺の型に変換されるため、第 3 の副作用が生じる可能性があります。
  • 代入の副作用 -- 正しい型の値を持つ変数の突然変異 -- が発生し、値 -- 左辺に代入されたばかりの値 -- が生成されます。
于 2011-07-05T14:20:45.453 に答える
4

明らかに、rhs が常に lhs の前に評価されるという仮定は間違っています。ここhttp://msdn.microsoft.com/en-us/library/aa691315(v=VS.71).aspxを見ると、インデクサーアクセスの場合、インデクサーアクセス式の引数のように見えます。 lhs は、rhs の前に評価されます。

つまり、最初に rhs の結果を格納する場所が決定され、その後で rhs が評価されます。

于 2011-07-02T18:42:02.687 に答える
3

インデックス 1 でインデックス付けを開始するため、例外がスローされます。最後の代入newArray1で各要素を反復処理しているため、 is が等しいため、つまり、配列の末尾を 1 つ過ぎているため、例外がスローされます。から要素を抽出するために使用される前にインデックス変数をインクリメントします。これは、クラッシュするだけでなく、 の最初の要素を見逃すことを意味します。newArray1IndTmpnewArray1.LengthnewArray1newArray1

于 2011-07-02T18:10:40.887 に答える