使用foreach迭代IEnumerable会跳过一些元素 [英] Iterating over IEnumerable using foreach skips some elements

查看:71
本文介绍了使用foreach迭代IEnumerable会跳过一些元素的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我遇到了遍历enumerable和遍历enumerable.ToList()之间的行为差​​异.

I've faced with difference in behavior between iterating over enumerable and over 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>上迭代.但是,最后一种方法有时会跳过元素( Idone示例).

As you can see, the only difference between these methods is that the first one iterates over List of points, while the Kill_DoesNotWork iterates over IEnumerable<Point>. However, the last method sometimes skips elements (Ideone example).

有完整的代码(对170行代码很抱歉,但是我无法对其进行更多压缩)

There is full code (I am sorry for 170 lines of code, but I cannot compress it more)

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

由于这是压缩代码,因此大多数功能并不完全符合其应有的功能.如果您想进一步削减代码,可以使用此要点来共享您的代码(要点与单元测试).

Since this is compressed code, most of functions does not exactly what they should. If you want to cut it more, you could use this gist for sharing your code (gist with unit tests).

我正在将MSVS 2013(12.0.30110.00更新1)与.NET Framework v4.5.51650一起使用

I am using MSVS 2013 (12.0.30110.00 Update 1) with .NET Framework v4.5.51650

推荐答案

ToList()的调用将具体化结果项的选择,因为它是在那个时间点上的.在IEnumerable上进行迭代将评估为每个项目给出的表达式,并逐个生成它们,因此实际情况可能在迭代之间发生了变化.实际上,很可能发生这种情况,因为您在两次迭代之间更改了项目的属性.

The call to ToList() will materialise the resulting selection of items as it looked at that point in time. Iterating over an IEnumerable will evaluate the expressions given for each item and yield them, one by one, and thus reality can have changed between iterations. In fact, it's probably very likely that this happens since you change the properties of items between iterations.

在迭代主体中,进行设置

In the body of your iteration, you set

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

在选择方法时,您查询

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

这意味着这种查询的内在动态结果将发生变化,因为您已将某些结果设置为false.但这仅仅是因为它根据需要评估每个项目.这称为延迟执行,最常用于优化大型查询或长时间运行的动态大小的操作.

This means that the inherently dynamic result of such a query will change, since you've set some of those results to false. But only because it evaluates every item, one by one, as needed. This is called deferred execution and is most often used to optimize large queries or long-running, dynamically sized operations.

这篇关于使用foreach迭代IEnumerable会跳过一些元素的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆