39

据我了解,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;
    }
}

我不能在 C# 的 foreach 中执行上述操作,因为迭代器 i 是不可变的。我认为(如果我错了,请纠正我),这是特定于语言中 foreach 的设计。

我对为什么 foreach 迭代器是不可变的很感兴趣。

4

6 回答 6

26

让我们从一个愚蠢但说明性的例子开始:

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);
}

如果要编译,则Location在您的示例中,星号会引用 in 中的值Map,但随后您将其更改为引用新的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);
}

那么,为什么微软禁止你重新分配用于迭代的指针呢?澄清一件事——你不希望人们认为他们已经改变了你在循环中的位置。另一个易于实现:变量可能隐藏一些内部逻辑,指示正在进行的循环状态。

但更重要的是,您没有理由分配给它。它表示循环序列的当前元素。如果您遵循 Coding Horror ,则为其分配值会违反“单一责任原则”或Curly 定律。一个变量应该只意味着一件事。

于 2009-04-23T03:22:05.883 回答
14

如果变量是可变的,那可能会产生错误的印象。例如:

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

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

有些人可能认为这会改变数组内容。它达到了一点,但这可能是一个原因。今晚我会在我的注释规范中查找它......

于 2009-04-22T09:55:04.357 回答
11

我认为这是人为的限制,没有必要这样做。为了证明这一点,请考虑此代码,并为您的问题提供可能的解决方案。问题是分配,但对象的内部变化不会出现问题:

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;
        }
    }
}

我不知道这种现象,因为我总是按照我描述的方式修改对象。

于 2009-04-22T11:35:54.817 回答
4

修改集合时,修改可能会产生不可预知的副作用。枚举器无法知道如何正确处理这些副作用,因此他们使集合不可变。

另请参阅此问题: What is the best way to modify a list in a foreach

于 2009-04-22T11:00:53.730 回答
1

foreach 语句适用于 Enumerable。Enumerable 的不同之处在于它不了解整个集合,它几乎是盲目的。

这意味着它不知道接下来会发生什么,或者实际上在下一个迭代周期之前是否有任何事情。这就是为什么你经常看到 .ToList() 以便人们可以获取 Enumerable 的计数。

出于某种我不确定的原因,这意味着您需要确保您的集合在您尝试移动枚举器时不会发生变化。

于 2009-04-22T09:58:51.180 回答
1

使用 IEnumerable 对象的条件是,当您使用 Enumerable 访问它时,基础集合不得更改。您可以假定 Enumerable 对象是原始集合的快照。因此,如果您在枚举时尝试更改集合,它将引发异常。然而,枚举中获取的对象根本不是不可变的。

由于 foreach 循环中使用的变量是循环块的本地变量,因此该变量在块外不可用。

于 2009-04-22T11:14:04.090 回答