웨비나 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
.
두 가지 질문이 있습니다.
컴파일러가 재귀 생성자 호출을 허용하는 이유는 무엇입니까?
이러한 클래스 내에서 초기화 된 필드에 대한 컴파일러의 이러한 동작을 관찰하는 이유는 무엇입니까?
참고 사항 : ReSharper 는 Possible cyclic constructor calls
. 또한 Java에서 이러한 생성자 호출은 이벤트 컴파일되지 않으므로 Java 컴파일러는이 시나리오에서 더 제한적입니다 (Jon은 웨비나에서이 정보를 언급했습니다).
이것은 Java 커뮤니티와 관련하여 C # 컴파일러가 적어도 현대적 이기 때문에 이러한 질문을 더 흥미롭게 만듭니다 .
이것은 C # 4.0 및 C # 5.0 컴파일러를 사용하여 컴파일 되었으며 dotPeek를 사용하여 디 컴파일 되었습니다 .
답변
흥미로운 발견.
실제로 두 종류의 인스턴스 생성자가있는 것 같습니다.
- 인스턴스 생성자 이은 다른 인스턴스 생성자 동일한 유형의 함께,
: this( ...)
구. - 기본 클래스 의 인스턴스 생성자를 연결하는 인스턴스 생성자 . 여기에는
: 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 detected
IL로 변환되지 않은 필드 이니셜 라이저의 행과 열 번호를 말하고 가리킬 수 있습니다.
답변
언어 사양 은 정의되는 동일한 생성자를 직접 호출하는 것을 배제 하기 때문에 생각합니다 .
10.11.1부터 :
모든 인스턴스 생성자 (class 용 제외
object
)는 constructor-body 바로 앞에 다른 인스턴스 생성자의 호출을 암시 적으로 포함합니다. 암시 적으로 호출 할 생성자는 constructor-initializer에 의해 결정됩니다.
…
- 형식의 인스턴스 생성자 이니셜 라이저
this(
argument-list
opt
)
클래스 자체의 인스턴스 생성자를 호출하도록합니다 … 인스턴스 생성자 선언에 생성자 자체를 호출하는 생성자 이니셜 라이저가 포함 된 경우 컴파일 타임 오류가 발생합니다.
마지막 문장은 컴파일 시간 오류를 생성하는 직접 호출만을 배제하는 것 같습니다.
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