C# - For循环和Lambda表达式

4
我的问题是为什么在lambda表达式中使用迭代器变量时结果不正确。
我已经按照SWeko的答案在这里进行了操作,但对我没有起作用。 以下是我的伪代码:
List<string> list = new List<string>(3);
// where list.Count=3
for (int i=0;i<list.Count;i++){
    int yy=i;
    AFunctionWithLambda(() => Console.WriteLine (list[yy]));
}

但是控制台仅会输出信息

list[3]

三次,好像“yy”的唯一可能值是“i”的最后一个值。似乎起作用是因为在声明“yy”变量之前,控制台返回了“i = 1”,但实际上并不是这样的,我不知道我错过了什么。

提前感谢!


1
AFunctionWithLambda是什么?请提供一个最小、完整和可验证的示例 - Thomas Levesque
1
你没有展示列表是如何被填充的。在所有yy的情况下,list[yy]的值可能为3。 - John Gardner
1
这种类型的问题非常注重细节。你能否展示一个最小化的可复现的例子?特别是,需要提供 i/yy 的确切代码,并且我们需要知道 lambda 表达式是立即调用、作为委托保留并稍后执行还是异步执行。 - Marc Gravell
1
如果 list.Count=3,那么首先 list[3] 就没有意义。 - Marc Gravell
@MarcGravell:我之前不知道有捕获变量的问题。我猜我本能地避免在循环中进行这种捕获。 - IAbstract
显示剩余7条评论
3个回答

3

我知道你想要做什么。为了达到这个目的,你需要以下内容:

for (int i=0;i<list.Count;i++){
    String s=list[i] ;
    AFunctionWithLambda(() => Console.WriteLine(s));
}

我假设AFunctionWithLambda正在将动作添加到一个动作列表中?然后在此for循环之后执行这些动作?使用您的代码,所有动作都引用了i,而在for循环结束时,i被设置为3。当每个动作运行时,i仍然设置为3。使用上述方法,我们创建了3个单独的字符串,并且每个动作都有对正确字符串的引用。
确保填充字符串列表,否则您将不会得到任何输出。

等你完成那个,使用 foreach(var s in list) { AFunctionWithLambda(() => Console.WriteLine(s)); } 可能会更容易。请注意,我并不反对,只是在澄清。 - Marc Gravell
抱歉,是的,它适用于C# 5编译器。这与作用域无关;这完全是关于创建一个新的字符串常量并在操作中引用正确的常量。 - Darren Gourley
1
实际上,这与作用域有很大关系。抱歉,但我曾与Eric、Mads和Anders就这个话题进行了漫长的讨论(如果我没记错的话是在吃鸡肉时)。这归结于l-value声明的位置。在2.0规范中,l-value在规范中被声明为循环之外 - 即foreach如何扩展为实际代码(while)。在后来的规范中(奇怪的是,在1.2规范的“明确赋值”的附录中,如果我没记错的话),它被声明为循环内部。捕获变量逻辑对作用域非常敏感;内部=每次迭代;外部=全局循环。 - Marc Gravell
1
明确来说,两者的区别在于:foreach(SomeType x in foo) {...} 展开为 { SomeType x; while(iter.MoveNext()) {x = iter.Current; ...},而不是 {while(iter.MoveNext) { SomeType x = iter.Current; ...}}。其中还有一些 using(var iter = sequence.GetEnumerator()) {...} 的内容,但这并不影响两者之间的区别,因此我省略了它 - 而且它还有很多边缘情况。 - Marc Gravell
嘿,我在英国,所以这里也是凌晨2点 :) “常量”的概念只在编译器和IL中真正定义,而不是在运行时。字符串(至少对外部世界来说)是不可变的,但这并不完全相同 - 但很接近。这是一个非常复杂和微妙的领域 - 我只有在开始做大量IL工作(以及因为我已经阅读了几乎每个版本的C#规范 出于好奇心,这使得阅读有趣)时才有必要理解它。 - Marc Gravell
显示剩余11条评论

0

从你的例子中很难判断,因为它看起来已经完美地形成并正确地捕获了。这是我的扩展版本,它也能正常工作:

    public void Main()
    {
        List<string> list = new List<string>(3)
        { "a", "b", "c"};
        for (int i = 0; i < list.Count; i++)
        {
            int yy = i;
            AFunctionWithLambda(() => Console.WriteLine(list[yy]));
        }

        Thread.Sleep(1000);
        Console.WriteLine("all done, probably");
        Console.ReadLine();
    }

    private void AFunctionWithLambda(Action action)
    {  // runs it asynchronously, for giggles
        ThreadPool.QueueUserWorkItem(o => {
            Thread.Sleep(500); // delay, to let the loop finish
            action();
        });
    }

如果你将它改为:
        for (int i = 0; i < list.Count; i++)
        {
            AFunctionWithLambda(() => Console.WriteLine(list[i]));
        }

然后它以另外描述的方式失败了。

我们需要一个更好的例子; p


是的,我只是写了一个非常简短的代码来解释它,但如果我们采用您的代码,我们应该在控制台输出得到:a,b,c,但实际上我只得到了 c 的值,这就是我试图用 list[3] 表达的意思。 - Jotarata
@Jotarata 如果clist[2],那么list[3]将会抛出异常;但是,在观察到委托被调用的时候,没有看到你的列表内容,所以这样推断毫无意义。在那个时候,它完全有可能是所有的c。因此,forforeach之间有区别,foreach方法捕获,而for方法捕获索引 - Marc Gravell
дҪҝз”ЁforeachеҫӘзҺҜиҺ·еҸ–зҙўеј•жҳҜеҸҜиғҪзҡ„еҗ—пјҹпјҲйҷ„жіЁпјҡжҳҜlist[2]иҖҢдёҚжҳҜxdпјү - Jotarata
@Jotarata 你为什么想要这样做?以及:不。 - Marc Gravell

0

我想我有一个解决方案:

我不能在lambda表达式中使用循环变量,但我可以创建一个新的方法来实现。

大概是这样:

{
for (int i=0;i<list.count;i++){
    ToAction(i);
    }
}
void ToAction(int value){
    AFunctionWithLambda(()=>Console.WriteLine(list[value]);
}

它运行良好

感谢每个人提出这个(愚蠢的)问题:D


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接