web-dev-qa-db-ja.com

C#foreachステートメントの反復変数が読み取り専用であるのはなぜですか?

私が理解しているように、C#のforeach反復変数は不変です。

つまり、次のようにイテレータを変更することはできません。

foreach (Position Location in Map)
{
     //We want to fudge the position to hide the exact coordinates
     Location = Location + Random();     //Compiler Error

     Plot(Location);
}

イテレータ変数を直接変更することはできません。代わりに、forループを使用する必要があります。

for (int i = 0; i < Map.Count; i++)
{
     Position Location = Map[i];
     Location = Location + Random();

     Plot(Location);        
     i = Location;
}

C++のバックグラウンドから来ているので、foreachはforループの代わりになると思います。しかし、上記の制限があるため、私は通常、forループの使用にフォールバックします。

私は興味があります、イテレータを不変にする背後にある理論的根拠は何ですか?


編集:

この質問は、コーディングの質問ではなく、好奇心の質問です。コーディングの回答には感謝していますが、回答としてマークすることはできません。

また、上記の例は単純化しすぎています。これが私がやりたいことのC++の例です:

// The game's rules: 
//   - The "Laser Of Death (tm)" moves around the game board from the
//     start area (index 0) until the end area (index BoardSize)
//   - If the Laser hits a teleporter, destroy that teleporter on the
//     board and move the Laser to the square where the teleporter 
//     points to
//   - If the Laser hits a player, deal 15 damage and stop the laser.

for (int i = 0; i < BoardSize; i++)
{
    if (GetItem(Board[i]) == Teleporter)
    {
        TeleportSquare = GetTeleportSquare(Board[i]);
        SetItem(Board[i], FreeSpace);
        i = TeleportSquare;
    }

    if (GetItem(Board[i]) == Player)
    {
        Player.Life -= 15;
        break;
    }
}

イテレータiは不変であるため、C#のforeachでは上記を実行できません。私は(私が間違っている場合は訂正してください)、これは言語でのforeachの設計に固有だと思います。

Foreachイテレータが不変である理由に興味があります。

37
MrValdez

ばかげているが説明的な例から始めましょう:

Object o = 15;
o = "apples";

15という数字をリンゴの列に変えたという印象は決してありません。 oは単なるポインタであることがわかっています。それでは、これをイテレータ形式で実行しましょう。

int[] nums = { 15, 16, 17 };

foreach (Object o in nums) {
     o = "apples";
}

繰り返しますが、これは実際には何も達成しません。または、少なくともそれコンパイルしても何も達成しませんでした。それは確かに私たちの文字列をint配列に挿入しません-それは許可されていません、そして私たちはoがとにかく単なるポインタであることを知っています。

あなたの例を見てみましょう:

foreach (Position Location in Map)
{
     //We want to fudge the position to hide the exact coordinates
     Location = Location + Random();     //Compiler Error

     Plot(Location);
}

これをコンパイルすると、例のLocationMapの値を参照するようになりますが、新しいPosition(暗黙的に作成された)を参照するように変更します。加算演算子)。機能的にはこれと同等です(コンパイルされます):

foreach (Position Location in Map)
{
     //We want to fudge the position to hide the exact coordinates
     Position Location2 = Location + Random();     //No more Error

     Plot(Location2);
}

では、なぜMicrosoftは、反復に使用されるポインターの再割り当てを禁止しているのでしょうか。明確さの1つは、ループ内で自分の位置が変わったと人々が考えて割り当ててほしくないことです。別の実装の容易さ:変数は、進行中のループの状態を示す内部ロジックを非表示にする場合があります。

しかし、もっと重要なことは、あなたがそれに割り当てたいしたい理由がないということです。これは、ループシーケンスの現在の要素を表します。それに値を割り当てると、「単一責任の原則」または カーリーの法則 (コーディングホラーに従う場合)に違反します。変数は1つのことだけを意味する必要があります。

25
tylerl

変数が可変である場合、それは誤った印象を与える可能性があります。例えば:

string[] names = { "Jon", "Holly", "Tom", "Robin", "William" };

foreach (string name in names)
{
    name = name + " Skeet";
}

一部の人々かもしれないそれは配列の内容を変えるだろうと思います。少し近づいていますが、それが理由かもしれません。今夜、注釈付きの仕様で調べます...

14
Jon Skeet

私はその人為的な制限だと思います、本当にそれをする必要はありません。ポイントを証明するために、このコードを検討し、問題の可能な解決策を検討してください。問題は署名することですが、オブジェクトの内部変更は問題を引き起こしません。

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {

            List<MyObject> colection =new List<MyObject>{ new MyObject{ id=1, Name="obj1" }, 
                                                    new MyObject{ id=2, Name="obj2"} };

            foreach (MyObject b in colection)
            {
             // b += 3;     //Doesn't work
                b.Add(3);   //Works
            }
        }

        class MyObject
        {
            public static MyObject operator +(MyObject b1, int b2)
            {
                return new MyObject { id = b1.id + b2, Name = b1.Name };
            }

          public void Add(int b2)
          {
              this.id += b2;
          }
            public string Name;
            public int id;
        }
    }
}

私はいつも私が説明した方法でオブジェクトを変更していたので、私はこの現象について知りませんでした。

11
majkinetor

コレクションを変更すると、変更によって予期しない副作用が発生する可能性があります。列挙子には、これらの副作用に正しく対処する方法を知る方法がないため、コレクションを不変にしました。

この質問も参照してください: foreachのリストを変更する最良の方法は何ですか

3
Rik

ForeachステートメントはEnumerableで機能します。 Enumerableとの違いは、コレクション全体を認識しておらず、ほとんどブラインドであるということです。

これは、次に何が来るのか、または実際に次の反復サイクルまで何かがあるかどうかがわからないことを意味します。そのため、.ToList()がたくさん表示されるので、人々はEnumerableのカウントを取得できます。

なんらかの理由で私にはわかりませんが、これは、列挙子を移動しようとするときにコレクションが変更されていないことを確認する必要があることを意味します。

1
Ian

IEnumerableオブジェクトを操作するための条件は、Enumerableを使用してアクセスしている間、基になるコレクションを変更してはならないということです。 Enumerableオブジェクトは元のコレクションのスナップショットであると推測できます。したがって、列挙中にコレクションを変更しようとすると、例外がスローされます。ただし、列挙でフェッチされたオブジェクトはまったく不変ではありません。

ただし、foreachループで使用される変数はループブロックに対してローカルであるため、この変数はブロック外では使用できません。

1
S M Kamran