Поведение лямбда-компиляции с локальными переменными - PullRequest
1 голос
/ 26 марта 2012

Мне было любопытно, каким образом компилятор обрабатывает лямбды, объявленные в циклах. У меня было несколько случаев, когда я объявлял лямбда-выражения встроенными, чтобы я мог использовать переменные, локальные для вызывающего метода внутри лямбды. Я включил два примера ниже.
Будет ли компилятор создавать N делегатов для лямбды в цикле? Какие (и какие) оптимизации может сделать компилятор в таких случаях? Кроме того, MSDN упоминает об использовании лямбда-выражений над анонимными функциями , но не углубляется в вопрос, почему. Итак, почему?

Пример 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. Писать лямбды немного проще, так как мне не нужно выполнять кастинг. Однако любую локальную переменную метода, которую я хочу передать в делегат, я могу сделать через параметр object state.
В Примере 2 кажется намного более чистым использовать локальные переменные метода, в отличие от установки статической переменной, тем более что реализация статической переменной не кажется поточно-ориентированной.

Я ищу ответ о том, как наилучшим образом использовать лямбда-функции и какие из случаев лучше всего оптимизировать в каждом из приведенных выше примеров.

1 Ответ

1 голос
/ 26 марта 2012

В вашем первом случае единственное преимущество, которое вы получаете, используя методы лямбда-выражения / анонимные по сравнению с методом, состоит в том, что вы избегаете приведения между object и типом, который вы хотите использовать;если вы в большой степени полагаетесь на типы значений, то вы будете подвергаться боксу каждый раз, когда вызывается этот метод (с параметром типа object).

Обычно это не имеет большого значения, но если вашИмея дело с действительно большим количеством обратного вызова, он может начать оказывать влияние.

Лямбды (которые не Expression<T>) / анонимные методы и делегат, который указывает на метод, не являютсяразные;компилятор C # компилирует лямбда / анонимную функцию в метод класса, который ваш код не видит, а затем подключает делегата до , .

страницы MSDN, которую выссылка на состояния (выделено мной):

лямбда-выражения заменяют анонимные методы как предпочтительный способ написания встроенного кода

, поскольку ониупомянуть «встроенный код», лямбда-выражения обеспечивают наиболее сжатый способ написания кода;поскольку он встроенный, большинство, естественно, хотело бы иметь максимально краткое представление.В конце концов, что более читабельно, это:

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

Или это?

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

Во втором случае это больше, чем в первом, для то же самое .Кроме того, если сигнатура метода, которая принимала делегата, когда-либо изменялась с делегата на Expression<T> (предположительно, для некоторого анализа лямбды), то код, вызывающий эту лямбду, все еще компилировать, в то время как код, который использует делегат или анонимную функцию, будет не .

Обратите внимание, что для лямбда / анонимного генерируется только один один метод .делегировать.Он не создает несколько методов в зависимости от того, где объявлена ​​переменная.В итоге компилятор создает метод с более широкой областью действия .

Используя ваш (модифицированный) пример:

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 принимает вызов).

Однако, когда вы присваиваете переменную внутри цикла, компилятор достаточно умен, чтобы знать, что ему нужен только код внутри , например:

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 выведет другое значение.

Следует отметить, что в C # 5.0, это явное изменение в поведении для оператора foreach, но не для оператора for .

Многие разработчики не понимают влияние замыкания на цикл, что является основной причиной изменения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...