オブジェクトコンストラクタから仮想メンバへの呼び出しについて、ReSharperから警告を受けています。
なぜこれはしてはいけないのでしょうか。
C#で書かれたオブジェクトが構築されると、初期化子は最も派生クラスから基本クラスへと順番に実行され、次にコンストラクタは基本クラスから最も派生クラスへと順番に実行されます( Eric Lippert's blogを参照)。これがなぜであるかについての詳細については )。
また.NETでは、オブジェクトは構築時に型を変更するのではなく、最も派生型として開始し、メソッド表は最も派生型になります。つまり、仮想メソッド呼び出しは常に最も派生型で実行されます。
これら2つの事実を組み合わせると、コンストラクター内で仮想メソッド呼び出しを行い、それが継承階層内で最も派生型ではない場合、コンストラクターが継承されていないクラスで呼び出されるという問題が残ります。そのため、そのメソッドが呼び出されるのに適した状態になっていない可能性があります。
継承階層で最も派生型であることを保証するためにクラスをシール済みとしてマークした場合はもちろん、この問題は軽減されます。その場合は、仮想メソッドを呼び出しても安全です。
あなたの質問に答えるために、この質問を考慮してください:Child
name__オブジェクトがインスタンス化されるとき、以下のコードは何をプリントアウトしますか?
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
name__がnullであるため、foo
name__がスローされることです。 オブジェクトの基本コンストラクタは、それ自身のコンストラクタの前に呼び出されます 。オブジェクトのコンストラクタでvirtual
name__を呼び出すことで、継承しているオブジェクトが完全に初期化される前にコードを実行する可能性があります。
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#では、基本クラスのコンストラクターはbefore派生クラスのコンストラクター)を実行するため、派生クラスがオーバーライドされる可能性のある仮想メンバーで使用するインスタンスフィールドはまだ初期化されていません。
これは注意を向けさせるためのwarningにすぎません。このシナリオには実際のユースケースがあります。仮想メンバのビヘイビアをドキュメント化を呼び出すだけで、その下の派生クラスで宣言されたインスタンスフィールドを使用できないようにするだけです。
あなたがそれをしたくない理由については、上によく書かれた答えがあります。これは、おそらくあなたがすることを望む反例です( Rubyの実用的なオブジェクト指向設計 からC#に翻訳されています)サンディ・メッツ、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 { }
はい、コンストラクタで仮想メソッドを呼び出すのは一般的には良くありません。
この時点で、オブジェクトはまだ完全に構築されていない可能性があり、メソッドによって期待される不変式はまだ成り立たない可能性があります。
あなたのコンストラクタは(後で、あなたのソフトウェアの拡張で)仮想メソッドをオーバーライドするサブクラスのコンストラクタから呼ばれるかもしれません。サブクラスの関数の実装ではなく、基本クラスの実装が呼び出されます。したがって、ここで仮想関数を呼び出すことは実際には意味がありません。
しかし、あなたのデザインがLiskov Substitutionの原則を満たしている場合、害は何も行われません。おそらくそれが許容される理由です - エラーではなく警告です。
他の答えがまだ対処していないこの質問の重要な側面の1つは、基本クラスがそのコンストラクタ内から仮想メンバーを呼び出すことは安全であることですそれが派生クラスがそれを期待している場合 。このような場合、派生クラスの設計者は、構築が完了する前に実行されるメソッドが状況下で可能な限り賢明に動作することを保証する責任があります。たとえば、C++/CLIでは、コンストラクターはコードでラップされ、構築が失敗した場合に部分的に構築されたオブジェクトでDispose
を呼び出します。このような場合にDispose
を呼び出すことは、リソースのリークを防ぐために必要になることがよくありますが、Dispose
メソッドは、実行対象のオブジェクトが完全に構築されていない可能性に備えなければなりません。
コンストラクタが実行を完了するまで、オブジェクトは完全にインスタンス化されていません。仮想関数によって参照されるメンバーは初期化されない可能性があります。 C++では、コンストラクタの中にいるとき、this
は自分がいるコンストラクタの静的型のみを参照し、作成されているオブジェクトの実際の動的型は参照しません。これは、仮想関数呼び出しがあなたが期待するところまで行かないかもしれないことを意味します。
欠けている重要な点の1つは、この問題を解決するための正しい方法は何ですか?
Gregが で説明したように、ここでの根本的な問題は、派生クラスが構築される前に基本クラスのコンストラクタが仮想メンバを呼び出すということです。
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);
}
}
警告は、仮想メンバーが派生クラスでオーバーライドされる可能性が高いことを思い出させるものです。その場合、親クラスが仮想メンバーに対して行ったことはすべて、子クラスをオーバーライドすることによって取り消されるか、または変更されます。明快さのために小さい例打撃を見てください
以下の親クラスは、コンストラクタの仮想メンバーに値を設定しようとします。そしてこれは再シャープ警告を引き起こすでしょう、コードで見てみましょう:
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があなたに警告しようとしていることです。 、 Parentクラスコンストラクタに設定された値は、親クラスコンストラクタ の直後に呼び出される子クラスコンストラクタによって上書きされます。
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; }
私の考えを加えるためだけに。プライベートフィールドを定義するときに常に初期化するのであれば、この問題は避けるべきです。少なくとも以下のコードは魅力のように動作します。
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());
}
}
この特定のケースでは、C++とC#の間に違いがあります。 C++では、オブジェクトは初期化されていないため、コンストラクタ内で仮想関数を呼び出すのは危険です。 C#では、クラスオブジェクトが作成されると、そのすべてのメンバはゼロで初期化されます。コンストラクタ内で仮想関数を呼び出すことは可能ですが、それでもゼロであるメンバにアクセスする可能性がある場合はそうです。メンバーにアクセスする必要がない場合は、C#で仮想関数を呼び出しても安全です。
私が見つけたもう一つの面白いことは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";
}
}