С# - замыкает поля класса внутри инициализатора?

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

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 это печально известная Операция может дестабилизировать время выполнения.

Ответ 1

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

Проблема заключается в том, что 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)
    {
    }
}

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

Edit

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

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

Ответ 2

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

Ответ 3

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

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;
    }
}

Ответ 4

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

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