ReSharper로부터 객체 생성자의 가상 멤버에 대한 호출에 대한 경고가 표시됩니다.
왜이 일을하지 않는 것이 좋을까요?
C #으로 작성된 객체가 생성되면 초기화 프로그램이 가장 파생 된 클래스에서 기본 클래스로 순서대로 실행 된 다음 생성자가 기본 클래스에서 가장 파생 된 클래스로 순서대로 실행됩니다.이것이에 대한 자세한 내용은 Eric Lippert의 블로그를 참조하십시오.).
또한 .NET 객체는 생성 될 때 유형을 변경하지 않지만 가장 파생 된 유형으로 시작합니다. 가장 많이 파생 된 유형의 메소드 테이블이 있습니다. 즉, 가상 메소드 호출은 항상 가장 파생 된 유형에서 실행됩니다.
이 두 가지 사실을 결합 할 때 생성자에서 가상 메소드 호출을 만들고 상속 계층 구조에서 가장 파생 된 유형이 아니라면 생성자가 생성되지 않은 클래스에서 호출된다는 문제가 남아 있습니다 실행되므로 적절한 메소드가 호출되지 않는 상태 일 수 있습니다.
클래스를 상속 계층 구조에서 가장 파생 된 유형으로 보장하기 위해 클래스를 봉인으로 표시하면이 문제는 물론 완화됩니다.이 경우 가상 메서드를 호출하는 것이 안전합니다.
귀하의 질문에 대답하기 위해 다음 질문을 고려하십시오.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
null객체의 기본 생성자는 자체 생성자보다 먼저 호출됩니다.. ~함으로써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)
생성자가 클라이언트 코드로 돌아갈 때, 생성자가 throw 될 때 또는 객체가 포기 될 때 호출되는 메서드입니다. 기본 클래스 생성자에서 가상 메서드를 호출해야하는 대부분의 시나리오는 2 단계 구성을 사용하는 것이 가장 좋지만 2 단계 구성은 클라이언트가 두 번째 단계를 호출해야한다는 요구 사항이 아닌 구현 세부 사항으로 작동해야합니다. - supercatGetDependency
전에 호출하기에 안전하다는 보장이 없다.MySubClass
생성자가 호출되었습니다. 또한 기본적으로 인스턴스화 된 기본 종속성을 갖는 것은 사용자가 "순수 DI"라고 부르는 것이 아닙니다. - Groo
예, 일반적으로 생성자에서 가상 메서드를 호출하는 것은 좋지 않습니다.
이 시점에서, 오브제는 아직 완전히 구축되지 않았을 수 있으며, 방법으로 예상되는 불변량은 아직 성립되지 않을 수 있습니다.
생성자는 (나중에 소프트웨어의 확장 부분에서) 가상 메소드를 오버라이드하는 서브 클래스의 생성자에서 호출 할 수 있습니다. 이제 하위 클래스의 함수 구현이 아니라 기본 클래스의 구현이 호출됩니다. 그래서 여기에 가상 함수를 호출하는 것은 실제로 의미가 없습니다.
그러나 디자인이 Liskov Substitution 원리를 만족한다면 해를 끼치 지 않을 것입니다. 아마도 그것이 허용 된 이유 일 것입니다 - 경고가 아니라 오류.
이 질문의 한 가지 중요한 측면은 다른 대답이 아직 해결하지 못했기 때문에 기본 클래스가 생성자 내에서 가상 멤버를 호출하는 것이 안전하다는 것입니다그것이 파생 된 클래스가 기대하는 바라면. 그러한 경우, 파생 클래스의 설계자는 구축이 완료되기 전에 실행되는 모든 메소드가 상황에 따라 가능한 것처럼 현저하게 동작하도록해야합니다. 예를 들어 C ++ / CLI에서 생성자는 호출 할 코드로 싸여 있습니다.Dispose
구성이 실패하면 부분적으로 구성된 객체에 부름Dispose
이러한 경우 자원 유출을 막기 위해 종종 필요합니다.Dispose
메소드가 실행되는 객체가 완전히 구성되지 않았을 가능성을 대비하여 메소드를 준비해야합니다.
생성자가 실행을 완료 할 때까지 객체가 완전히 인스턴스화되지 않기 때문입니다. 가상 함수가 참조하는 멤버는 초기화 할 수 없습니다. C ++에서는 생성자에있을 때,this
현재 생성중인 객체의 실제 동적 유형이 아니라 생성자의 정적 유형 만 참조합니다. 즉, 가상 함수 호출은 예상 한 위치로 이동하지 않을 수도 있습니다.
경고는 가상 멤버가 파생 클래스에서 재정의 될 가능성이 있음을 상기시켜줍니다. 이 경우 부모 클래스가 가상 멤버에 한 모든 작업이 취소되거나 하위 클래스를 재정 의하여 변경됩니다. 명확성을 위해 작은 예제를보십시오.
아래의 부모 클래스는 생성자에서 가상 멤버에 값을 설정하려고 시도합니다. 그리고 이것은 Re-sharper warning을 발생 시키며, 코드를 보자 :
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();
}
}
여기에있는 자식 클래스는 부모 속성을 재정의합니다. 이 속성이 가상으로 표시되지 않은 경우 컴파일러는 속성이 부모 클래스의 속성을 숨기고 사용자가 의도적 인 경우 '새'키워드를 추가 할 것을 제안합니다.
public class Child: Parent
{
public Child():base()
{
this.Obj = "Something";
}
public override object Obj{get;set;}
}
마지막으로 사용에 미치는 영향, 아래 예제의 결과는 부모 클래스 생성자가 설정 한 초기 값을 포기합니다.그리고 이것은 재 예리함이 당신에게 경고하려고 시도하는 것입니다.,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의 모델 인 경우 가상 키워드가 제거되어 관계의 지연로드가 비활성화됩니다.
public **virtual** User User{ get; set; }
한 가지 중요한 누락 된 비트는이 문제를 해결할 올바른 방법은 무엇입니까?
같이그렉은 설명했다.근본적인 문제는 파생 클래스가 생성되기 전에 기본 클래스 생성자가 가상 멤버를 호출한다는 것입니다.
다음 코드는 다음 코드에서 가져온 것입니다.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를 두 번 호출하지 않는 방법 - Sven Vranckx
이 특별한 경우에는 C ++과 C #사이에 차이점이 있습니다. C ++에서는 객체가 초기화되지 않으므로 생성자 내부에서 virutal 함수를 호출하는 것이 안전하지 않습니다. C #에서는 클래스 객체가 만들어지면 모든 멤버가 초기화됩니다. 생성자에서 가상 함수를 호출 할 수 있지만 여전히 0 인 멤버는 액세스 할 수 있습니다. 멤버에 액세스 할 필요가 없다면 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 오류가 나에게 벙어리 인 것을 아래와 같이함으로써 만족 될 수 있다는 것이다. (그러나 이전에 많이 언급했듯이, ctor에서 virtual prop / methods를 호출하는 것은 좋은 생각이 아니다.
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 () 메서드를 추가 한 다음 파생 생성자에서 호출합니다. 이 메서드는 모든 생성자가 실행 된 후에 가상 / 추상 메서드 / 속성을 호출합니다. :)