C # - замыкания над полями классов внутри инициализатора? - PullRequest
12 голосов
/ 16 марта 2010

Рассмотрим следующий код:

using System;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            var square = new Square(4);
            Console.WriteLine(square.Calculate());
        }
    }

    class MathOp
    {        
        protected MathOp(Func<int> calc) { _calc = calc; }
        public int Calculate() { return _calc(); }
        private Func<int> _calc;
    }

    class Square : MathOp
    {
        public Square(int operand)
            : base(() => _operand * _operand)  // runtime exception
        {
            _operand = operand;
        }

        private int _operand;
    }
}

(не обращайте внимания на дизайн класса; я на самом деле не пишу калькулятор! Этот код просто представляет минимальное повторение для гораздо более крупной проблемы, которая потребовалась некоторое время для сужения)

Я бы ожидал, что либо:

  • печать "16", ИЛИ
  • выдает ошибку времени компиляции, если закрытие по полю члена не разрешено в этом сценарии

Вместо этого я получаю бессмысленное исключение в указанной строке. В 3.0 CLR это NullReferenceException ; в Silverlight CLR это печально известная операция может дестабилизировать время выполнения.

Ответы [ 4 ]

14 голосов
/ 16 марта 2010

Это была ошибка компилятора, которая была исправлена. Во-первых, код никогда не должен был быть законным, и если бы мы собирались это разрешить, мы должны были бы по крайней мере сгенерировать действительный код. Виноват. Извините за неудобства.

11 голосов
/ 16 марта 2010

Это не приведет к ошибке во время компиляции, потому что является допустимым закрытием.

Проблема в том, что this еще не инициализирован во время создания закрытия. Ваш конструктор еще не запущен, когда этот аргумент указан. Таким образом, результирующее NullReferenceException на самом деле вполне логично. Это this это null!

Я докажу это тебе. Давайте перепишем код следующим образом:

class Program
{
    static void Main(string[] args)
    {
        var test = new DerivedTest();
        object o = test.Func();
        Console.WriteLine(o == null);
        Console.ReadLine();
    }
}

class BaseTest
{
    public BaseTest(Func<object> func)
    {
        this.Func = func;
    }

    public Func<object> Func { get; private set; }
}

class DerivedTest : BaseTest
{
    public DerivedTest() : base(() => this)
    {
    }
}

Угадайте, что это печатает? Да, это true, замыкание возвращает null, потому что this не инициализируется при выполнении.

Редактировать

Мне было любопытно высказывание Томаса о том, что, возможно, они изменили поведение в следующем выпуске VS. На самом деле я обнаружил проблему Microsoft Connect об этой самой вещи. Он был закрыт как "не починить". Одд.

Как Microsoft говорит в своем ответе, обычно недопустимо использовать ссылку this из списка аргументов вызова базового конструктора; ссылка просто не существует в этот момент времени, и вы получите ошибку во время компиляции, если попытаетесь использовать ее «голым». Таким образом, возможно, что должен вызвать ошибку компиляции для случая закрытия, но ссылка this скрыта от компилятора, который (по крайней мере в VS 2008) должен был бы знать look за это внутри затвора, чтобы люди этого не делали. Это не так, вот почему вы в конечном итоге с таким поведением.

2 голосов
/ 16 марта 2010

Как насчет этого:

using System;
using System.Linq.Expressions;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            var square = new Square(4);
            Console.WriteLine(square.Calculate());
        }
    }

    class MathOp
    {
        protected MathOp(Expression<Func<int>> calc) { _calc = calc.Compile(); }
        public int Calculate() { return _calc(); }
        private Func<int> _calc;
    }

    class Square : MathOp
    {
        public Square(int operand)
            : base(() => _operand * _operand)
        {
            _operand = operand;
        }

        private int _operand;
    }
}
0 голосов
/ 16 марта 2010

Вы пробовали использовать () => operand * operand вместо этого? Проблема в том, что нет уверенности, что _operand будет установлен к тому времени, когда вы вызываете базу. Да, он пытается создать замыкание в вашем методе, и здесь нет никакой гарантии порядка вещей.

Поскольку вы вообще не устанавливаете _operand, я бы рекомендовал просто использовать () => operand * operand.

...