“安全”解除lambda表达式中引用的局部变量的规则是什么?

时间:2013-12-07 05:53:02

标签: c# lambda

下面的示例在throw语句处引发IndexOutOfRangeException,因为变量i超出了其限制(例如,当循环覆盖0和1时为2)。我期待这段代码创建lambda块0和1,每个块都将结果存储在相应的数组元素中。我注意到,从设置断点开始,在我调用Task.WaitAll()之前,异步任务实际上并没有开始执行。从C# Programming Guide开始,我理解编译器在循环退出后不顾一切地将i保留在范围内。

所以,我的问题是:

  1. 有人可以建议一种方法来实现我想要创建的效果,每个异步任务应该将其结果存储在数组中的不同插槽中吗? Task.Run()没有提供参数的重载(我用它来传递循环中的i),并且lambda块声明抵制了我无论如何都要声明参数的尝试。

  2. 有人可以提供一个理由说明为什么lambda表达式在其声明块超出范围后能够继续引用局部变量是理想的行为吗? C#语言引用使“本地”声明的含义弯曲,以涵盖匿名函数和lambda块的提升,但这只会打开拾取意外值的大门,正如我的示例所示。

  3. 以下是样本:

    using System;
    using System.Threading.Tasks;
    
    namespace AsyncLifting
    {
        class Program
        {
            static void Main(string[] args)
            {
                const int numTasks = 2;
                double[] taskResult = new double[numTasks];
                Task<int>[] taskHandles = new Task<int>[numTasks];
    
                for (int i = 0; i < numTasks; i++)
                {
                    taskHandles[i] = Task.Run(async () =>
                    {
                        DateTime startTime = DateTime.UtcNow;
                        await Task.Delay(10);
                        try
                        {
                            taskResult[i] = (DateTime.UtcNow - startTime).TotalMilliseconds;
                        }
                        catch (Exception e) { 
                            throw e;  // IndexOutOfRange, i is 2
                        }
                        return i;
                    });
                }
    
                Task.WaitAll(taskHandles);
    
                Console.WriteLine("Task waits:");
                foreach (double tr in taskResult)
                {
                    Console.WriteLine("  {0}ms.", tr);
                }
            }
        }
    }
    

2 个答案:

答案 0 :(得分:4)

委托是关闭变量 - 它不仅仅是捕获当时的值,而是整个变量。这有时可能很有用。

无论如何,为了防止意外行为,只需创建一个新变量并在块内使用它:

for(int i = 0; i < n; i++) {
    int index = i;
    DoSomething(delegate() {
        myArray[index] = /* something */;
    });
}

答案 1 :(得分:1)

icktoofay为您的第一个问题提供了一个很好的答案。

至于为什么这是有用的行为,好吧,如果局部变量一旦超出范围就被删除,你将无法引用lambda局部变量之外的任何东西,因为lambda可能持续很多如果lambda没有故意保留它,那么它的上下文就会超过它。

更具体地说,可能从游戏库中考虑这个功能(我最近做了类似的事情)

public static Func<double,Point> MakeSimpleVelocityTrajectory(double xv, double yv, double x0, double y0)
{
    return (t) => {
        return new Point(x0+xv*t,y0+yv*t);
    }
}

如果lambda没有保留局部变量,那么就没有必要返回lambda,因为它无法做任何事情,因为它引用的变量不再存在。