【问题标题】:Why does this fail when I don't use the ToList method in a for loop?当我不在 for 循环中使用 ToList 方法时,为什么会失败?
【发布时间】:2018-05-15 03:57:09
【问题描述】:

我想知道为什么要换行

"sub = sub.SelectMany(x => x.Next(i)).ToList();" 

"sub = sub.SelectMany(x => x.Next(i));"  

我得到了错误

当我向 SolveNQueens 方法提供输入 4 时,第 48 行:System.IndexOutOfRangeException: Index was outside the bounds of the array”。

我认为这可能与惰性评估有关。

下面列出了完整的代码示例,是一个有效的解决方案 到 n 个皇后问题。

  public class Solution {
        public IList<IList<string>> SolveNQueens(int n) 
        {
            IEnumerable<PartialQueens> sub = new List<PartialQueens>(){
                new PartialQueens(n)};

            for(int i=0;i<n;i++)
            {
                sub = sub.SelectMany(x => x.Next(i)).ToList(); 
            }

           return sub.Select(x => x.ToPosition()).ToList();
        }
    }

   public class PartialQueens
    {
    public byte FREE   = 0;
    public byte BLOCKED = 1;
    public byte QUEEN    = 2; 

    public byte[,] fill;
    int n;

    public PartialQueens(int n)
    {
        this.n = n;
        fill = new byte[n,n];
    }

    public PartialQueens(byte[,] fill, int n)
    {
        this.fill = fill;
        this.n    = n;
    }

    public PartialQueens Fill(int row, int column)
    {
        byte[,] newFill = fill.Clone() as byte[,];

        newFill[row,column] = QUEEN;

        Action<int,int> f = (x,y) => 
        {
            if(y >= 0 && y < n)
                newFill[x,y] = BLOCKED;
        };

        for(int i=1;i<n-row;i++)
        {
            f(row+i,column+i);
            f(row+i,column-i);
            f(row+i,column);
        }

        return new PartialQueens(newFill,n);
    }

    public IEnumerable<PartialQueens> Next(int row)
    { 
        for(int j=0;j<n;j++)
        {            
            if(fill[row,j] == FREE)
                yield return Fill(row,j);
        }
    }  

    public IList<string> ToPosition()
    {
        return Enumerable.Range(0,n).Select(i => ConvertRow(i)).ToList();
    }

    public string ConvertRow(int i)
    {
        StringBuilder builder = new StringBuilder();

        for(int j=0;j<n;j++)
        {
            if(fill[i,j] == QUEEN)
                builder.Append("Q");
            else
                builder.Append(".");
        }

        return builder.ToString();
    }
}

【问题讨论】:

    标签: c# functional-programming ienumerable


    【解决方案1】:

    失败的原因是for loop 中使用的迭代器变量在captured by a closure 时被评估的方式。当您删除循环内的ToList() 时,sub IEnumerable 仅在sub 在返回语句return sub.Select(x =&gt; x.ToPosition()).ToList(); 中实现时才被评估。此时,for 循环变量i 的值将是 n(例如标准棋盘上的 8),它在数组边界之外。

    但是,当您立即实现 List 时,不会遇到副作用,因为在下一次迭代之前使用了 i 的值(ToList 实现)。

    作品:

    for (int i = 0; i < n; i++)
    {
        // Materialized here so `i` evaluated immediately
        sub = sub.SelectMany(x => x.Next(i)).ToList(); 
    }
    

    破碎:

    for (int i = 0; i < n; i++)
    {
        sub = sub.SelectMany(x => x.Next(i));
    }
    return sub.Select(x => x.ToPosition()).ToList(); // `i` evaluated here
    

    对于fixfor循环变量求值问题,可以显式捕获迭代器变量的当前值:

    for (int i = 0; i < n; i++)
    {
        var loop = i;
        sub = sub.SelectMany(x => x.Next(loop)); // No To List - lazy evaluation
    }
    

    Re:避免 FP 范式代码中的 for 循环

    OP 的 SolveNQueens 方法使用循环逐渐改变 sub,而不是递归,但 for 也可以替换为 foreach 和 range:

    foreach(var i in Enumerable.Range(0, n))
    {
        sub = sub.SelectMany(x => x.Next(i));
    }
    

    然后哪个 Resharper 提供重写为左折叠:

    sub = Enumerable.Range(0, n)
        .Aggregate(sub, (current, i) => current.SelectMany(x => x.Next(i)));
    

    无论哪种方式,都可以避免 for 循环中迭代器变量的惰性求值缺陷。

    【讨论】:

    • 更多阅读此issue here
    • 非常感谢!我很惊讶在 c# 中以这种方式完成闭包。
    • 许多人认为这是一个缺陷,MS 进行了重大更改以修复 foreach 循环中的类似问题。问题仍然存在于 for 循环中,尽管在您的情况下,循环的 FP 范式将更好地表示为 Range。我会编辑。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-20
    • 1970-01-01
    相关资源
    最近更新 更多