使用foreach迭代IEnumerable会跳过一些元素

awe*_*oon 1 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,这些方法之间的唯一区别是第一个迭代List点,而Kill_DoesNotWork迭代遍历IEnumerable<Point>.但是,最后一种方法有时会跳过元素(Ideone示例).

有完整的代码(我很抱歉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());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

由于这是压缩代码,因此大多数功能并不完全符合它们的要求.如果你想更多地削减它,你可以使用这个要点来共享你的代码(要点单元测试).

我在.NET Framework v4.5.51650中使用MSVS 2013(12.0.30110.00 Update 1)

J. *_*een 7

调用ToList()将在查看该时间点时实现所得到的项目选择.迭代一个IEnumerable将评估为每个项目给出的表达式并逐个产生它们,因此现实可以在迭代之间发生变化.实际上,由于您在迭代之间更改项的属性,因此很可能发生这种情况.

在迭代的主体中,您设置

CellsWithShips[point.X, point.Y] = false;
Run Code Online (Sandbox Code Playgroud)

在选择方法时,您将查询

things.Where(p => CellsWithShips[p.X, p.Y]);
Run Code Online (Sandbox Code Playgroud)

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