解决';构造函数中的虚拟方法调用';问题

本文关键字:方法 调用 问题 虚拟 构造函数 解决 | 更新日期: 2023-09-27 18:25:44

我正在用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());
}

所以Resharper告诉我,在标记行,我正在"调用构造函数中的虚拟方法",这很糟糕。我理解调用构造函数的顺序。GetRealInstruction方法的所有重写如下所示:

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

因此,它们不依赖于类中的任何数据;它们只是返回一些依赖于派生类型的内容。(因此构造函数顺序不会影响它们)。

那么,我应该忽略它吗?我宁愿不要;有人能告诉我怎样才能避免这个警告吗?

我不能巧妙地使用委托,因为GetRealInstruction方法还有一个重载。

解决';构造函数中的虚拟方法调用';问题

我已经多次遇到这个问题,我发现正确解决这个问题的最佳方法是将从构造函数调用的虚拟方法抽象到一个单独的类中。然后,您将这个新类的一个实例传递到原始抽象类的构造函数中,每个派生类都将自己的版本传递给基构造函数。解释起来有点棘手,所以我将根据你的例子来举一个例子。

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

在您的特定示例中,它确实有点令人困惑,我开始用完合理的名称,但这是因为您已经在Instructions中嵌套了Instructions,即Instruction有RealInstruction(或至少可选地有);但正如您所看到的,仍然可以实现您想要的,并避免任何来自构造函数的虚拟成员调用。

如果这还不清楚,我还将给出一个基于我最近在自己的代码中使用的示例。在这种情况下,我有两种类型的表单,一种是头表单,另一种是消息表单,这两种表单都继承自基表单。所有表单都有字段,但每个表单类型都有不同的构造字段的机制,所以我最初有一个名为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
        }
    }
}

当您创建派生类的实例时,您的调用堆栈将如下所示:

GetRealInstruction()
BaseContructor()
DerivedConstructor()

GetRealInstruction在派生类中被重写,该派生类的构造函数尚未完成运行。

我不知道您的其他代码看起来如何,但在这种情况下,您应该首先检查是否真的需要一个成员变量。您有一个返回所需对象的方法。如果您确实需要它,那么创建一个属性并在getter中调用GetRealInstruction()

也可以使GetRealInstruction抽象化。这样,您就不必抛出异常,如果您忘记在派生类中重写它,编译器会给您一个错误。

您可以引入另一个抽象类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。通过这种方式,您应该将它们全部正确初始化。

编辑:好的,这只会给你一个更干净的设计(如果在构造函数中,则不会),但不会消除警告。现在,如果你想知道为什么你首先得到警告,你可以参考这个问题。基本上,关键是当您将实现抽象方法的类标记为密封时,您将是安全的。

您可以将实际指令传递给基类构造函数:

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()方法,在该方法中执行所有需要完全构造对象的初始化。