[c#] 재귀 생성자 호출이 잘못된 C # 코드를 컴파일하는 이유는 무엇입니까?

웨비나 Jon Skeet Inspects ReSharper를 시청 한 후 , 재귀 생성자 호출을 조금 사용하기 시작했고 다음 코드가 유효한 C # 코드라는 것을 발견했습니다 (유효하게 컴파일된다는 의미입니다).

class Foo
{
    int a = null;
    int b = AppDomain.CurrentDomain;
    int c = "string to int";
    int d = NonExistingMethod();
    int e = Invalid<Method>Name<<Indeeed();

    Foo()       :this(0)  { }
    Foo(int v)  :this()   { }
}

우리 모두가 알고 있듯이 필드 초기화는 컴파일러에 의해 생성자로 이동됩니다. 이 같은 필드가 있다면 그래서 int a = 42;, 당신은해야합니다 a = 42모든 생성자. 그러나 다른 생성자를 호출하는 생성자가있는 경우 호출 된 생성자에만 초기화 코드가 있습니다.

예를 들어, 기본 생성자를 호출하는 매개 변수가있는 생성자가 a = 42있는 경우 기본 생성자에만 할당 됩니다.

두 번째 경우를 설명하기 위해 다음 코드 :

class Foo
{
    int a = 42;

    Foo() :this(60)  { }
    Foo(int v)       { }
}

다음으로 컴파일됩니다.

internal class Foo
{
    private int a;

    private Foo()
    {
        this.ctor(60);
    }

    private Foo(int v)
    {
        this.a = 42;
        base.ctor();
    }
}

따라서 주요 문제는이 질문의 시작 부분에 제공된 내 코드가 다음과 같이 컴파일된다는 것입니다.

internal class Foo
{
    private int a;
    private int b;
    private int c;
    private int d;
    private int e;

    private Foo()
    {
        this.ctor(0);
    }

    private Foo(int v)
    {
        this.ctor();
    }
}

보시다시피 컴파일러는 필드 초기화를 배치 할 위치를 결정할 수 없으므로 결과적으로 어디에도 배치하지 않습니다. 또한 base생성자 호출 이 없습니다 . 물론 개체를 만들 수 없으며 StackOverflowException의 인스턴스를 만들려고하면 항상로 끝납니다 Foo.

두 가지 질문이 있습니다.

컴파일러가 재귀 생성자 호출을 허용하는 이유는 무엇입니까?

이러한 클래스 내에서 초기화 된 필드에 대한 컴파일러의 이러한 동작을 관찰하는 이유는 무엇입니까?


참고 사항 : ReSharperPossible cyclic constructor calls. 또한 Java에서 이러한 생성자 호출은 이벤트 컴파일되지 않으므로 Java 컴파일러는이 시나리오에서 더 제한적입니다 (Jon은 웨비나에서이 정보를 언급했습니다).

이것은 Java 커뮤니티와 관련하여 C # 컴파일러가 적어도 현대적 이기 때문에 이러한 질문을 더 흥미롭게 만듭니다 .

이것은 C # 4.0C # 5.0 컴파일러를 사용하여 컴파일 되었으며 dotPeek를 사용하여 디 컴파일 되었습니다 .



답변

흥미로운 발견.

실제로 두 종류의 인스턴스 생성자가있는 것 같습니다.

  1. 인스턴스 생성자 이은 다른 인스턴스 생성자 동일한 유형의 함께, : this( ...)구.
  2. 기본 클래스 의 인스턴스 생성자를 연결하는 인스턴스 생성자 . 여기에는 : base()기본값 이므로 chainig가 지정되지 않은 인스턴스 생성자가 포함됩니다 .

(나는 System.Object특별한 경우 의 인스턴스 생성자를 무시했습니다 . System.Object기본 클래스 System.Object가 없습니다 ! 그러나 필드도 없습니다.)

클래스에있을 수있는 인스턴스 필드 이니셜 라이저는 유형의 모든 인스턴스 생성자의 본문 시작 부분에 복사해야합니다. 2하지만 유형 1 의 인스턴스 생성자는 필드 할당 코드가 필요하지 않습니다.

따라서 C # 컴파일러 는 순환이 있는지 여부를 확인하기 위해 유형 1 의 생성자를 분석 할 필요가 없습니다.

이제 예제는 모든 인스턴스 생성자가 유형 1 인 상황을 제공합니다 . 이 경우 필드 초기화 코드를 어디에도 넣을 필요가 없습니다. 그래서 그것은 매우 깊이 분석되지 않은 것 같습니다.

모든 인스턴스 생성자가 유형 1 이면 액세스 가능한 생성자가없는 기본 클래스에서 파생 할 수도 있습니다. 하지만 기본 클래스는 봉인되지 않아야합니다. 예를 들어 private인스턴스 생성자 만있는 클래스를 작성하는 경우 파생 클래스의 모든 인스턴스 생성자를 위 의 유형 1로 만들면 사람들이 여전히 클래스에서 파생 될 수 있습니다 . 그러나 새로운 객체 생성 표현식은 물론 끝나지 않습니다. 파생 클래스의 인스턴스를 만들려면 “속임수”를 사용하고 System.Runtime.Serialization.FormatterServices.GetUninitializedObject메서드 와 같은 것을 사용해야합니다 .

또 다른 예 : System.Globalization.TextInfo클래스에는 internal인스턴스 생성자 만 있습니다. 그러나이 mscorlib.dll기술을 사용하지 않고 어셈블리에서이 클래스를 파생시킬 수 있습니다 .

마지막으로

Invalid<Method>Name<<Indeeed()

통사론. C # 규칙에 따라 다음과 같이 읽습니다.

(Invalid < Method) > (Name << Indeeed())

왼쪽 시프트 연산자 <<는보다 작음 연산자 <및 보다 큼 연산자보다 우선 순위가 높기 때문입니다 >. 후자의 두 연산자는 동일한 우선 순위를 가지므로 왼쪽 연관 규칙에 의해 평가됩니다. 유형이

MySpecialType Invalid;
int Method;
int Name;
int Indeed() { ... }

그리고는 경우 MySpecialType도입 된 (MySpecialType, int)의 과부하 operator <다음 식을

Invalid < Method > Name << Indeeed()

합법적이고 의미가 있습니다.


제 생각에는 컴파일러가이 시나리오에서 경고를 발행하면 더 좋을 것입니다. 예를 들어 unreachable code detectedIL로 변환되지 않은 필드 이니셜 라이저의 행과 열 번호를 말하고 가리킬 수 있습니다.


답변

언어 사양 은 정의되는 동일한 생성자를 직접 호출하는 것을 배제 하기 때문에 생각합니다 .

10.11.1부터 :

모든 인스턴스 생성자 (class 용 제외 object)는 constructor-body 바로 앞에 다른 인스턴스 생성자의 호출을 암시 적으로 포함합니다. 암시 적으로 호출 할 생성자는 constructor-initializer에 의해 결정됩니다.

  • 형식의 인스턴스 생성자 이니셜 라이저 this(argument-listopt) 클래스 자체의 인스턴스 생성자를 호출하도록합니다 … 인스턴스 생성자 선언에 생성자 자체를 호출하는 생성자 이니셜 라이저가 포함 된 경우 컴파일 타임 오류가 발생합니다.

마지막 문장은 컴파일 시간 오류를 생성하는 직접 호출만을 배제하는 것 같습니다.

Foo() : this() {}

불법입니다.


나는 인정한다 – 나는 그것을 허용하는 구체적인 이유를 볼 수 없다. 물론 IL 수준에서는 런타임에 다른 인스턴스 생성자를 선택할 수 있기 때문에 이러한 구문이 허용됩니다. 따라서 종료되면 재귀를 가질 수 있습니다.


이 상황에 대해 플래그를 지정하거나 경고하지 않는 또 다른 이유는 이 상황 을 감지 할 필요가 없기 때문이라고 생각합니다 . 수백 개의 다른 생성자를 쫓아가는 것을 상상해보십시오. 존재 사용 시도가 런타임에 빠르게 (아시다시피) 폭발 할 때 상당히 예외적 인 경우입니다.

각 생성자에 대해 코드 생성을 수행 할 때 고려되는 것은 constructor-initializer, 필드 이니셜 라이저 및 생성자의 본문뿐입니다. 다른 코드는 고려하지 않습니다.

  • 만약 constructor-initializer클래스 자체에 대한 인스턴스 생성자 인 필드 이니셜 라이저를 내 보내지 않고 constructor-initializer호출을 내 보낸 다음 본문을 내 보냅니다.

  • constructor-initializer직접 기본 클래스에 대한 인스턴스 생성자 인 경우 필드 이니셜 라이저, constructor-initializer호출, 본문 순으로 내 보냅니다.

두 경우 모두 다른 곳으로 갈 필요가 없습니다. 따라서 필드 이니셜 라이저를 배치 할 위치를 “불가능”하는 경우가 아닙니다. 현재 생성자 만 고려하는 몇 가지 간단한 규칙을 따르는 것입니다.


답변

당신의 예

class Foo
{
    int a = 42;

    Foo() :this(60)  { }
    Foo(int v)       { }
}

문제없이 Foo 개체를 인스턴스화 할 수 있다는 점에서 잘 작동합니다. 그러나 다음은 당신이 묻는 코드와 더 비슷할 것입니다.

class Foo
{
    int a = 42;

    Foo() :this(60)     { }
    Foo(int v) : this() { }
}

재귀가 절대 바닥을 벗어나지 않기 때문에 그와 코드 모두 스택 오버플로 (!)를 생성합니다. 따라서 코드는 실행되지 않기 때문에 무시됩니다.

즉, 컴파일러는 재귀가 절대 바닥이되지 않는다는 것을 알 수 있기 때문에 결함이있는 코드를 어디에 넣을지 결정할 수 없습니다. 한 번만 호출되는 곳에 두어야하기 때문이라고 생각하지만 생성자의 재귀 적 특성으로 인해 불가능합니다.

예를 들어 각 노드가 다른 노드를 가리키는 트리를 인스턴스화하는 데 사용할 수 있기 때문에 생성자의 본문 내에서 자신의 인스턴스를 만드는 생성자의 의미에서의 재귀는 나에게 의미가 있습니다. 그러나이 질문에 설명 된 것과 같은 사전 생성자를 통한 재귀는 절대 바닥이 될 수 없으므로 허용되지 않는 경우 나에게 의미가 있습니다.


답변

여전히 예외를 포착하고 의미있는 일을 할 수 있기 때문에 이것이 허용된다고 생각합니다.

초기화는 실행되지 않으며 거의 ​​확실하게 StackOverflowException이 발생합니다. 그러나 이것은 여전히 ​​원하는 동작 일 수 있으며 항상 프로세스가 충돌해야한다는 의미는 아닙니다.

여기에 설명 된대로 https://stackoverflow.com/a/1599236/869482


답변