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

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

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", ИЛИ
  • выдать ошибку времени компиляции, если закрытие поля члена не разрешено в этом сценарии

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


person Richard Berg    schedule 15.03.2010    source источник
comment
У меня он не компилируется... Для нестатического поля, метода или свойства "ConsoleApplication2 .Square._operand" требуется ссылка на объект. Это ваш точный код?   -  person Thomas Levesque    schedule 16.03.2010
comment
Да, это копия/вставка, и она компилируется для меня.   -  person Richard Berg    schedule 16.03.2010
comment
Обратите внимание, что я на VS2008 - как заметил Аарон, команда компилятора 2010 года могла классифицировать это как ошибку (т.е. согласиться со мной :))   -  person Richard Berg    schedule 16.03.2010
comment
Это ошибка в компиляторе, она должна была вызвать ошибку времени компиляции. Эрик Липперт уже знает об этом из другой темы.   -  person Hans Passant    schedule 16.03.2010
comment
Действительно, он компилируется с VS2008, а не с VS2010.   -  person Thomas Levesque    schedule 16.03.2010


Ответы (4)


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

Проблема в том, что 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) должен знать, чтобы смотреть для этого внутри замыкания, чтобы люди не делали этого. Это не так, поэтому вы в конечном итоге с таким поведением.

person Aaronaught    schedule 16.03.2010
comment
@Thomas Levesque: Да, я это сделал, и он скомпилировался, и я получил ту же ошибку времени выполнения. Любопытно, что вы получили ошибку компиляции; Я на VS 2008, а ты на VS 2010? Может быть, они классифицировали это как ошибку и обновили компилятор, чтобы обнаружить это? - person Aaronaught; 16.03.2010
comment
+1 Хорошее объяснение. Я подозревал это, но не мог быть уверен, что отсутствие указателя /this/ в моем окне просмотра не было просто причудой VS (я считаю, что его слишком легко запутать). - person Richard Berg; 16.03.2010
comment
@Thomas Levesque: Это вызывает дополнительные вопросы, поскольку, как я сейчас отмечаю в своем редактировании, об этом было сообщено в Microsoft, и они закрыли проблему как «Не исправить», а затем они ее исправили! Вздох - person Aaronaught; 16.03.2010
comment
@Aaronaught: отличное расследование, я хотел бы проголосовать больше одного раза;) - person Thomas Levesque; 16.03.2010
comment
@Daniel Brückner: Ха-ха, ну, я полагаю, если код находится внутри замыкания, предоставляемого в качестве аргумента конструктора из производного класса, тогда да. :P Если this это null в любое другое время, то у вас есть о чем посерьезнее беспокоиться! - person Aaronaught; 16.03.2010

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

person Eric Lippert    schedule 16.03.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;
    }
}
person Artem Govorov    schedule 16.03.2010
comment
интересный способ отложить разрешение. Хотя в 2010 не работает. - person Jimmy; 16.03.2010
comment
Умная. +1 за самое быстрое исправление (рефакторинг не требуется). - person Richard Berg; 16.03.2010

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

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

person David Morton    schedule 16.03.2010
comment
Достаточно сказать, что это побеждает цель. В моем реальном коде есть несколько очень сложных MathOps. Некоторые шаги являются общими для всех MathOps, поэтому я помещаю их в базовый класс. В одном конкретном случае первая часть расчета неизменна — я хотел оптимизировать ее, кэшируя промежуточный результат в поле-члене, а затем позволяя остальной части расчета (которая зависит от параметров для расчета) выполняться как обычно. . - person Richard Berg; 16.03.2010
comment
@Richard Berg: Возможно, вы могли бы обойти проблему, используя вместо этого метод защищенного инициализатора? Я уверен, что вы уже думали об этом, но не помешает упомянуть... - person Aaronaught; 16.03.2010
comment
Это может сработать. /// FWIW, реальный сценарий — это большое дерево наследования с корнем в IComparable‹T›. Многие абстрактные классы строятся друг на друге таким образом, что я чувствую, что они высоко оптимизированы [при условии правильного встраивания JIT], но строго подчиняются DRY. К тому времени, когда я добрался до сегодняшнего выпуска, я уже не занимался этим фреймворком, реализовывал пользовательский компаратор для некоторых сложных типов в конкретном приложении, глубоко погрузившись в Expression‹› mojo. Думаю, меня удивило, что первопричина была настолько фундаментальной для дизайна, который к тому времени отлично работал во многих других местах. - person Richard Berg; 16.03.2010
comment
Я думаю, что вы хотите сделать, это вызвать частную статическую функцию в вашем производном классе, чтобы вычислить результат оптимизации и вернуть его; вот так: public Square(int operand) : base(ComputeSquare(operand)) {} вместе с private static int ComputeSquare(int operand) { return operand * operand; } - person Gabe; 16.03.2010
comment
В итоге я переместил назначение из конструкторов базового класса в некоторые защищенные методы Init(). Это также потребовало создания защищенного конструктора без аргументов. Во многих производных классах, не нуждающихся в кэшировании, я оставил все как есть (вся работа выполняется в инициализаторе конструктора), но убедился, что все такие классы запечатаны. До сих пор очень доволен результатами. - person Richard Berg; 16.03.2010