etc./StackOverFlow

생성자의 가상 멤버 호출

청렴결백한 만능 재주꾼 2022. 3. 22. 11:04
반응형

질문자 :JasonS


내 객체 생성자에서 가상 멤버에 대한 호출에 대해 ReSharper로부터 경고를 받고 있습니다.

이것이 왜 하지 말아야 할 일이겠습니까?



C#으로 작성된 개체가 생성되면 이니셜라이저가 가장 많이 파생된 클래스에서 기본 클래스로 순서대로 실행되고 생성자가 기본 클래스에서 가장 많이 파생된 클래스로 순서대로 실행됩니다( 자세한 내용은 Eric Lippert의 블로그 참조 왜 그런지 ).

또한 .NET에서 개체는 생성될 때 형식을 변경하지 않지만 가장 많이 파생된 형식에 대한 메서드 테이블을 사용하여 가장 많이 파생된 형식으로 시작합니다. 즉, 가상 메서드 호출은 항상 가장 많이 파생된 형식에서 실행됩니다.

이 두 가지 사실을 결합하면 생성자에서 가상 메서드 호출을 수행하고 상속 계층에서 가장 파생된 형식이 아닌 경우 생성자가 지정되지 않은 클래스에서 호출된다는 문제가 남습니다. 실행되므로 해당 메서드를 호출하기에 적합한 상태가 아닐 수 있습니다.

물론 이 문제는 상속 계층에서 가장 파생된 형식이 되도록 클래스를 봉인된 것으로 표시하면 완화됩니다. 이 경우 가상 메서드를 호출하는 것이 완벽하게 안전합니다.


Greg Beech

귀하의 질문에 답하기 위해 다음 질문을 고려하십시오. 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!?! } }

대답은 foo 가 null이기 때문에 NullReferenceException 객체의 기본 생성자는 자신의 생성자보다 먼저 호출 됩니다. virtual 호출을 가짐으로써 상속 개체가 완전히 초기화되기 전에 코드를 실행할 가능성을 소개합니다.


Matt Howells

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 간에 규칙이 매우 다르기 때문에 어쨌든 생성자에서 가상 함수를 피하는 것이 좋습니다. 프로그래머는 무엇을 기대해야 할지 모를 수도 있습니다!


Lloyd

경고의 이유는 이미 설명되어 있지만 경고를 수정하는 방법은 무엇입니까? 클래스 또는 가상 멤버를 봉인해야 합니다.

 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(); } }

Ilya Ryzhenkov

C#에서 기본 클래스의 생성자는 파생 클래스의 생성자보다 먼저 실행되므로 파생 클래스가 재정의할 수 있는 가상 멤버에서 사용할 수 있는 모든 인스턴스 필드는 아직 초기화되지 않았습니다.

이것은 주의를 기울이고 문제가 없는지 확인하기 위한 경고일 뿐입니다. 이 시나리오에 대한 실제 사용 사례 가 있습니다. 가상 멤버의 동작 을 문서화하기만 하면 생성자가 호출하는 아래 파생 클래스에서 선언된 인스턴스 필드를 사용할 수 없습니다.


Alex Lyman

당신이하고 싶지 이유를 위해 위의 잘 쓰여진 답변이 있습니다. 다음은 아마도 그렇게 하고 싶을 수 있는 반대 예 입니다(Sandi Metz의 Practical Object-Oriented Design in Ruby , p. 126에서 C#으로 번역).

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 { }

Josh Kodroff

예, 일반적으로 생성자에서 가상 메서드를 호출하는 것은 좋지 않습니다.

이 시점에서 대상은 아직 완전히 구성되지 않았을 수 있으며 메서드에서 기대하는 불변성은 아직 유지되지 않을 수 있습니다.


David Pierre

한 가지 중요한 누락 사항은 이 문제를 해결하는 올바른 방법은 무엇입니까?

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); } }

Gustavo Mori

생성자가 실행을 완료할 때까지 개체가 완전히 인스턴스화되지 않기 때문입니다. 가상 함수에서 참조하는 멤버는 초기화되지 않을 수 있습니다. C++에서 생성자에 있을 때 this 생성 중인 객체의 실제 동적 유형이 아니라 현재 있는 생성자의 정적 유형만 참조합니다. 이는 가상 함수 호출이 예상한 대로 진행되지 않을 수 있음을 의미합니다.


1800 INFORMATION

생성자는 (나중에 소프트웨어 확장에서) 가상 메서드를 재정의하는 하위 클래스의 생성자에서 호출될 수 있습니다. 이제 하위 클래스의 함수 구현이 아니라 기본 클래스의 구현이 호출됩니다. 따라서 여기서 가상 함수를 호출하는 것은 의미가 없습니다.

그러나 디자인이 Liskov 치환 원칙을 만족한다면 아무런 해를 끼치지 않습니다. 아마도 그것이 용인되는 이유일 것입니다. 오류가 아니라 경고입니다.


xtofl

다른 답변에서 아직 다루지 않은 이 질문의 중요한 측면 중 하나는 파생 클래스가 수행할 것으로 예상되는 경우 기본 클래스가 생성자 내에서 가상 멤버를 호출하는 것이 안전하다는 것입니다. 이러한 경우 파생 클래스의 디자이너는 구성이 완료되기 전에 실행되는 모든 메서드가 상황에서 최대한 현명하게 동작하도록 할 책임이 있습니다. 예를 들어, C++/CLI에서 생성자는 생성이 실패할 경우 부분적으로 생성된 객체에 대해 Dispose Dispose 를 호출하는 것은 리소스 누수를 방지하는 데 필요한 경우가 많지만 실행되는 개체가 완전히 구성되지 않았을 수 있으므로 Dispose


supercat

이 경고는 가상 멤버가 파생 클래스에서 재정의될 가능성이 있다는 알림입니다. 이 경우 부모 클래스가 가상 멤버에 대해 수행한 모든 작업은 자식 클래스를 재정의하여 실행 취소되거나 변경됩니다. 명확성을 위해 작은 예시 타격을 보십시오.

아래의 부모 클래스는 생성자의 가상 멤버에 값을 설정하려고 시도합니다. 그러면 Re-sharper 경고가 트리거됩니다. 코드에서 살펴보겠습니다.

 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" } }

Biniam Eyakem

Resharper의 조언을 맹목적으로 따르고 클래스를 봉인하지 않도록 주의하십시오! EF Code First의 모델인 경우 가상 키워드를 제거하고 해당 관계의 지연 로드를 비활성화합니다.

 public **virtual** User User{ get; set; }

typhon04

이 특정한 경우에는 C++와 C# 사이에 차이가 있습니다. C++에서 객체는 초기화되지 않으므로 생성자 내에서 가상 함수를 호출하는 것은 안전하지 않습니다. C#에서는 클래스 개체가 생성될 때 모든 해당 멤버가 0으로 초기화됩니다. 생성자에서 가상 함수를 호출하는 것이 가능하지만 여전히 0인 멤버에 액세스할 수 있습니다. 멤버에 액세스할 필요가 없는 경우 C#에서 가상 함수를 호출하는 것이 매우 안전합니다.


Yuval Peled

제 생각을 덧붙이자면. private 필드를 정의할 때 항상 초기화하면 이 문제를 피해야 합니다. 적어도 아래 코드는 매력처럼 작동합니다.

 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()); } }

Jim Ma

부모 생성자가 즉시 사용할 속성을 설정하거나 재정의할 수 있는 기능을 자식 클래스에 부여하려는 경우 경고를 무시하는 것이 합법적일 수 있다고 생각합니다.

 internal class Parent { public Parent() { Console.WriteLine("Parent ctor"); Console.WriteLine(Something); } protected virtual string Something { get; } = "Parent"; } internal class Child : Parent { public Child() { Console.WriteLine("Child ctor"); Console.WriteLine(Something); } protected override string Something { get; } = "Child"; }

여기서 위험은 자식 클래스가 해당 생성자에서 속성을 설정하는 것입니다. 이 경우 기본 클래스 생성자가 호출된 후에 값이 변경됩니다.

내 사용 사례는 자식 클래스가 변환기와 같은 특정 값이나 유틸리티 클래스를 제공하기를 원하고 기본에서 초기화 메서드를 호출할 필요가 없다는 것입니다.

자식 클래스를 인스턴스화할 때 위의 출력은 다음과 같습니다.

 Parent ctor Child Child ctor Child

pasx

기본 클래스에 Initialize() 메서드를 추가한 다음 파생 생성자에서 호출합니다. 이 메서드는 모든 생성자가 실행된 후 가상/추상 메서드/속성을 호출합니다. :)


Ross

내가 찾은 또 다른 흥미로운 점은 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"; } }

adityap

출처 : http:www.stackoverflow.com/questions/119506/virtual-member-call-in-a-constructor

반응형