【问题标题】:Lambda compile behavior with local variablesLambda 使用局部变量编译行为
【发布时间】:2012-03-26 14:37:14
【问题描述】:

我很好奇编译器如何处理循环中声明的 lambda。在一些情况下,我已将 lambda 声明为内联,以便可以在 lambda 中使用调用方法的本地变量。我在下面包含了两个示例。
编译器会在循环中为 lambda 创建 N 个委托吗?在这种情况下,编译器可以进行哪些(以及哪些)优化?此外,MSDN mentions to use lambdas over anonymous functions,但没有深入说明原因。所以为什么?

示例 1:

public static class MethodPlayground1
{
    public static void TestMethod()
    {
        for (int i = 1; i <= 12; i++)
        {
            int methodLocal1 = i;
            string methodLocal2 = i.ToString();
            KeyValuePair<int, string> methodLocal3 = new KeyValuePair<int, string>(i, i.ToString());
            // Case A
            ThreadPool.QueueUserWorkItem((state) =>
            {
                int threadLocal1 = methodLocal1 + 1;
                string threadLocal2 = methodLocal2 + " o'clock";
                int threadLocal3 = methodLocal3.Key + 2;
                string threadLocal4 = methodLocal3.Value + " oranges";
            });
            // Case B
            ThreadPool.QueueUserWorkItem(delegate(object state)
            {
                int threadLocal1 = methodLocal1 + 1;
                string threadLocal2 = methodLocal2 + " o'clock";
                int threadLocal3 = methodLocal3.Key + 2;
                string threadLocal4 = methodLocal3.Value + " oranges";
            });
            // Case C
            ThreadPool.QueueUserWorkItem((state) =>
            {
                int threadLocal1 = methodLocal1 + 1;
                string threadLocal2 = methodLocal2 + " o'clock";
                int threadLocal3 = methodLocal3.Key + 2;
                string threadLocal4 = methodLocal3.Value + " oranges";
            });
            // Case D
            ThreadPool.QueueUserWorkItem(delegate(object state)
            {
                int threadLocal1 = methodLocal1 + 1;
                string threadLocal2 = methodLocal2 + " o'clock";
                int threadLocal3 = methodLocal3.Key + 2;
                string threadLocal4 = methodLocal3.Value + " oranges";
            });
            // Case E
            ThreadPool.QueueUserWorkItem(AsyncMethod, new object[] { methodLocal1, methodLocal2, methodLocal3 });
        }
    }
    private static void AsyncMethod(object state)
    {
        object[] methodArgs = (object[])state;
        int methodArg1 = (int)methodArgs[0];
        string methodArg2 = (string)methodArgs[1];
        KeyValuePair<int, string> methodArg3 = (KeyValuePair<int, string>)methodArgs[2];

        int threadLocal1 = methodArg1 + 1;
        string threadLocal2 = methodArg2 + " o'clock";
        int threadLocal3 = methodArg3.Key + 2;
        string threadLocal4 = methodArg3.Value + " oranges";
    }
}

示例 2:

public static class MethodPlayground2
{
    private static int PriorityNumber;
    public static void TestMethod()
    {
        List<int> testList = new List<int>(new int[] { 1, 2, 3, 4, 5, 6, 7 });
        for (int i = 1; i <= 7; i++)
        {
            // Case A
            testList.Sort((i1, i2) =>
            {
                if ((i1 == i) || (i2 == i))
                {
                    if (i1 == i2) return 0;
                    else if (i1 == i) return -1;
                    else return 1;
                }
                else return i1.CompareTo(i2);
            });
            // Case B
            testList.Sort(delegate(int i1, int i2)
            {
                if ((i1 == i) || (i2 == i))
                {
                    if (i1 == i2) return 0;
                    else if (i1 == i) return -1;
                    else return 1;
                }
                else return i1.CompareTo(i2);
            });
            PriorityNumber = i;
            // Case C
            testList.Sort(IntCompareWithPriority1);
            // Case D
            testList.Sort(IntCompareWithPriority2);
            // Case E
            testList.Sort(IntCompareWithPriority3);
        }
    }
    private static Comparison<int> IntCompareWithPriority1 = (i1, i2) =>
    {
        if ((i1 == PriorityNumber) || (i2 == PriorityNumber))
        {
            if (i1 == i2) return 0;
            else if (i1 == PriorityNumber) return -1;
            else return 1;
        }
        else return i1.CompareTo(i2);
    };
    private static Comparison<int> IntCompareWithPriority2 = delegate(int i1, int i2)
    {
        if ((i1 == PriorityNumber) || (i2 == PriorityNumber))
        {
            if (i1 == i2) return 0;
            else if (i1 == PriorityNumber) return -1;
            else return 1;
        }
        else return i1.CompareTo(i2);
    };
    private static int IntCompareWithPriority3(int i1, int i2)
    {
        if ((i1 == PriorityNumber) || (i2 == PriorityNumber))
        {
            if (i1 == i2) return 0;
            else if (i1 == PriorityNumber) return -1;
            else return 1;
        }
        else return i1.CompareTo(i2);
    }
}

似乎我在示例 1 中完成的并不多。使用 lambda 更容易编写,因为我不必进行任何转换。但是,我想传递给委托的任何方法局部变量都可以通过object state 参数来实现。
在示例 2 中,使用方法局部变量似乎更简洁,而不是设置静态变量,特别是因为静态变量实现似乎不是线程安全的。

我正在寻找关于 lambda 函数的最佳使用以及在上述每个示例中哪些案例得到最佳优化的答案。

【问题讨论】:

    标签: .net c#-4.0 lambda


    【解决方案1】:

    在第一种情况下,与方法相比,使用 lambdas/匿名方法获得的唯一好处是避免在 object 和要使用的类型之间进行转换;如果您严重依赖值类型,那么每次调用该方法(带有object 类型的参数)时都会发生装箱。

    通常,这没什么大不了的,但是如果您处理的回调数量非常多,它就会开始产生影响。

    Lambdas(不是Expression&lt;T&gt;)/匿名方法和指向方法的委托没有什么不同; C# 编译器将 lambda/匿名函数编译为您的代码看不到的类上的方法,然后将委托连接到 that。

    MSDN page you link to 状态(强调我的):

    lambda 表达式取代匿名方法成为编写内联代码

    的首选方式

    因为他们提到“内联代码”,lambdas 提供了最简洁的代码编写方式;由于它是内联的,因此大多数人自然希望尽可能地获得最简短的表示。毕竟,更具可读性的是:

    var query = myEnumerable.Where(x => x > 2);
    

    还是这个?

    var query = myEnumerable.Where(delegate(x) { return x > 2; });
    

    对于完全相同的事情,在第二种情况下比在第一种情况下输入更多。此外,如果接受委托的方法签名曾经从委托更改为 Expression&lt;T&gt;(大概是为了某些好处对 lambda 进行一些分析),那么使用 lambda 调用它的代码仍然 em> 编译,而使用委托或匿名函数的代码将不。

    注意,也只有 一个 方法是为 lambda/匿名委托生成的。它不会根据声明变量的位置创建 multiple 方法。编译器最终做的是创建一个具有更广泛范围的方法。

    使用您的(修改后的)示例:

    public static void TestMethod()
    {
        // Scope of the lambda starts here.
    
        for (int i = 1; i <= 12; i++)
        {
            // Case A
            ThreadPool.QueueUserWorkItem((state) => {
                Console.WriteLine(i);
            });
        }
    
        // And ends here.
     }
    

    编译器创建的方法将关闭 循环,因此您可能会得到 i 的重复值(取决于 ThreadPool 何时接听调用)。

    但是,当您在循环中分配变量 inside 时,编译器足够聪明,知道它只需要 inside 的代码,如下所示:

    public static void TestMethod()
    {
        for (int i = 1; i <= 12; i++)
        {
            // Scope of the lambda starts here.  
    
            // Create copy.
            int copy = i;
    
            // Case A
            ThreadPool.QueueUserWorkItem((state) => {
                Console.WriteLine(copy);
            });
    
            // And ends here.
        }
    }
    

    在上面,对ThreadPool 的每个回调都会打印出一个不同的值。

    需要注意的是in C# 5.0, there is a breaking change to this behavior for the foreach statement, but not the for statement。

    许多开发人员不了解闭包对循环的影响,这是更改的主要原因。

    【讨论】:

    • 感谢您提供的出色且非常完整的答案!另外,最后一个链接很好读。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-01
    • 2020-10-17
    • 2020-06-14
    • 1970-01-01
    • 1970-01-01
    • 2017-08-18
    相关资源
    最近更新 更多