为什么 C# foreach 语句中的迭代变量是只读的?

发布于 2024-07-18 05:16:23 字数 1609 浏览 5 评论 0原文

据我了解,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 迭代器是不可变的。

As I understand it, C#'s foreach iteration variable is immutable.

Which means I can't modify the iterator like this:

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

     Plot(Location);
}

I can't modify the iterator variable directly and instead, I have to use a for loop

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

     Plot(Location);        
     i = Location;
}

Coming from a C++ background, I see foreach as an alternative to the for loop. But with the above restriction, I usually fallback to using the for loop.

I'm curious, what is the rationale behind making the iterator immutable?


Edit:

This question is more of a curiousity question and not as a coding question. I appreciated the coding answers but I can't mark them as answers.

Also, the example above was over-simplified. Here is a C++ example of what I want to do:

// 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 can't do the above in C#'s foreach because the iterator i is immutable. I think (correct me if I'm wrong), this is specific to the design of foreach in languages.

I'm interested in why the foreach iterator is immutable.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

酒解孤独 2024-07-25 05:16:23

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

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 会引用 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);
}

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

但更重要的是,您没有理由想要分配给它。 它表示循环序列的当前元素。 为其赋值会违反“单一职责原则”或 Curly 定律(如果您这样做)关注《编码恐怖》。 变量应该只表示一件事。

Lets start out with a silly but illustrative example:

Object o = 15;
o = "apples";

At no point do we get the impression that we just turned the number 15 into a string of apples. We know that o is simply a pointer. Now lets do this in iterator form.

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

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

Again, this really accomplishes nothing. Or at least it would accomplish nothing were it to compile. It certainly wouldn't insert our string into the int array -- that's not allowed, and we know that o is just a pointer anyway.

Let's take your example:

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

     Plot(Location);
}

Were this to compile, the Location in your example stars out referring to a value in Map, but then you change it to refer to a new Position (implicitly created by the addition operator). Functionally it's equivalent to this (which DOES compile):

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

So, why does Microsoft prohibit you from re-assigning the pointer used for iteration? Clarity for one thing -- you don't want people assigning to it thinking they've changed your position within the loop. Ease of implementation for another: The variable might hide some internal logic indicating the state of the loop in progress.

But more importantly, there is no reason for you to want to assign to it. It represents the current element of the looping sequence. Assigning a value to it breaks the "Single Responsibility Principle" or Curly's Law if you follow Coding Horror. A variable should mean one thing only.

情域 2024-07-25 05:16:23

如果变量是可变的,则可能会给人错误的印象。 例如:

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

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

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

If the variable were mutable, that might give an incorrect impression. For example:

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

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

Some people might think that would change the array contents. It's reaching a bit, but it might be a reason. I'll look it up in my annotated spec tonight...

三生一梦 2024-07-25 05:16:23

我认为这是人为的限制,实际上没有必要这样做。 为了证明这一点,请考虑此代码以及解决您的问题的可能方法。 问题在于分配,但对象的内部更改不会出现问题:

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

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

I think its artificial limitation, no really need to do it. To prove the point, take this code into considiration, and a possible solution to your problem. The problem is to asign, but internal changes of objects don't present a problem:

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

I didn't know about this phenomena, because I was always modifying the objects the way I described.

快乐很简单 2024-07-25 05:16:23

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

另请参阅这个问题:
什么是最好的修改 foreach 中的列表的方法

When you modify the collection, the modification might have unpredictable side effects. The enumerator has no way of knowing how to deal correctly with these side effects, so they made the collections immutable.

See also this question:
What is the best way to modify a list in a foreach

江湖彼岸 2024-07-25 05:16:23

为什么 C# foreach 语句中的迭代变量是只读的?

不再是了! 自 C# 7.3(2018 年 9 月)我们有ref return,允许Enumerator.Current返回对迭代器值的引用。

这可以防止复制值(或引用),并允许您使用 foreach (ref var v ...) 语法就地覆盖数组元素:

var arr = new[] { "foo", "bar" };

foreach (ref var v in arr.AsSpan())
{
    v = "baz";
    Console.WriteLine(v);
}

Console.WriteLine(string.Join(", ", arr));

此输出:

baz
baz
baz, baz

引用(哈哈!):

Why is The Iteration Variable in a C# foreach statement read-only?

Not anymore it's not! Since C# 7.3 (September 2018) we have ref return, allowing Enumerator.Current to return a reference to an iterator value.

This prevents copying a value (or reference), and allows you to overwrite array elements in-place using the foreach (ref var v ...) syntax:

var arr = new[] { "foo", "bar" };

foreach (ref var v in arr.AsSpan())
{
    v = "baz";
    Console.WriteLine(v);
}

Console.WriteLine(string.Join(", ", arr));

This outputs:

baz
baz
baz, baz

References (hah!):

九八野马 2024-07-25 05:16:23

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

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

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

The foreach statement works with Enumerable. The thing that is different with an Enumerable is that it doesn't know about the collection as a whole, it's almost blind.

This means that it doesn't know what's coming next, or indeed if there is anything until the next iteration cycle. That why there you see .ToList() a lot so people can grab a count of the Enumerable.

For some reason that I'm not sure about, this means that you need to ensure your collection isn't changing as you try to move the enumerator along.

零崎曲识 2024-07-25 05:16:23

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

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

The condition to work with IEnumerable objects is that the underlying collection must not change while you are accessing it with Enumerable. You can presume that the Enumerable object is a snap shot of the original collection. So if you tries to change the collection while enumerating it'll throw an exception. However the fetched objects in Enumeration is not immutable at all.

Since the variable used in foreach loop is local to the loop block this variable is however not available outside the block.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文