オブジェクトコンストラクタから仮想メンバへの呼び出しについて、ReSharperから警告を受けています。
なぜこれはしてはいけないのでしょうか。
C#で書かれたオブジェクトが構築されると、初期化子は最も派生クラスから基本クラスへと順番に実行され、次にコンストラクタは基本クラスから最も派生クラスへと実行されるこれがなぜなのかについての詳細はEric Lippertのブログを見てください。)
また.NETでは、オブジェクトは構築時に型を変更するのではなく、最も派生型として開始し、メソッド表は最も派生型になります。つまり、仮想メソッド呼び出しは常に最も派生型で実行されます。
これら2つの事実を組み合わせると、コンストラクタ内で仮想メソッド呼び出しを行い、それが継承階層で最も派生型ではない場合、コンストラクタが設定されていないクラスで呼び出されるという問題が残ります。そのため、そのメソッドが呼び出されるのに適した状態になっていない可能性があります。
継承階層で最も派生型であることを保証するためにクラスをシール済みとしてマークした場合、この問題はもちろん軽減されます。その場合、仮想メソッドを呼び出しても安全です。
あなたの質問に答えるために、この質問を考慮してください。Child
オブジェクトはインスタンス化されている?
class Parent
{
public Parent()
{
DoSomething();
}
protected virtual void DoSomething()
{
}
}
class Child : Parent
{
private string foo;
public Child()
{
foo = "HELLO";
}
protected override void DoSomething()
{
Console.WriteLine(foo.ToLower()); //NullReferenceException!?!
}
}
答えは、実際にはNullReferenceException
スローされるのは、foo
無効です。オブジェクトの基本コンストラクタは、それ自身のコンストラクタの前に呼び出されます。。を持つことによってvirtual
オブジェクトのコンストラクタを呼び出すと、継承しているオブジェクトが完全に初期化される前にコードを実行する可能性があります。
C#の規則は、JavaやC ++の規則とは大きく異なります。
C#のオブジェクトのコンストラクタにいるとき、そのオブジェクトは完全に派生した型として、完全に初期化された(単に「構築された」ものではない)形式で存在します。
namespace Demo
{
class A
{
public A()
{
System.Console.WriteLine("This is a {0},", this.GetType());
}
}
class B : A
{
}
// . . .
B b = new B(); // Output: "This is a Demo.B"
}
つまり、Aのコンストラクターから仮想関数を呼び出すと、Bのオーバーライドが提供されていれば、そのオーバーライドが解決されます。
システムの動作を完全に理解して、意図的にAとBをこのように設定したとしても、後でショックを受ける可能性があります。 Bのコンストラクタで仮想関数を呼び出し、それらがBまたはAによって適切に処理されることを「知って」いるとします。それから時間が経ち、他の誰かがCを定義し、そこで仮想関数のいくつかをオーバーライドする必要があると決心します。突然のBのコンストラクタはすべてCでコードを呼び出すことになり、これは非常に驚くべき動作につながる可能性があります。
とにかくコンストラクタの中で仮想関数を避けることはおそらく良い考えです。ありますC#、C ++、そしてJavaの間ではとても違います。あなたのプログラマは何を期待すべきかわからないかもしれません!
警告の理由はすでに説明されていますが、警告をどのように修正しますか。あなたはクラスか仮想メンバーのどちらかを封印しなければなりません。
class B
{
protected virtual void Foo() { }
}
class A : B
{
public A()
{
Foo(); // warning here
}
}
あなたはクラスAを封印することができます:
sealed class A : B
{
public A()
{
Foo(); // no warning
}
}
あるいは、メソッドFooを封印することもできます。
class A : B
{
public A()
{
Foo(); // no warning
}
protected sealed override void Foo()
{
base.Foo();
}
}
C#では、基本クラスのコンストラクタが実行されます。前派生クラスのコンストラクタ。派生クラスがオーバーライドされる可能性のある仮想メンバーで使用する可能性のあるインスタンスフィールドはまだ初期化されていません。
これはただの警告あなたが注意を払うようにし、それが大丈夫であることを確認するために。このシナリオには実際のユースケースがあります。動作を文書化するそれが呼び出しているコンストラクタがあるところの下の派生クラスで宣言されたインスタンスフィールドを使用することはできないという仮想メンバの。
なぜあなたのために上記のよく書かれた答えがありますしないそれをやりたい。これはおそらくあなたがいる反例です。だろうやりたいこと(C#からに翻訳)Rubyにおける実用的なオブジェクト指向設計著Sandi Metz、p。 126)。
ご了承くださいGetDependency()
インスタンス変数には触れません。静的メソッドが仮想の場合、静的になります。
(公平を期すために、依存性注入コンテナまたはオブジェクト初期化子を介してこれを行うより賢い方法がおそらくあります...)
public class MyClass
{
private IDependency _myDependency;
public MyClass(IDependency someValue = null)
{
_myDependency = someValue ?? GetDependency();
}
// If this were static, it could not be overridden
// as static methods cannot be virtual in C#.
protected virtual IDependency GetDependency()
{
return new SomeDependency();
}
}
public class MySubClass : MyClass
{
protected override IDependency GetDependency()
{
return new SomeOtherDependency();
}
}
public interface IDependency { }
public class SomeDependency : IDependency { }
public class SomeOtherDependency : IDependency { }
Finalize
のデフォルトメンバーとしてObject
、そのvtableスロットを使用していたManageLifetime(LifetimeStatus)
コンストラクタがクライアントコードに戻るとき、コンストラクタがスローされるとき、またはオブジェクトが放棄されたことが判明したときに呼び出されるメソッド。基本クラスのコンストラクターから仮想メソッドを呼び出すことを伴うほとんどのシナリオは、2段階構成を使用して最もうまく処理できますが、2段階構成は、クライアントが2段階を呼び出すという要件ではなく、実装の詳細として動作するはずです。 - supercatGetDependency
以前に呼び出しても安全であるとは保証されていません。MySubClass
コンストラクターが呼び出されました。また、デフォルトの依存関係をデフォルトでインスタンス化することは、「純粋なDI」と呼ぶものではありません。 - Groo
はい、コンストラクタで仮想メソッドを呼び出すのは一般的には良くありません。
この時点で、オブジェクトはまだ完全に構築されていない可能性があり、メソッドによって期待される不変式はまだ成り立たない可能性があります。
あなたのコンストラクタは(後で、あなたのソフトウェアの拡張で)仮想メソッドをオーバーライドするサブクラスのコンストラクタから呼ばれるかもしれません。サブクラスの関数の実装ではなく、基本クラスの実装が呼び出されます。したがって、ここで仮想関数を呼び出すことは実際には意味がありません。
しかし、あなたの設計がLiskov Substitutionの原則を満たしているなら、害はありません。おそらくそれが許容される理由です - エラーではなく警告です。
他の答えがまだ述べていないこの質問の1つの重要な側面は、コンストラクタ内から仮想クラスを呼び出すことが基本クラスにとって安全であるということです。それが派生クラスが期待していることであれば。そのような場合、派生クラスの設計者は、構築が完了する前に実行されるメソッドが、状況下で可能な限り賢明に振舞うようにする責任があります。例えば、C ++ / CLIでは、コンストラクタは以下を呼び出すコードでラップされています。Dispose
構築が失敗した場合は、部分的に構築されたオブジェクトに対して。呼び出しDispose
そのような場合、リソースリークを防ぐためにしばしば必要ですが、Dispose
メソッドは、それらが実行されるオブジェクトが完全には構築されていない可能性がある可能性について準備しなければなりません。
コンストラクタが実行を完了するまで、オブジェクトは完全にインスタンス化されていません。仮想関数によって参照されるメンバーは初期化されない可能性があります。 C ++では、あなたがコンストラクタの中にいるとき、this
自分がいるコンストラクタの静的型のみを参照し、作成中のオブジェクトの実際の動的型は参照しません。これは、仮想関数呼び出しがあなたが期待するところまで行かないかもしれないことを意味します。
警告は、仮想メンバーが派生クラスでオーバーライドされる可能性が高いことを思い出させるものです。その場合、親クラスが仮想メンバーに対して行ったことはすべて、子クラスをオーバーライドすることによって元に戻されるか変更されます。明快さのために小さい例打撃を見てください
以下の親クラスは、コンストラクタの仮想メンバーに値を設定しようとします。そしてこれは再シャープ警告を引き起こすでしょう、コードで見てみましょう:
public class Parent
{
public virtual object Obj{get;set;}
public Parent()
{
// Re-sharper warning: this is open to change from
// inheriting class overriding virtual member
this.Obj = new Object();
}
}
この子クラスは、親プロパティをオーバーライドします。このプロパティに仮想のマークが付けられていない場合、コンパイラはそのプロパティが親クラスのプロパティを隠していることを警告し、意図的であれば 'new'キーワードを追加することを提案します。
public class Child: Parent
{
public Child():base()
{
this.Obj = "Something";
}
public override object Obj{get;set;}
}
最後に使用への影響、以下の例の出力は親クラスのコンストラクタによって設定された初期値を放棄します。そしてこれがRe-sharperがあなたに警告しようとしていることです、親クラスのコンストラクタに設定された値は、親クラスのコンストラクタの直後に呼び出される子クラスのコンストラクタによって上書きされることがあります。
public class Program
{
public static void Main()
{
var child = new Child();
// anything that is done on parent virtual member is destroyed
Console.WriteLine(child.Obj);
// Output: "Something"
}
}
盲目的にResharperの助言に従い、クラスを封印することに気をつけてください! EF Code Firstのモデルの場合、virtualキーワードが削除され、関係の遅延ロードが無効になります。
public **virtual** User User{ get; set; }
欠けている重要な点の1つは、この問題を解決する正しい方法は何ですか?
としてグレッグは説明したここでの根本的な問題は、派生クラスが構築される前に基本クラスのコンストラクタが仮想メンバを呼び出すということです。
次のコードは、MSDNのコンストラクタ設計ガイドライン、この問題を示しています。
public class BadBaseClass
{
protected string state;
public BadBaseClass()
{
this.state = "BadBaseClass";
this.DisplayState();
}
public virtual void DisplayState()
{
}
}
public class DerivedFromBad : BadBaseClass
{
public DerivedFromBad()
{
this.state = "DerivedFromBad";
}
public override void DisplayState()
{
Console.WriteLine(this.state);
}
}
の新しいインスタンスがDerivedFromBad
が作成されると、基本クラスのコンストラクタはDisplayState
とショーBadBaseClass
フィールドは派生コンストラクタによってまだ更新されていないためです。
public class Tester
{
public static void Main()
{
var bad = new DerivedFromBad();
}
}
改良された実装は、基本クラスのコンストラクタから仮想メソッドを削除して、Initialize
方法。の新しいインスタンスを作成するDerivedFromBetter
予想される "DerivedFromBetter"を表示します
public class BetterBaseClass
{
protected string state;
public BetterBaseClass()
{
this.state = "BetterBaseClass";
this.Initialize();
}
public void Initialize()
{
this.DisplayState();
}
public virtual void DisplayState()
{
}
}
public class DerivedFromBetter : BetterBaseClass
{
public DerivedFromBetter()
{
this.state = "DerivedFromBetter";
}
public override void DisplayState()
{
Console.WriteLine(this.state);
}
}
bool initialize
パラメータ。Initialize
基本コンストラクターで呼び出されます。派生コンストラクタは、次に呼び出すでしょうbase(false)
Initializeを2回呼び出さないでください。 - Sven Vranckx
この特定のケースでは、C ++とC#の間に違いがあります。 C ++では、オブジェクトは初期化されていないため、コンストラクタ内で仮想関数を呼び出すのは危険です。 C#では、クラスオブジェクトが作成されると、そのすべてのメンバはゼロで初期化されます。コンストラクタ内で仮想関数を呼び出すことは可能ですが、それでもゼロであるメンバにアクセスする可能性がある場合はそうです。メンバーにアクセスする必要がない場合は、C#で仮想関数を呼び出しても安全です。
私の考えを加えるためだけに。プライベートフィールドを定義するときに常に初期化する場合は、この問題を回避する必要があります。少なくとも以下のコードは魅力のように動作します。
class Parent
{
public Parent()
{
DoSomething();
}
protected virtual void DoSomething()
{
}
}
class Child : Parent
{
private string foo = "HELLO";
public Child() { /*Originally foo initialized here. Removed.*/ }
protected override void DoSomething()
{
Console.WriteLine(foo.ToLower());
}
}
私が見つけたもう一つの興味深いことはReSharperエラーが私にはつまらない以下のような何かをすることによって「満足」されることができるということです。
public class ConfigManager
{
public virtual int MyPropOne { get; private set; }
public virtual string MyPropTwo { get; private set; }
public ConfigManager()
{
Setup();
}
private void Setup()
{
MyPropOne = 1;
MyPropTwo = "test";
}
}
Initialize()メソッドを基本クラスに追加してから、派生コンストラクタから呼び出します。そのメソッドは、すべてのコンストラクタが実行された後で、仮想/抽象メソッド/プロパティを呼び出します。