c# - 使用 foreach 遍历 IEnumerable 会跳过一些元素

标签 c# ienumerable

我遇到过遍历 enumerable 之间的行为差​​异及以上enumerable.ToList() .

public static void Kill(Point location)
{
    Wound(location);
    foreach(var point in GetShipPointsAndTheirNeighbors(location).ToList())
    {
        CellsWithShips[point.X, point.Y] = false;
    }
}

/// <summary>
/// This version does not work for strange reasons, it just skips a half of points. See TestKill_DoesNotWork_1 test case
/// </summary>
/// <param name="location"></param>
public static void Kill_DoesNotWork(Point location)
{
    Wound(location);
    foreach(var point in GetShipPointsAndTheirNeighbors(location))
    {
        CellsWithShips[point.X, point.Y] = false;
    }
}

如您所见,这些方法之间的唯一区别是第一个方法遍历 List。点数,而 Kill_DoesNotWork遍历 IEnumerable<Point> .但是,最后一种方法有时会跳过元素 ( Ideone example )。

有完整的代码(170行代码我很抱歉,但我不能再压缩了)

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace SampleAi
{
    [DebuggerDisplay("Pont({X}, {Y})")]
    public class Point
    {
        #region Constructors

        public Point(int x, int y)
        {
            X = x;
            Y = y;
        } 

        #endregion // Constructors

        #region Properties

        public int X
        {
            get;
            private set;
        }

        public int Y
        {
            get;
            private set;
        }

        #endregion // Properties

        #region Methods

        public Point Add(Point point)
        {
            return new Point(X + point.X, Y + point.Y);
        }

        #endregion // Methods

        #region Overrides of Object

        /// <summary>
        /// Returns a string that represents the current object.
        /// </summary>
        /// <returns>
        /// A string that represents the current object.
        /// </returns>
        public override string ToString()
        {
            return string.Format("Point({0}, {1})", X, Y);
        }

        #endregion
    }

    public static class Map
    {
        #region Properties

        private static bool[,] CellsWithShips
        {
            get;
            set;
        }

        #endregion // Properties

        #region Methods

        public static IEnumerable<Point> GetAllShipPoints()
        {
            return Enumerable.Range(0, CellsWithShips.GetLength(0))
                             .SelectMany(x => Enumerable.Range(0, CellsWithShips.GetLength(1)).Select(y => new Point(x, y)))
                             .Where(p => CellsWithShips[p.X, p.Y]);
        }

        public static void Init(int width, int height)
        {
            CellsWithShips = new bool[width, height];
        }

        public static void Wound(Point location)
        {
            CellsWithShips[location.X, location.Y] = true;
        }

        public static void Kill(Point location)
        {
            Wound(location);
            foreach(var point in GetShipPointsAndTheirNeighbors(location).ToList())
            {
                CellsWithShips[point.X, point.Y] = false;
            }
        }

        /// <summary>
        /// This version does not work for strange reasons, it just skips a half of points. See TestKill_DoesNotWork_1 test case
        /// </summary>
        /// <param name="location"></param>
        public static void Kill_DoesNotWork(Point location)
        {
            Wound(location);
            foreach(var point in GetShipPointsAndTheirNeighbors(location))
            {
                CellsWithShips[point.X, point.Y] = false;
            }
        }

        private static IEnumerable<Point> GetShipPointsAndTheirNeighbors(Point location)
        {
            return GetShipPoints(location).SelectMany(Near);
        }

        private static IEnumerable<Point> Near(Point location)
        {
            return new[]
            {
                location.Add(new Point(0, -1)),
                location.Add(new Point(0, 0))
            };
        }

        private static IEnumerable<Point> GetShipPoints(Point location)
        {
            var beforePoint = new[]
            {
                location,
                location.Add(new Point(0, -1)),
                location.Add(new Point(0, -2)),
                location.Add(new Point(0, -3))
            };
            return beforePoint.TakeWhile(p => CellsWithShips[p.X, p.Y]);
        }

        #endregion // Methods
    }

    public static class Program
    {
        private static void LoadMap()
        {
            Map.Init(20, 20);

            Map.Wound(new Point(1, 4));
            Map.Wound(new Point(1, 5));
            Map.Wound(new Point(1, 6));
        }

        private static int TestKill()
        {
            LoadMap();
            Map.Kill(new Point(1, 7));
            return Map.GetAllShipPoints().Count();
        }

        private static int TestKillDoesNotWork()
        {
            LoadMap();
            Map.Kill_DoesNotWork(new Point(1, 7));
            return Map.GetAllShipPoints().Count();
        }

        private static void Main()
        {
            Console.WriteLine("Test kill: {0}", TestKill());
            Console.WriteLine("Test kill (does not work): {0}", TestKillDoesNotWork());
        }
    }
}

由于这是压缩代码,所以大部分功能并不完全符合它们应有的功能。如果你想削减更多,你可以使用 this gist用于共享您的代码 ( gist with unit tests )。

我正在使用 MSVS 2013(12.0.30110.00 更新 1)和 .NET Framework v4.5.51650

最佳答案

调用 ToList() 将实现项目的结果选择,因为它在那个时间点查看。迭代 IEnumerable 将评估为每个项目给出的表达式并逐一生成它们,因此现实可能在迭代之间发生变化。事实上,很可能会发生这种情况,因为您在迭代之间更改了项的属性。

在你的迭代体中,你设置

CellsWithShips[point.X, point.Y] = false;

在选择你的方法时,你查询

things.Where(p => CellsWithShips[p.X, p.Y]);

这意味着此类查询的固有动态结果将发生变化,因为您已将其中一些结果设置为 false。但只是因为它会根据需要逐一评估每个项目。这称为延迟执行,最常用于优化大型查询或长时间运行的动态大小操作。

关于c# - 使用 foreach 遍历 IEnumerable 会跳过一些元素,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28322474/

相关文章:

C# 类同时是 IEnumerable 和 IEnumerator。这有什么问题?

c# - 使用 LINQ 获取列表中的项目

c# - 在 Microsoft Build Framework 中使用自定义构建任务时出错

C# 方法原型(prototype)就像 C++ 中的方法原型(prototype)

c# - 如何在Winforms C#中获取按钮背景图片的名称

c# - 将 IEnumerable 分成三部分 : "above", "item", "below"并提高效率

c# - IEnumerable<XElement> 和 foreach

C#线程问题

c# - 与 C# 'using' 混淆

c# - IEnumerable 的可能多重枚举。怎么解决?我需要解决吗?