Как мне обойти эту проблему с внешней переменной лямбда-выражения? - PullRequest
4 голосов
/ 27 апреля 2010

Я играю с PropertyDescriptor и ICustomTypeDescriptor ( все еще ), пытаясь связать WPF DataGrid с объектом, для которого данные хранятся в словаре.

Поскольку, если вы передадите WPF DataGrid список объектов Dictionary, он автоматически сгенерирует столбцы на основе открытых свойств словаря (Comparer, Count, Keys and Values) моего словаря подклассов Person и реализует ICustomTypeDescriptor.

ICustomTypeDescriptor определяет метод GetProperties, который возвращает PropertyDescriptorCollection.

PropertyDescriptor является абстрактным, поэтому вы должны создать его подкласс, я подумал, что у меня будет конструктор, который принимает Func и параметры Action, которые делегируют получение и установку значений в словаре.

Затем я создаю PersonPropertyDescriptor для каждого ключа в словаре следующим образом:

            foreach (string s in this.Keys)
            {
                var descriptor = new PersonPropertyDescriptor(
                        s,
                        new Func<object>(() => { return this[s]; }),
                        new Action<object>(o => { this[s] = o; }));
                propList.Add(descriptor);
            }

Проблема в том, что каждое свойство получает свои собственные Func и Action, но все они совместно используют внешнюю переменную s , поэтому, хотя DataGrid автоматически генерирует столбцы для "ID", "FirstName", "LastName", "Age "," Gender "они все получают и устанавливают против" Gender ", который является окончательным значением покоя s в цикле foreach.

Как я могу убедиться, что каждый делегат использует нужный словарный ключ, то есть значение s во время создания функции / действия?

Обязан.


Вот и остальная часть моей идеи, я просто экспериментирую здесь, это не «настоящие» классы ...

// DataGrid binds to a People instance
public class People : List<Person>
{
    public People()
    {
        this.Add(new Person());
    }
}

public class Person : Dictionary<string, object>, ICustomTypeDescriptor
{
    private static PropertyDescriptorCollection descriptors;

    public Person()
    {
        this["ID"] = "201203";
        this["FirstName"] = "Bud";
        this["LastName"] = "Tree";
        this["Age"] = 99;
        this["Gender"] = "M";        
    }        

    //... other ICustomTypeDescriptor members...

    public PropertyDescriptorCollection GetProperties()
    {
        if (descriptors == null)
        {
            var propList = new List<PropertyDescriptor>();

            foreach (string s in this.Keys)
            {
                var descriptor = new PersonPropertyDescriptor(
                        s,
                        new Func<object>(() => { return this[s]; }),
                        new Action<object>(o => { this[s] = o; }));
                propList.Add(descriptor);
            }

            descriptors = new PropertyDescriptorCollection(propList.ToArray());
        }

        return descriptors;
    }

    //... other other ICustomTypeDescriptor members...

}

public class PersonPropertyDescriptor : PropertyDescriptor
{
    private Func<object> getFunc;
    private Action<object> setAction;

    public PersonPropertyDescriptor(string name, Func<object> getFunc, Action<object> setAction)
        : base(name, null)
    {
        this.getFunc = getFunc;
        this.setAction = setAction;
    }

    // other ... PropertyDescriptor members...

    public override object GetValue(object component)
    {
        return getFunc();
    }

    public override void SetValue(object component, object value)
    {
        setAction(value);
    }
}

Ответы [ 4 ]

7 голосов
/ 27 апреля 2010

Просто:

        foreach (string s in this.Keys)
        {
            string copy = s;
            var descriptor = new PersonPropertyDescriptor(
                    copy,
                    new Func<object>(() => { return this[copy]; }),
                    new Action<object>(o => { this[copy] = o; }));
            propList.Add(descriptor);
        }

Для перехваченных переменных важно знать, где объявлено . Таким образом, объявляя захваченную переменную внутри цикла, вы получаете другой экземпляр класса capture для каждой итерации (переменная цикла, s, технически объявляется вне цикла).

3 голосов
/ 27 апреля 2010

Решение Марка, конечно, правильное, но я решил расширить ПОЧЕМУ ниже. Как большинство из нас знает, если вы объявляете переменную в операторе for или foreach, она живет только столько, сколько находится внутри, что делает ее кажущейся подобной переменной, как переменная объявляется в блоке операторов такого оператора, но это не так.

Чтобы понять это лучше, возьмите следующий цикл for. Затем я переименую «эквивалентный» цикл в форме while.

for(int i = 0; i < list.Length; i++)
{
    string val;
    list[i] = list[i]++;
    val = list[i].ToString();
    Console.WriteLine(val);
}

Это работает в форме while, как показано ниже: (это не совсем то же самое, потому что continue будет действовать по-другому, но для правил области видимости это то же самое)

{
    int i = 0;
    while(i < list.Length)
    {
        {
            string val;
            list[i] = list[i]++;
            val = list[i].ToString();
            Console.WriteLine(val);
        }
        i++;
    }
}

Когда "взорван" таким образом, область видимости переменных становится более ясной, и вы можете понять, почему она всегда захватывает одно и то же значение "s" в вашей программе, и почему решение Марка показывает, где разместить вашу переменную так, чтобы каждый раз захватывается уникальный.

2 голосов
/ 27 апреля 2010

Дополнительные мысли по этому вопросу см.

.

http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/

2 голосов
/ 27 апреля 2010

создайте локальную копию s внутри цикла for и используйте ее.

for(string s in this.Keys) {
string key = s;
//...
}
...