1190

ReSharper로부터 객체 생성자의 가상 멤버에 대한 호출에 대한 경고가 표시됩니다.

왜이 일을하지 않는 것이 좋을까요?


  • @ m.edmondson, 진실하게 .. 당신의 의견은 여기의 대답이어야합니다. Greg 설명이 정확하지만 나는 당신의 블로그를 읽을 때까지 그것을 이해하지 못했습니다. - Rosdi Kasim
  • @ m.edmondson 귀하의 도메인이 만료되었습니다. - Robert Noack
  • @ m.edmondson의 기사를 지금보실 수 있습니다 :codeproject.com/Articles/802375/… - SpeziFish

17 답변


1072

C #으로 작성된 객체가 생성되면 초기화 프로그램이 가장 파생 된 클래스에서 기본 클래스로 순서대로 실행 된 다음 생성자가 기본 클래스에서 가장 파생 된 클래스로 순서대로 실행됩니다.이것이에 대한 자세한 내용은 Eric Lippert의 블로그를 참조하십시오.).

또한 .NET 객체는 생성 될 때 유형을 변경하지 않지만 가장 파생 된 유형으로 시작합니다. 가장 많이 파생 된 유형의 메소드 테이블이 있습니다. 즉, 가상 메소드 호출은 항상 가장 파생 된 유형에서 실행됩니다.

이 두 가지 사실을 결합 할 때 생성자에서 가상 메소드 호출을 만들고 상속 계층 구조에서 가장 파생 된 유형이 아니라면 생성자가 생성되지 않은 클래스에서 호출된다는 문제가 남아 있습니다 실행되므로 적절한 메소드가 호출되지 않는 상태 일 수 있습니다.

클래스를 상속 계층 구조에서 가장 파생 된 유형으로 보장하기 위해 클래스를 봉인으로 표시하면이 문제는 물론 완화됩니다.이 경우 가상 메서드를 호출하는 것이 안전합니다.


  • 그렉, 제발 왜 누군가가 수업을 봉인 해 버렸는지 말해 줄 수 없습니까? (상속받지 않을 수는 없습니다) [버젼 클래스로 오버라이드하는] 가상 멤버가있을 때 말입니다. - Paul Pacurar
  • 파생 클래스가 더 이상 파생되지 않도록하려면 봉인 클래스를 완벽하게 수락 할 수 있습니다. - Øyvind
  • @ 폴 - 그 핵심은베이스class [es]를 사용하여 클래스를 원하는대로 완전히 파생시킨 것으로 표시합니다. - ljs
  • @Greg 가상 메소드의 동작이 인스턴스 변수와 아무런 관련이 없다면이 작업을 수행 할 수 있습니까? 가상 메서드가 인스턴스 변수를 수정하지 않는다는 것을 선언 할 수 있어야합니다. (정적?) 예를 들어, 더 파생 된 유형을 인스턴스화하기 위해 대체 할 수있는 가상 메소드를 원할 경우. 이것은 나에게 안전하고,이 경고를 보증하지 않습니다. - Dave Cousineau
  • @PaulPacurar - 가장 파생 된 클래스에서 가상 메서드를 호출하려는 경우에도 문제가 발생하지 않을 것이라는 것을 알면서 경고 메시지가 표시됩니다. 이 경우 해당 클래스를 봉인하여 시스템에 대한 지식을 공유 할 수 있습니다. - Revolutionair

527

귀하의 질문에 대답하기 위해 다음 질문을 고려하십시오.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왜냐하면foonull객체의 기본 생성자는 자체 생성자보다 먼저 호출됩니다.. ~함으로써virtual객체의 생성자를 호출하면 객체를 상속하면 코드가 완전히 초기화되기 전에 코드가 실행될 가능성이 있습니다.


  • 표시된 것보다 더 나은 대답. - Hele
  • 위의 답변보다 더 분명합니다. 샘플 코드는 천 단어의 가치가 있습니다. - Novice in.NET
  • 나는 동의한다, 좋은 본보기. - Joris Brauns

156

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에서 매우 다릅니다. 프로그래머는 무엇을 기대해야할지 모를 수 있습니다!


  • Greg Beech의 대답은 유감스럽게도 내 대답만큼 높은 점수를받지 못했지만 좋은 대답이라고 생각합니다. 포함 할 가치가있는 몇 가지 중요한 설명 정보가 있습니다. - Lloyd
  • 사실 Java의 규칙은 동일합니다. - OlegYch
  • @ Jo &oPortela C ++은 실제로 매우 다릅니다. 생성자 (및 소멸자!)의 가상 메서드 호출은 현재 생성되는 형식 (및 vtable)을 사용하여 해결되며 Java 및 C #과 마찬가지로 대부분 파생 형식이 아닙니다.관련 FAQ 항목은 다음과 같습니다.. - Jacek Sieka
  • @ JacekSieka 당신은 절대적으로 옳습니다. 그것은 C ++로 코딩 된 이후로 꽤 오래되었습니다. 나는이 모든 것을 어떻게 든 혼란스럽게 보았습니다. 다른 사용자를 혼동시키지 않도록 댓글을 삭제해야합니까? - João Portela
  • C #이 Java와 VB.NET과 다른 중요한 방법이 있습니다. C #에서 선언 지점에서 초기화되는 필드는 기본 생성자 호출 전에 처리 된 초기화를 갖습니다. 이는 생성자에서 파생 클래스 객체를 사용할 수 있도록하기 위해 수행되었지만 불행히도 그러한 능력은 초기화가 파생 클래스 매개 변수에 의해 제어되지 않는 파생 클래스 피처에만 적용됩니다. - supercat

83

경고의 이유는 이미 설명되어 있지만 경고를 어떻게 수정합니까? 클래스 또는 가상 멤버 중 하나를 봉인해야합니다.

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


16

C #에서는 기본 클래스의 생성자가 실행됩니다.전에파생 클래스가 생성자이므로 상속 된 가상 멤버에서 파생 클래스가 사용할 수있는 인스턴스 필드는 아직 초기화되지 않습니다.

이것이 단지경고주의를 기울이고 모든 것이 올바른지 확인하십시오. 이 시나리오의 실제 사용 사례가 있습니다.행동을 문서화하다생성자가 호출하는 곳 아래에있는 파생 클래스에서 선언 된 인스턴스 필드를 사용할 수 없다는 가상 멤버의 이름입니다.


11

위의 잘 쓰여진 답변이 위에 나와 있습니다.~ 않을거야.그렇게하고 싶다. 다음은 반대 사례입니다.할 것이다그것을 원한다 (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 { }


  • 나는 이것을 위해 factory 메소드를 사용할 것이다. - Ian Ringrose
  • 나는 .NET Framework가 가지고 있었으면 좋겠다.Finalize기본 회원으로Object, vtable 슬롯을 사용하여ManageLifetime(LifetimeStatus)생성자가 클라이언트 코드로 돌아갈 때, 생성자가 throw 될 때 또는 객체가 포기 될 때 호출되는 메서드입니다. 기본 클래스 생성자에서 가상 메서드를 호출해야하는 대부분의 시나리오는 2 단계 구성을 사용하는 것이 가장 좋지만 2 단계 구성은 클라이언트가 두 번째 단계를 호출해야한다는 요구 사항이 아닌 구현 세부 사항으로 작동해야합니다. - supercat
  • 여전히이 스레드에 표시된 다른 모든 경우와 마찬가지로이 코드로 문제가 발생할 수 있습니다.GetDependency전에 호출하기에 안전하다는 보장이 없다.MySubClass생성자가 호출되었습니다. 또한 기본적으로 인스턴스화 된 기본 종속성을 갖는 것은 사용자가 "순수 DI"라고 부르는 것이 아닙니다. - Groo
  • 이 예는 "의존성 퇴출"을 수행한다. ;-) 나에게 이것은 생성자로부터의 가상 메소드 호출을위한 또 다른 좋은 예이다. SomeDependency는 MySubClass 파생에서 더 이상 인스턴스화되지 않아 SomeDependency에 의존하는 모든 MyClass 기능에 대한 작동이 중단됩니다. - Nachbars Lumpi

5

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

이 시점에서, 오브제는 아직 완전히 구축되지 않았을 수 있으며, 방법으로 예상되는 불변량은 아직 성립되지 않을 수 있습니다.


5

생성자는 (나중에 소프트웨어의 확장 부분에서) 가상 메소드를 오버라이드하는 서브 클래스의 생성자에서 호출 할 수 있습니다. 이제 하위 클래스의 함수 구현이 아니라 기본 클래스의 구현이 호출됩니다. 그래서 여기에 가상 함수를 호출하는 것은 실제로 의미가 없습니다.

그러나 디자인이 Liskov Substitution 원리를 만족한다면 해를 끼치 지 않을 것입니다. 아마도 그것이 허용 된 이유 일 것입니다 - 경고가 아니라 오류.


5

이 질문의 한 가지 중요한 측면은 다른 대답이 아직 해결하지 못했기 때문에 기본 클래스가 생성자 내에서 가상 멤버를 호출하는 것이 안전하다는 것입니다그것이 파생 된 클래스가 기대하는 바라면. 그러한 경우, 파생 클래스의 설계자는 구축이 완료되기 전에 실행되는 모든 메소드가 상황에 따라 가능한 것처럼 현저하게 동작하도록해야합니다. 예를 들어 C ++ / CLI에서 생성자는 호출 할 코드로 싸여 있습니다.Dispose구성이 실패하면 부분적으로 구성된 객체에 부름Dispose이러한 경우 자원 유출을 막기 위해 종종 필요합니다.Dispose메소드가 실행되는 객체가 완전히 구성되지 않았을 가능성을 대비하여 메소드를 준비해야합니다.


4

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


3

경고는 가상 멤버가 파생 클래스에서 재정의 될 가능성이 있음을 상기시켜줍니다. 이 경우 부모 클래스가 가상 멤버에 한 모든 작업이 취소되거나 하위 클래스를 재정 의하여 변경됩니다. 명확성을 위해 작은 예제를보십시오.

아래의 부모 클래스는 생성자에서 가상 멤버에 값을 설정하려고 시도합니다. 그리고 이것은 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"
    }
} 


3

맹목적으로 Resharper의 조언을 따르고 수업을 봉인하는 것을 조심하십시오! EF Code First의 모델 인 경우 가상 키워드가 제거되어 관계의 지연로드가 비활성화됩니다.

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


3

한 가지 중요한 누락 된 비트는이 문제를 해결할 올바른 방법은 무엇입니까?

같이그렉은 설명했다.근본적인 문제는 파생 클래스가 생성되기 전에 기본 클래스 생성자가 가상 멤버를 호출한다는 것입니다.

다음 코드는 다음 코드에서 가져온 것입니다.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기본 클래스 생성자가DisplayStateBadBaseClass필드가 파생 생성자에 의해 아직 업데이트되지 않았기 때문입니다.

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


  • 음, DerivedFromBetter 생성자가 BetterBaseClass 생성자를 암시한다고 생각합니다. 위의 코드는 public DerivedFromBetter () : base ()와 같아야하므로 intialize가 두 번 호출됩니다. - user1778606
  • 추가 BetterBaseClass 클래스에 보호 된 생성자를 정의 할 수 있습니다.bool initialize매개 변수.Initialize기본 생성자에서 호출됩니다. 그런 다음 파생 생성자는base(false)Initialize를 두 번 호출하지 않는 방법 - Sven Vranckx
  • @ user1778606 : 절대적으로! 귀하의 관찰에 따라이를 수정했습니다. 감사! - Gustavo Mori

1

이 특별한 경우에는 C ++과 C #사이에 차이점이 있습니다. C ++에서는 객체가 초기화되지 않으므로 생성자 내부에서 virutal 함수를 호출하는 것이 안전하지 않습니다. C #에서는 클래스 객체가 만들어지면 모든 멤버가 초기화됩니다. 생성자에서 가상 함수를 호출 할 수 있지만 여전히 0 인 멤버는 액세스 할 수 있습니다. 멤버에 액세스 할 필요가 없다면 C #에서 가상 함수를 호출하는 것이 안전합니다.


  • C ++에서 생성자 내에서 가상 함수를 호출하는 것은 금지되지 않습니다. - qbeuek
  • C ++에 대해서도 동일한 인수가 있습니다. 멤버에 액세스 할 필요가 없다면 초기화되지 않았 음을 신경 쓰지 마십시오. - David Pierre
  • 아닙니다. C ++의 생성자에서 가상 메서드를 호출하면 오버라이드 된 가장 심한 구현이 아니라 현재 유형과 관련된 버전이 호출됩니다. 사실상 호출되지만 현재 클래스의 한 유형 인 것처럼 파생 클래스의 메소드 및 멤버에 액세스 할 수 없습니다. - qbeuek

1

그냥 내 생각을 추가하십시오. 정의 할 때 개인 필드를 항상 초기화하는 경우이 문제는 피해야합니다. 최소한 아래 코드는 매력처럼 작동합니다.

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


  • 생성자를 단계별로 실행하려면 디버깅을 다소 어렵게 만듭니다. - Phil1970

0

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

}


  • 해결 방법을 찾지 말고 실제 문제를 해결하십시오. - alzaimar
  • @alzaimar에 동의합니다! 비슷한 문제에 직면 해있는 사람들을위한 옵션을 남기려고하고 있으며, 위에 제시된 해결책을 구현하기를 원하지 않는 사람은 아마도 몇 가지 제한 사항 때문일 것입니다. (위의 해결 방법에서 언급했듯이) 내가 지적하고자하는 또 다른 사항은 ReSharper가 가능한 경우이 해결 방법을 오류로 표시 할 수 있어야한다는 것입니다. 그러나 현재 두 가지로 이어질 수있는 것은 아닙니다. 그들은이 시나리오를 잊어 버렸거나 지금 당장 생각할 수없는 유용한 유즈 케이스에 대해 의도적으로 외면하고 싶었습니다. - adityap

-1

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


  • 이렇게하면 경고가 사라지지만 문제가 해결되지는 않습니다. 더 파생 된 클래스를 추가 할 때 다른 사람들이 설명한 것과 같은 문제가 발생합니다. - Stefan Bormann

연결된 질문


관련된 질문

최근 질문