Помешает подъём локальных переменных анонимными функциями

2

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

using System;
using System.Collections.Generic;
using System.Linq;

namespace AnonymousFuncTest
{
    class Program
    {
        static void Main(string[] args)
        {
            foreach (var f in GetFuncs())
            {
                Console.WriteLine(f());
            }
            Console.ReadLine();
        }

        static IEnumerable<Func<int>> GetFuncs()
        {
            List<Func<int>> list = new List<Func<int>>();
            foreach(var i in Enumerable.Range(1, 20))
            {
                list.Add(delegate() { return i; });
            }

            return list;
        }
    }
}

Я знаю, что смена GetFuncs будет работать:

    static IEnumerable<Func<int>> GetFuncs()
    {
        foreach(var i in Enumerable.Range(1, 20))
        {
            yield return () => i;
        }
    }

Но скажу, что я делаю что-то вроде следующего:

            foreach (var arg in someArgList)
            {
                var item = new ToolStripMenuItem(arg.ToString());
                ritem.Click += delegate(object sender, EventArgs e)
                {
                    new Form(arg).Show();
                };
                mainMenu.DropDownItems.Add(ritem);
            }                

Это, конечно, не имеет предполагаемого эффекта. Я знаю, почему это не работает, просто нужны предложения о том, как его исправить, чтобы он это сделал.

  • 0
    Эта проблема, заключающаяся в том, что замыкание захватывает одну итерационную переменную foreach, а не захватывает каждую переменную каждый раз в цикле, является наиболее распространенным номером «этот код работает не так, как я ожидаю», сообщая об ошибке, которую мы получить. Мы рассматриваем возможность изменения в будущей версии языка и переместим переменную итерации в логический цикл. Если кто-нибудь знает о реальном коде, который может сломаться из-за такого изменения, пожалуйста, отправьте его мне по электронной почте. В моем блоге есть ссылка "свяжитесь со мной". Спасибо!
Теги:

5 ответов

9
Лучший ответ

Вы должны изменить его следующим образом:

    static IEnumerable<Func<int>> GetFuncs()
    {
        List<Func<int>> list = new List<Func<int>>();
        foreach (var i in Enumerable.Range(1, 20))
        {
            int i_local = i;
            list.Add(() => i_local);
        }

        return list;
    }

ИЗМЕНИТЬ

Благодаря Джону Скиту, прочитайте его ответ.

9

Чтобы уточнить ответ kek444, проблема заключается не в том, что локальные переменные захватываются - это то, что одна и та же локальная переменная захватывается всеми вашими делегатами.

Используя копию переменной внутри цикла, на каждой итерации цикла создается "экземпляр" новой переменной, поэтому каждый делегат захватывает другую переменную. Подробнее см. мою статью о закрытии.


Альтернативный подход:

В этой конкретной ситуации на самом деле есть хорошая альтернатива, использующая LINQ:

static IEnumerable<Func<int>> GetFuncs()
{
    return Enumerable.Range(1, 20)
                     .Select(x => (Func<int>)(() => x))
                     .ToList();
}

Если вам нужна ленивая оценка, вы можете просто отказаться от вызова ToList().

  • 0
    Спасибо за разработку! Я подумала, что подожду, чтобы узнать, возникнет ли вопрос в комментариях. :)
  • 0
    Спасибо мистер Скит, как всегда информативно. Поскольку в конечном счете мне нужна анонимная функция в качестве обработчика событий, я буду использовать локальную переменную route, я не могу использовать альтернативу LINQ.
Показать ещё 2 комментария
1

Вы можете правильно зафиксировать значение переменной цикла, скопировав ее в локальную переменную цикла.

static IEnumerable<Func<Int32>> GetFuncs()
{
    List<Func<Int32>> list = new List<Func<Int32>>();

    foreach(Int32 i in Enumerable.Range(1, 20))
    {
        Int32 local_i = i;
        list.Add(delegate() { return local_i; });
    }

    return list;
}
0

Альтернативный способ выразить свой последний пример:

foreach (var item in someArgList
              .Select( a => 
                      var i = new ToolStripMenuItem(a.ToString()); 
                      i.Click+= (sender, e) => new Form(a).Show();
                      return i;) 
        )
{
    mainMenu.DropDownItems.Add(item);
}

Исправление для плохого закрытия/захвата в цикле foreach обычно является вызовом .Select().

0

Это работает:

List<Func<int>> list = new List<Func<int>>();        
Enumerable.Range(1, 20).ToList().ForEach(i => {
    list.Add(delegate() { return i; });            
});

Так делает это:

Action action;
List<Action> objects = new List<Action>();
var items = new string [] { "whatever", "something" };
items.ToList().ForEach((arg) => {   
    action = () => Console.WriteLine(arg.ToString());   
    objects.Add(action);
});
objects[0]();  // prints whatever
objects[1](); // prints something

Ещё вопросы

Сообщество Overcoder
Наверх
Меню