【问题标题】:Solving the 'Virtual method call in constructor' issue解决“构造函数中的虚拟方法调用”问题
【发布时间】:2013-08-01 10:24:50
【问题描述】:

我正在用 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 方法多了一个重载。

【问题讨论】:

    标签: c# inheritance constructor resharper


    【解决方案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'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
            }
        }
    }
    

    【讨论】:

    • 我忘了提,但这种方式的另一个好处是,在您的情况下,您无需使用虚拟的基本方法,而不是抽象的,因此您可以获得编译时检查而不是抛出例外(如@TarasDzyoba 所述)。
    • 这个想法很好,谢谢。我最终以另一种方式避免了这个问题,但这是未来非常好的解决方案。
    【解决方案2】:

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

    GetRealInstruction()
    BaseContructor()
    DerivedConstructor()
    

    GetRealInstruction在派生类中被覆盖,其构造函数尚未完成运行。

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

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

    【讨论】:

    • 我了解调用顺序问题。我只需要某些派生类中的GetRealInstruction(设置了doesRequireRealInstruction 位的类),所以我不应该把它抽象化。我真的不想为此创建一个属性,因为我喜欢字段和方法之间的访问/操作差异。 (在这种情况下我确实使用了属性,但对于更简单的操作)我最终将调用移到了其他地方。
    【解决方案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 派生,所有其他指令都从 Instruction 派生。这样,您应该将它们全部正确初始化。

    编辑:好的,这只会为您提供更简洁的设计(如果在构造函数中没有),但不会消除警告。 现在,如果您想知道为什么会首先收到警告,可以参考this question。基本上,关键是当您将实现抽象方法的类标记为密封时,您将是安全的。

    【讨论】:

    • 这无疑更干净。但是,我设法通过在需要之前移动GetRealInstruction 调用来解决它,而不是在构造函数中。这个功能不太贵。
    【解决方案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
    

    【讨论】:

    • 这可以正常工作,但它不会消除警告的原因,只是隐藏它。如果没有其他解决方案,我可以使用它。我喜欢工作一次并设置好自己的东西。我不喜欢将设置交给调用者。
    • 答案已修改。但我想这仍然不是你想要的。我怀疑是否有这样的解决方案。 ReSharper 警告有 a good reason
    • 我明白了。我最终将 GetRealInstruction 调用移动到另一个函数,以便在需要时调用它。
    【解决方案5】:

    另一种选择是引入Initialize() 方法,您可以在其中执行所有需要完全构造的对象的初始化。

    【讨论】:

      猜你喜欢
      • 2015-06-27
      • 1970-01-01
      • 2012-09-02
      • 2011-09-21
      • 2016-02-29
      • 2016-04-09
      • 2010-09-12
      • 2018-02-09
      • 2010-11-12
      相关资源
      最近更新 更多