Özyinelemeli bir kurucu çağrısı neden geçersiz C # kodu derlemesi yapar?


82

Jon Skeet Inspects ReSharper adlı web seminerini izledikten sonra , yinelemeli kurucu çağrıları ile biraz oynamaya başladım ve aşağıdaki kodun geçerli C # kodu olduğunu (geçerli derken derlediğini kastediyorum) buldum.

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

Muhtemelen hepimizin bildiği gibi, alan başlatma derleyici tarafından kurucuya taşınır. Yani bir alanınız int a = 42;varsa a = 42, tüm kurucularda olacaktır. Ancak, başka bir kurucuyu çağıran bir kurucunuz varsa, yalnızca çağrıda başlatma koduna sahip olacaksınız.

Örneğin, varsayılan kurucuyu çağıran parametrelere sahip bir kurucunuz varsa, atama a = 42yalnızca varsayılan kurucuda olacaktır.

İkinci durumu göstermek için sonraki kod:

class Foo
{
    int a = 42;

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

Aşağıdakileri derler:

internal class Foo
{
    private int a;

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

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

Yani asıl sorun, bu sorunun başında verilen kodumun şu şekilde derlenmiş olmasıdır:

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

Gördüğünüz gibi, derleyici alan başlatmayı nereye koyacağına karar veremez ve sonuç olarak onu hiçbir yere koymaz. Ayrıca baseyapıcı çağrılarının olmadığını unutmayın . Elbette, hiçbir nesne oluşturulamaz ve StackOverflowExceptionbir örnek oluşturmaya çalışırsanız her zaman sonuçta kalırsınız Foo.

İki sorum var:

Derleyici neden yinelemeli yapıcı çağrılarına izin veriyor?

Neden böyle bir sınıf içinde başlatılan alanlar için derleyicinin bu tür davranışını gözlemliyoruz?


Bazı notlar: ReSharper sizi uyarıyor Possible cyclic constructor calls. Dahası, Java'da bu tür yapıcı çağrıları olay derlenmez, bu nedenle Java derleyicisi bu senaryoda daha kısıtlayıcıdır (Jon bu bilgiden web seminerinde bahsetmiştir).

Bu, bu soruları daha ilginç hale getirir, çünkü Java topluluğu ile ilgili olarak, C # derleyicisi en azından daha moderndir.

Bu, C # 4.0 ve C # 5.0 derleyicileri kullanılarak derlendi ve dotPeek kullanılarak derlendi .


3
Bu videoyu nasıl özledim ???
Royi Namir

7
Harika soru.
Dennis

2
Güzel alan başlatıcıları var: int a = null; int b = AppDomain.CurrentDomain; int c = "string to int"; int d = NonExistingMethod(); int e = Invalid<Method>Name<<Indeeed();Bir test yapmalı: "Bu alan bildirimleri hangi durumda uygun?" (Kullanılmayan alanlarla ilgili bir uyarı var, ancak bu uyarıdan, inans kurucularından birinin (veya başka bir yerin) gövdesindeki her alanı okuyarak kurtulabilirsiniz.)
Jeppe Stig Nielsen,

4
Buna da aynı nedenle izin verildiğine inanıyorum .
GSerg

4
Alan başlatma, bir temel oluşturucu çağıran tüm oluşturuculara yerleştirilir. Sonuç olarak, bir temel oluşturucuyu çağıran hiçbir kurucu yoksa, alan başlatma herhangi bir yere konmaz. En azından bu kısım bana çok mantıklı geliyor. Derleyici onu nereye koyacağını bilemediği için değil, çünkü derleyici onu herhangi bir yere koymak zorunda olmadığını fark ediyor .

Yanıtlar:


11

İlginç bul.

Görünüşe göre gerçekten sadece iki tür örnek oluşturucu var:

  1. Sözdizimi ile aynı türden başka bir örnek oluşturucuyu zincirleyen bir örnek oluşturucu : this( ...).
  2. Temel sınıfın bir örnek oluşturucusunu zincirleyen bir örnek oluşturucu . Bu : base(), varsayılan olduğundan , hiçbir chainig'in belirtilmediği örnek oluşturucuları içerir .

( System.ObjectÖzel bir durum olan örnek kurucusunu göz ardı ettim . System.ObjectTaban sınıfı yok! Ama System.Objectalan da yok.)

Sınıfta mevcut olabilecek örnek alan başlatıcılarının, yukarıdaki 2. türdeki tüm örnek oluşturucuların gövdesinin başlangıcına kopyalanması gerekirken, 1. türden örnek oluşturucuların alan atama koduna ihtiyacı yoktur .

Öyleyse görünüşe göre C # derleyicisinin döngü olup olmadığını görmek için tip 1'in kurucularının bir analizini yapmasına gerek yok.

Şimdi örneğiniz, tüm örnek oluşturucuların tip 1 olduğu bir durumu veriyor . Bu durumda alan başlatıcı kodunun herhangi bir yere konmasına gerek yoktur. Öyle görünüyor ki çok derinlemesine analiz edilmiyor.

Tüm örnek oluşturucular tip 1 olduğunda , erişilebilir bir kurucuya sahip olmayan bir temel sınıftan bile türetebileceğiniz ortaya çıktı. Yine de temel sınıf mühürlenmemiş olmalıdır. Örneğin, yalnızca privateörnek oluşturucularla bir sınıf yazarsanız , türetilmiş sınıftaki tüm örnek oluşturucularını yukarıdaki 1. türden yaparlarsa insanlar yine de sınıfınızdan türetebilirler . Bununla birlikte, yeni bir nesne oluşturma ifadesi elbette asla bitmeyecek. Türetilmiş sınıfın örneklerini oluşturmak için, "hile yapmak" ve System.Runtime.Serialization.FormatterServices.GetUninitializedObjectyöntem gibi şeyler kullanmak gerekir .

Başka bir örnek: System.Globalization.TextInfoSınıfın yalnızca bir internalörnek oluşturucusu vardır. Ancak yine mscorlib.dllde bu teknikten başka bir montajda bu sınıftan türetebilirsiniz .

Son olarak,

Invalid<Method>Name<<Indeeed()

sözdizimi. C # kurallarına göre, bu şu şekilde okunmalıdır:

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

Sol-kaydırma operatörü için <<daha azını operatörü hem daha yüksek önceliğe sahiptir <ve üçten fazla operatör >. Son iki işleç aynı önceliğe sahiptir ve bu nedenle sol-ilişkisel kural tarafından değerlendirilir. Türler olsaydı

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

ve eğer MySpecialTypebir (MySpecialType, int)aşırı yük getirilmişse operator <, o zaman ifade

Invalid < Method > Name << Indeeed()

yasal ve anlamlı olacaktır.


Kanımca, derleyicinin bu senaryoda bir uyarı yayınlaması daha iyi olur. Örneğin unreachable code detected, hiçbir zaman IL'ye çevrilmeyen alan başlatıcının satır ve sütun numarasını söyleyebilir ve işaret edebilir.


1
Anlamıyorum ... ctor'dan önce alan somutlaştırması çağrılmıyor mu?
Royi Namir

2
@RoyiNamir Evet. Ancak IL'ye bakarsanız, soruyu soranın yazdığı gibi çalışır: "Hepimizin bildiği gibi, alan başlatma derleyici tarafından kurucuya taşınır." Bununla kastedilen, bu sınıfı C #: olarak yazdığınızı varsayalım class Example { int field = 42; internal Example() { /* some code here */ field = 100; } }, sonra onun ürettiği bilgi okulu, 42atamayı her şeyden önce örnek kurucusuna koyar , tıpkı sizin class Example { int field; internal Example() { field = 42; /* some code here */ field = 100; } }
yazmışsınız gibi

5

Sanırım dil özelliği yalnızca tanımlanmakta olan aynı kurucuya doğrudan başvurmayı dışlıyor.

10.11.1'den itibaren:

Tüm örnek oluşturucular (sınıf için olanlar hariç object), yapıcı gövdesinden hemen önce başka bir örnek oluşturucunun çağrılmasını örtük olarak içerir. Örtük olarak çağrılacak yapıcı, yapıcı başlatıcı tarafından belirlenir

...

  • Formun bir örnek oluşturucu başlatıcısı , sınıfın kendisinden bir örnek oluşturucunun çağrılmasına neden olur ... Bir örnek oluşturucu bildirimi, yapıcının kendisini çağıran bir yapıcı başlatıcı içeriyorsa, bir derleme zamanı hatası oluşurthis(argument-listopt)

Bu son cümle, bir derleme zamanı hatası oluşturduğu için kendisini doğrudan çağırmayı yalnızca engelliyor gibi görünüyor, örneğin

Foo() : this() {}

yasa dışıdır.


Yine de itiraf ediyorum - buna izin vermek için belirli bir neden göremiyorum. Elbette, IL düzeyinde bu tür yapılara izin verilir, çünkü çalışma zamanında farklı örnek oluşturucular seçilebilir, inanıyorum - bu nedenle sona erdirilmesi koşuluyla özyinelemeye sahip olabilirsiniz.


Bence bunu işaret etmemesinin veya uyarmamasının bir diğer nedeni de bu durumu tespit etmeye gerek olmaması . Bir döngü sadece görmek için, farklı kurucular yüzlerce kovalayan düşünün yapar (bildiğimiz gibi) herhangi teşebbüs kullanımı hızla oldukça kenar durum için, çalışma zamanında havaya uçurmak ne zaman - mevcuttur.

Her kurucu için kod üretimi yaparken, tek düşündüğü constructor-initializeralan başlatıcıları ve kurucunun gövdesidir - başka herhangi bir kodu dikkate almaz:

  • Eğer constructor-initializersınıfın kendisi için bir örnek yapıcısı, bu alan başlatıcıları yaymaz - bu yayar constructor-initializersonra aramayı ve gövde.

  • Eğer constructor-initializerdoğrudan temel sınıf için bir örnek yapıcısı, bu alan başlatıcıları, sonra yayar constructor-initializerçağrıyı ve sonra o vücudu.

Her iki durumda da başka bir yere bakması gerekmez - bu yüzden alan başlatıcılarını nereye yerleştireceğine karar verememe durumu değildir - sadece mevcut kurucuyu dikkate alan bazı basit kuralları izler.


2
Ama ya bu derleme gibi çizgiler sağlayan gerçeği hakkında: int e = Invalid<Method>Name<<Indeeed();. Bunun bir derleyici hatası olduğunu söylüyorum.
Matthew Watson

@MatthewWatson Bu int e = Invalid < Method > Name << Indeed();ikili operatörler "küçüktür", "büyüktür" ve "sola kaydırma" olarak yorumlanabilir. Bu sözdizimsel olarak tamam, ancak güçlü yazımla bunu düzeltmek için operatörlerin gerçekten çılgınca aşırı yüklemeleri olurdu.
Jeppe Stig Nielsen

1
@JeppeStigNielsen Aye, ancak kodu yinelemeli kurucu kodunu kaldırmak dışında aynı şekilde bırakırsanız derlenmez. Bu yüzden bunun bir hata olduğunu düşünüyorum.
Matthew Watson

4
@MatthewWatson Hata, sınıf tamamlanmamış olduğundan ayrıştırma zamanında tespit edilemiyor. (Belki sınıfınız, Invalidonu geçerli kılacak etc adı verilen üyeleri tanımlayacaktır .) Hata normalde kod üretilirken algılanır, ancak hiç oluşturulmayan bir kod yazmanın bir yolunu buldunuz. Derleyicide sinsi bir delik buldunuz (asla derlenmeyecek bir kod yazmanın bir yolu), ancak sorun teşkil eden koda zaten ulaşılamadığı için ciddi bir delik bulamadınız.
Raymond Chen

2

Senin örnek

class Foo
{
    int a = 42;

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

bu Foo nesnesini sorunsuz bir şekilde somutlaştırabilmeniz açısından iyi çalışacaktır. Ancak, aşağıdakiler daha çok sorduğunuz koda benzeyecektir

class Foo
{
    int a = 42;

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

Hem bu hem de kodunuz bir yığın aşımı (!) Yaratacaktır, çünkü özyineleme asla dibe vurmaz. Yani kodunuz göz ardı edilir çünkü asla çalıştırılamaz.

Diğer bir deyişle, derleyici hatalı kodu nereye koyacağına karar veremez çünkü özyinelemenin asla dibe vurmadığını söyleyebilir. Sanırım bunun nedeni, onu yalnızca bir kez çağrılacağı yere koymak zorunda olması, ancak kurucuların yinelemeli doğası bunu imkansız kılıyor.

Yapıcının gövdesi içinde kendisinin örneklerini yaratan bir kurucu anlamında yineleme bana mantıklı geliyor, çünkü örneğin bu, her düğümün diğer düğümleri gösterdiği ağaçları somutlaştırmak için kullanılabilir. Ancak bu soruyla gösterilen türden ön kurucular aracılığıyla yineleme hiçbir zaman dip yapamaz, bu yüzden buna izin verilmemesi benim için mantıklı olacaktır.


1
Evet, katılıyorum, bu yüzden bu soruyu oluşturdum. Derleyici neden başlatma mantığını nereye koyacağına karar veremiyor ve bu nedenle neden yinelemeli çağrılara izin veriyor? Bunun bir sebebi var mı?
Ilya Ivanov

Bana öyle geliyor ki, derleyici hatalı kodu nereye koyacağına karar veremiyor çünkü özyinelemenin asla dibe vurmadığını söyleyebilir. Bu neden bir gizem?
Stokastik olarak

C # hangi yöntemin çağrılacağına karar veremezse, bir hata atar ambiguous method call, böyle bir yöntem çağrısını atlamaz. Derleyici olursam, bu senaryoda da hata veririm.
Ilya Ivanov

1
Kötü, yanıtların çok fazla olumsuz oy alması, hiçbirini olumsuz oylamıyorum (sadece durum böyledir). Bu senaryoda, başlatma mantığını nereye koyacağına da karar veremez. Öyleyse asıl sorum neden yinelemeli aramalara izin vermem gerektiği? Bunun arkasında bir sebep var mı? Belki bir şeyi kaçırıyorum
Ilya Ivanov

3
@IlyaIvanov - Bence daha uygun soru - derleyicide yinelemeli kurucu çağrılarını algılamak için neden bir döngü algılayıcısı yazasınız ?
Damien_The_Unbeliever

0

Sanırım buna izin verildi çünkü İstisnayı hala yakalayabilir ve onunla anlamlı bir şey yapabilirsiniz.

Başlatma hiçbir zaman çalıştırılmayacak ve neredeyse kesin olarak bir StackOverflowException oluşturacaktır. Ancak bu yine de istenen bir davranış olabilir ve her zaman sürecin çökmesi gerektiği anlamına gelmez.

Burada açıklandığı gibi https://stackoverflow.com/a/1599236/869482

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.