Решение проблемы "Виртуальный вызов метода в конструкторе"

Я делаю программное обеспечение в С#. Я использую абстрактный класс Instruction, который имеет эти биты кода:

protected Instruction(InstructionSet instructionSet, ExpressionElement newArgument,
    bool newDoesUseArgument, int newDefaultArgument, int newCostInBytes, bool newDoesUseRealInstruction) {

    //Some stuff

    if (DoesUseRealInstruction) {
        //The warning appears here.
        RealInstruction = GetRealInstruction(instructionSet, Argument);
    }
}

и

public virtual Instruction GetRealInstruction(InstructionSet instructionSet, ExpressionElement argument) {
    throw new NotImplementedException("Real instruction not implemented. Instruction type: " + GetType());
}

Итак, Resharper сообщает мне, что на отмеченной строке я вызываю виртуальный метод в конструкторе и что это плохо. Я понимаю вещь о порядке, в котором вызываются конструкторы. Все переопределения метода GetRealInstruction выглядят следующим образом:

public override Instruction GetRealInstruction(InstructionSet instructionSet, ExpressionElement argument) {
    return new GoInstruction(instructionSet, argument);
}

Таким образом, они не зависят от каких-либо данных в классе; они просто возвращают то, что зависит от производного типа. (поэтому порядок конструктора не влияет на них).

Итак, следует ли игнорировать его? Я бы не; так может ли кто-нибудь показать мне, как я мог избежать этого предупреждения?

Я не могу использовать делегатов аккуратно, потому что метод GetRealInstruction имеет еще одну перегрузку.

Ответы

Ответ 1

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

public abstract class Instruction
{
    protected Instruction(InstructionSet instructionSet, ExpressionElement argument, RealInstructionGetter realInstructionGetter)
    {
        if (realInstructionGetter != null)
        {
            RealInstruction = realInstructionGetter.GetRealInstruction(instructionSet, argument);
        }
    }

    public Instruction RealInstruction { get; set; }

    // Abstracted what used to be the virtual method, into it own class that itself can be inherited from.
    // When doing this I often make them inner/nested classes as they're not usually relevant to any other classes.
    // There nothing stopping you from making this a standalone class of it own though.
    protected abstract class RealInstructionGetter
    {
        public abstract Instruction GetRealInstruction(InstructionSet instructionSet, ExpressionElement argument);
    }
}

// A sample derived Instruction class
public class FooInstruction : Instruction
{
    // Passes a concrete instance of a RealInstructorGetter class
    public FooInstruction(InstructionSet instructionSet, ExpressionElement argument) 
        : base(instructionSet, argument, new FooInstructionGetter())
    {
    }

    // Inherits from the nested base class we created above.
    private class FooInstructionGetter : RealInstructionGetter
    {
        public override Instruction GetRealInstruction(InstructionSet instructionSet, ExpressionElement argument)
        {
            // Returns a specific real instruction
            return new FooRealInstuction(instructionSet, argument);
        }
    }
}

// Another sample derived Instruction classs showing how you effictively "override" the RealInstruction that is passed to the base class.
public class BarInstruction : Instruction
{
    public BarInstruction(InstructionSet instructionSet, ExpressionElement argument)
        : base(instructionSet, argument, new BarInstructionGetter())
    {
    }

    private class BarInstructionGetter : RealInstructionGetter
    {
        public override Instruction GetRealInstruction(InstructionSet instructionSet, ExpressionElement argument)
        {
            // We return a different real instruction this time.
            return new BarRealInstuction(instructionSet, argument);
        }
    }
}

В вашем конкретном примере это немного запутывает, и я начал иссякать из разумных имен, но это связано с тем, что у вас уже есть вложение инструкций в Инструкции, то есть инструкция имеет RealInstruction (или, по крайней мере, необязательно); но, как вы видите, все еще возможно достичь того, чего вы хотите, и избежать любых вызовов виртуальных членов от конструктора.

В случае, если это еще не ясно, я также приведу пример, основанный на том, что я использовал совсем недавно в своем собственном коде. В этом случае у меня есть 2 типа форм, форма заголовка и форма сообщения, каждая из которых наследуется от базовой формы. Все формы имеют поля, но каждый тип формы имеет другой механизм для построения своих полей, поэтому изначально у меня был абстрактный метод, называемый GetOrderedFields, который я вызывал из базового конструктора, и метод был переопределен в каждом классе производной формы. Это дало мне предостережение, которое вы упомянули. Мое решение было таким же, как и выше, и выглядит следующим образом

internal abstract class FormInfo
{
    private readonly TmwFormFieldInfo[] _orderedFields;

    protected FormInfo(OrderedFieldReader fieldReader)
    {
        _orderedFields = fieldReader.GetOrderedFields(formType);
    }

    protected abstract class OrderedFieldReader
    {
        public abstract TmwFormFieldInfo[] GetOrderedFields(Type formType);
    }
}

internal sealed class HeaderFormInfo : FormInfo
{
    public HeaderFormInfo()
        : base(new OrderedHeaderFieldReader())
    {
    }

    private sealed class OrderedHeaderFieldReader : OrderedFieldReader
    {
        public override TmwFormFieldInfo[] GetOrderedFields(Type formType)
        {
            // Return the header fields
        }
    }
}

internal class MessageFormInfo : FormInfo
{
    public MessageFormInfo()
        : base(new OrderedMessageFieldReader())
    {
    }

    private sealed class OrderedMessageFieldReader : OrderedFieldReader
    {
        public override TmwFormFieldInfo[] GetOrderedFields(Type formType)
        {
            // Return the message fields
        }
    }
}

Ответ 2

Когда вы создаете экземпляр вашего производного класса, ваш стек вызовов будет выглядеть следующим образом:

GetRealInstruction()
BaseContructor()
DerivedConstructor()

GetRealInstruction переопределяется в производном классе, конструктор которого еще не закончил выполнение.

Я не знаю, как выглядит ваш другой код, но сначала вы должны проверить, действительно ли вам нужна переменная-член. У вас есть метод, который возвращает нужный вам объект. Если вам это действительно нужно, создайте свойство и вызовите GetRealInstruction() в getter.

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

Ответ 3

Вы можете ввести другой абстрактный класс RealInstructionBase, чтобы ваш код выглядел следующим образом:

public abstract class Instruction {
   public Instruction() {
       // do common stuff
   }
}

public abstract class RealInstructionBase : Instruction {
   public RealInstructionBase() : base() {
       GetRealInstruction();
   }

   protected abstract object GetRealInstruction();
}

Теперь каждая инструкция, которая должна использовать RealInstruction, выводится из RealInstructionBase, а все остальные - из инструкции. Таким образом, вы должны правильно их инициализировать.

EDIT: Хорошо, это даст вам более чистый дизайн (нет, если в конструкторе), но не избавится от предупреждения. Теперь, если вы хотите знать, почему вы получаете предупреждение в первую очередь, вы можете обратиться к этому вопросу. В основном, дело в том, что вы будете в безопасности, когда помечаете свои классы, где вы применяете абстрактный метод, как запечатанный.

Ответ 4

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

protected Instruction(..., Instruction realInstruction)
{
    //Some stuff

    if (DoesUseRealInstruction) {
        RealInstruction = realInstruction;
    }
}

public DerivedInstruction(...)
    : base(..., GetRealInstruction(...))
{
}

Или, если вы действительно хотите вызвать виртуальную функцию из своего конструктора (с которой я вас очень не согласен), вы можете подавить предупреждение ReSharper:

// ReSharper disable DoNotCallOverridableMethodsInConstructor
    RealInstruction = GetRealInstruction(instructionSet, Argument);
// ReSharper restore DoNotCallOverridableMethodsInConstructor

Ответ 5

Другим вариантом является введение метода Initialize(), где выполняется вся инициализация, требующая полностью сконструированного объекта.