재귀 생성자 호출이 잘못된 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
.
두 가지 질문이 있습니다.
컴파일러가 재귀 생성자 호출을 허용하는 이유는 무엇입니까?
이러한 클래스 내에서 초기화 된 필드에 대한 컴파일러의 이러한 동작을 관찰하는 이유는 무엇입니까?
참고 사항 : 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 인 상황을 제공합니다 . 이 경우 필드 초기화 코드를 어디에도 넣을 필요가 없습니다. 그래서 그것은 매우 깊이 분석되지 않은 것 같습니다.
It turns out that when all instance constructors are of type 1., you can even derive from a base class that has no accessible constructor. The base class must be non-sealed, though. For example if you write a class with only private
instance constructors, people can still derive from your class if they make all instance constructors in the derived class be of type 1. above. However, an new object creation expression will never finish, of course. To create instances of the derived class, one would have to "cheat" and use stuff like the System.Runtime.Serialization.FormatterServices.GetUninitializedObject
method.
Another example: The System.Globalization.TextInfo
class has only an internal
instance constructor. But you can still derive from this class in an assembly other than mscorlib.dll
with this technique.
Finally, regarding the
Invalid<Method>Name<<Indeeed()
syntax. According to the C# rules, this is to be read as
(Invalid < Method) > (Name << Indeeed())
because the left-shift operator <<
has higher precedence than both the less-than operator <
and the greater-than operator >
. The latter two operarors have the same precedence, and are therefore evaluated by the left-associative rule. If the types were
MySpecialType Invalid;
int Method;
int Name;
int Indeed() { ... }
and if the MySpecialType
introduced an (MySpecialType, int)
overload of the operator <
, then the expression
Invalid < Method > Name << Indeeed()
would be legal and meaningful.
In my opinion, it would be better if the compiler issued a warning in this scenario. For example, it could say unreachable code detected
and point to the line and column number of the field initializer that is never translated into IL.
I think because the language specification only rules out directly invoking the same constructor that is being defined.
From 10.11.1:
All instance constructors (except those for class
object
) implicitly include an invocation of another instance constructor immediately before the constructor-body. The constructor to implicitly invoke is determined by the constructor-initializer
...
- An instance constructor initializer of the form
this(
argument-list
opt
)
causes an instance constructor from the class itself to be invoked ... If an instance constructor declaration includes a constructor initializer that invokes the constructor itself, a compile-time error occurs
That last sentence seems to only preclude direct calling itself as producing a compile time error, e.g.
Foo() : this() {}
is illegal.
I admit though - I can't see a specific reason for allowing it. Of course, at the IL level such constructs are allowed because different instance constructors could be selected at runtime, I believe - so you could have recursion provided it terminates.
I think the other reason it doesn't flag or warn on this is because it has no need to detect this situation. Imagine chasing through hundreds of different constructors, just to see if a cycle does exist - when any attempted usage will quickly (as we know) blow up at runtime, for a fairly edge case.
When it's doing code generation for each constructor, all it considers is constructor-initializer
, the field initializers, and the body of the constructor - it doesn't consider any other code:
If
constructor-initializer
is an instance constructor for the class itself, it doesn't emit the field initializers - it emits theconstructor-initializer
call and then the body.If
constructor-initializer
is an instance constructor for the direct base class, it emits the field initializers, then theconstructor-initializer
call, and then then body.
In neither case does it need to go looking elsewhere - so it's not a case of it being "unable" to decide where to place the field initializers - it's just following some simple rules that only consider the current constructor.
Your example
class Foo
{
int a = 42;
Foo() :this(60) { }
Foo(int v) { }
}
will work fine, in the sense that you can instantiate that Foo object without problems. However, the following would be more like the code that you're asking about
class Foo
{
int a = 42;
Foo() :this(60) { }
Foo(int v) : this() { }
}
Both that and your code will create a stackoverflow (!), because the recursion never bottoms out. So your code is ignored because it never gets to execute.
In other words, the compiler can't decide where to put the faulty code because it can tell that the recursion never bottoms out. I think this is because it has to put it where it will only be called once, but the recursive nature of the constructors makes that impossible.
Recursion in the sense of a constructor creating instances of itself within the body of the constructor makes sense to me, because e.g. that could be used to instantiate trees where each node points to other nodes. But recursion via the pre-constructors of the sort illustrated by this question can't ever bottom out, so it would make sense for me if that was disallowed.
I think this is allowed because you can (could) still catch the Exception and do something meaningfull with it.
The initialisation will never be run, and it will almost certaintly throw a StackOverflowException. But this can still be wanted behaviour, and didn't always mean the process should crash.
As explained here https://stackoverflow.com/a/1599236/869482
'program tip' 카테고리의 다른 글
SBT 및 IntelliJ IDEA로 여러 상호 의존적 모듈을 관리하는 방법은 무엇입니까? (0) | 2020.10.06 |
---|---|
CSS에서 텍스트와 선택 상자를 같은 너비로 정렬 하시겠습니까? (0) | 2020.10.06 |
드로어 블 -hdpi, 드로어 블 -mdpi, 드로어 블 -ldpi Android (0) | 2020.10.06 |
AlarmManager가 여러 장치에서 작동하지 않음 (0) | 2020.10.06 |
Eclipse에서 자동 완성을 트리거하는 스페이스 바 키 누르기를 중지합니다. (0) | 2020.10.06 |