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

Я делаю программное обеспечение в C#. Я использую абстрактный класс, 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());
}

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

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

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

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

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

5 ответов

Решение

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

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's 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's nothing stopping you from making this a standalone class of it's 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
        }
    }
}

Вы можете ввести другой абстрактный класс 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, а все остальные - от Instruction. Таким образом, вы должны правильно инициализировать их.

РЕДАКТИРОВАТЬ: Хорошо, это только даст вам более чистый дизайн (нет, если в конструкторе), но не избавится от предупреждения. Теперь, если вы хотите знать, почему вы получаете предупреждение, вы можете обратиться к этому вопросу. По сути, дело в том, что вы будете в безопасности, когда отметите свои классы, где вы реализуете абстрактный метод, как закрытые.

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

GetRealInstruction()
BaseContructor()
DerivedConstructor()

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

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

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

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

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

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

Другие вопросы по тегам