web-dev-qa-db-ja.com

「コンストラクターでの仮想メソッド呼び出し」問題の解決

私は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メソッドにはもう1つのオーバーロードがあるため、デリゲートを適切に使用できません。

19
Edward B.

この問題に何度も遭遇しましたが、適切に解決するために見つけた最善の方法は、コンストラクターから呼び出されている仮想メソッドを別のクラスに抽象化することです。次に、この新しいクラスのインスタンスを元の抽象クラスのコンストラクターに渡し、各派生クラスが独自のバージョンをベースコンストラクターに渡します。説明するのは少し難しいので、あなたに基づいて例を挙げます。

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を持っている(または少なくともオプションでそうする)という事実によるものです;しかし、ご覧のように、希望することを実現し、コンストラクターからの仮想メンバーの呼び出しを回避することも可能です。

それでも不明な場合は、自分のコードで最近使用したものに基づいた例も示します。この場合、ヘッダーフォームとメッセージフォームの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
        }
    }
}
19
Richard

派生クラスのインスタンスを作成すると、呼び出しスタックは次のようになります。

_GetRealInstruction()
BaseContructor()
DerivedConstructor()
_

GetRealInstructionは、コンストラクターがまだ実行を完了していない派生クラスでオーバーライドされます。

他のコードがどのように見えるかはわかりませんが、この場合、メンバー変数が本当に必要かどうかを最初に確認する必要があります。必要なオブジェクトを返すメソッドがあります。本当に必要な場合は、プロパティを作成し、ゲッターでGetRealInstruction()を呼び出します。

また、GetRealInstructionを抽象化することもできます。これにより、例外をスローする必要がなくなり、派生クラスでオーバーライドするのを忘れた場合、コンパイラーによってエラーが発生します。

2
Taras Dzyoba

別の抽象クラス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から派生します。このようにして、すべてを正しく初期化する必要があります。

[〜#〜] edit [〜#〜]:わかりました、これはよりきれいなデザインを提供するだけです(コンストラクターの場合は不可)が、取得されません警告を取り除きます。そもそもなぜ警告が出るのか知りたければ、 この質問 を参照してください。基本的に重要なのは、抽象メソッドを実装しているクラスをシール済みとしてマークすると安全になるということです。

2
Szymon Kuzniak

実際の命令を基本クラスのコンストラクターに渡すことができます。

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
1
Torbjörn Kalin

もう1つのオプションは、完全に作成されたオブジェクトを必要とするすべての初期化を行うInitialize()メソッドを導入することです。