Neden .NET'te bir yapı için varsayılan bir yapıcı tanımlayamıyorum?


261

.NET'te, bir değer türü (C # struct) parametresiz bir yapıcıya sahip olamaz. Göre bu yazı bu CLI özellikleri tarafından şart koşulmaktadır. Her değer türü için, tüm üyeleri sıfır (veya null) olarak başlatan varsayılan bir kurucu (derleyici tarafından?) Oluşturulur .

Neden böyle bir varsayılan kurucu tanımlamaya izin verilmiyor?

Önemsiz bir kullanım rasyonel sayılar içindir:

public struct Rational {
    private long numerator;
    private long denominator;

    public Rational(long num, long denom)
    { /* Todo: Find GCD etc. */ }

    public Rational(long num)
    {
        numerator = num;
        denominator = 1;
    }

    public Rational() // This is not allowed
    {
        numerator = 0;
        denominator = 1;
    }
}

Geçerli C # sürümünü kullanarak, varsayılan Rational 0/0o kadar güzel değil.

Not : Varsayılan parametreler bunu C # 4.0 için çözmeye yardımcı olur mu veya CLR tanımlı varsayılan kurucu çağrılır mı?


Jon Skeet yanıtladı:

Örneğinizi kullanmak için, birisi yaptığında ne olmak istersiniz:

 Rational[] fractions = new Rational[1000];

Yapıcıdan 1000 kez geçmeli mi?

Elbette, bu yüzden varsayılan kurucuyu ilk başta yazdım. Herhangi bir açık varsayılan kurucu tanımlanmadığında CLR varsayılan sıfırlama yapıcısını kullanmalıdır ; bu şekilde yalnızca kullandığınız kadar ödersiniz. Sonra 1000 varsayılan olmayan Rationals bir kapsayıcı istiyorsanız (ve 1000 yapıları optimize etmek istiyorsanız) List<Rational>bir dizi yerine bir kullanacağım .

Bu nedenle, bence, varsayılan bir kurucu tanımını engelleyecek kadar güçlü değil.


3
+1 bir kez benzer bir problem yaşadı, sonunda yapıyı bir sınıfa dönüştürdü.
Dirk Vollmar

4
C # 4'teki varsayılan parametreler yardımcı olamaz çünkü Rational()parametresiz ctor yerine Rational(long num=0, long denom=1).
LaTeX

6
O Not C # 6.0 Visual Studio 2015 ile birlikte gelen iki yapı için sıfır parametre örneği kurucular yazmak için izin verilecektir. Yani new Rational()eğer varsa böyle bir şey yok, ancak eğer, yapıcı çağıracağı new Rational()eşit olacaktır default(Rational). Her durumda, sözdizimini, yapınızın default(Rational)"sıfır değeri" ni (önerilen tasarımınızla birlikte "kötü" bir sayıdır Rational) istediğinizde kullanmanız önerilir . Bir değer türü için varsayılan değer Ther zaman değerdir default(T). Bu yüzden new Rational[1000]asla yapı kurucularını çağırmayacaksınız.
Jeppe Stig Nielsen

6
Bu özel sorunu çözmek için denominator - 1, yapı içinde saklayabilirsiniz , böylece varsayılan değer 0/1 olur
miniBill

3
Then if I want a container of 1000 non-default Rationals (and want to optimize away the 1000 constructions) I will use a List<Rational> rather than an array.Neden bir dizinin bir yapı için Listeye farklı bir kurucu çağırmasını beklersiniz?
mjwills

Yanıtlar:


197

Not: Aşağıdaki cevap, C # 6'dan uzun bir süre önce yazılmıştır, bu da yapılarda parametresiz kurucuları bildirme yeteneğini tanıtmayı planlamaktadır - ancak yine de her durumda (örneğin dizi oluşturma için) çağrılmayacaktır (sonunda) bu özellik C # 6'ya eklenmedi ).


EDIT: Grauenwolf'un CLR ile ilgili anlayışından dolayı aşağıdaki cevabı düzenledim.

CLR, değer türlerinin parametresiz yapıcılara sahip olmasına izin verir, ancak C # içermez. Bunun, kurucunun istemediği zaman çağrılması beklentisini getireceğine inanıyorum. Örneğin, şunu düşünün:

MyStruct[] foo = new MyStruct[1000];

CLR bunu sadece uygun belleği ayırarak ve hepsini sıfırlayarak çok verimli bir şekilde yapabilir. MyStruct yapıcısını 1000 kez çalıştırmak zorunda kalsaydı, bu çok daha az verimli olurdu. (- eğer Aslında, öyle değil yapmak parametresiz yapıcı olması size bir dizi oluşturmak çalıştırılamamasını almaz, veya bir başlatılmamış örnek değişken olduğunda.)

C # 'daki temel kural "herhangi bir tür için varsayılan değer herhangi bir başlatmaya güvenemez" dir. Şimdi parametresiz kurucuların tanımlanmasına izin verebilirlerdi , ancak daha sonra bu kurucunun tüm durumlarda yürütülmesini gerektirmeyebilirlerdi - ama bu daha fazla karışıklığa neden olurdu. (Ya da en azından argüman gittiğine inanıyorum.)

DÜZENLEME: Örneğinizi kullanmak için, birisi yaptığında ne olmak istersiniz:

Rational[] fractions = new Rational[1000];

Yapıcıdan 1000 kez geçmeli mi?

  • Değilse, 1000 geçersiz gerekçeyle sonuçlanırız
  • Eğer öyleyse, diziyi gerçek değerlerle doldurmak üzereysek, potansiyel olarak bir iş yükünü boşa harcadık.

EDIT: (Sorunun biraz daha cevap) Parametresiz yapıcı derleyici tarafından oluşturulmamış. Değer türlerinin CLR söz konusu olduğunda yapıcılara sahip olması gerekmez - IL'de yazarsanız ortaya çıkabilir . Eğer "yazdığınızda new Guid()" C # normal bir yapıcısı ararsanız ne elde etmek yayar farklı IL söyledi. Bu açıdan biraz daha fazla bilgi için bu SO sorusuna bakın .

Ben şüpheli parametresiz kurucular ile bir çerçevede herhangi bir değer türleri olmadığını. Şüphesiz NDepend bana yeterince iyi sorduğumu söyleyebilirdi ... C # 'ın bunu yasaklaması gerçeği muhtemelen kötü bir fikir olduğunu düşünmek için yeterince büyük bir ipucu.


9
Daha kısa açıklama: C ++ 'da, yapı ve sınıf aynı madalyonun sadece iki yüzüdür. Tek gerçek fark, biri varsayılan olarak herkese açık ve diğeri özeldi. .Net'te, bir yapı ve sınıf arasında çok daha büyük bir fark vardır ve onu anlamak önemlidir.
Joel Coehoorn

38
@Joel: Bu özel kısıtlamayı gerçekten açıklamıyor, değil mi?
Jon Skeet

6
CLR, değer türlerinin parametresiz yapıcılara sahip olmasına izin verir. Ve evet, bunu bir dizideki her öğe için çalıştıracaktır. C #, bunun kötü bir fikir olduğunu ve buna izin vermediğini düşünüyor, ancak bunu yapan bir .NET dili yazabilirsiniz.
Jonathan Allen

2
Üzgünüm, aşağıdakilerle biraz kafam karıştı. Does Rational[] fractions = new Rational[1000];eğer aynı zamanda işin bir yük atık Rationalsınıf bir yapı yerine nedir? Eğer öyleyse, sınıfların neden varsayılan bir kralı vardır?
Koltuk altı öpücüğüm

5
@ FifaEarthCup2014: "İş yükünü boşa harcamak" ile kastettiğiniz hakkında daha spesifik olmalısınız. Ama her iki durumda da, yapıcıyı 1000 kez çağırmayacak. Bir Rationalsınıfsa, 1000 null referanstan oluşan bir dizi elde edersiniz.
Jon Skeet

48

Bir yapı bir değer türüdür ve bir değer türü bildirilir bildirilmez varsayılan bir değere sahip olmalıdır.

MyClass m;
MyStruct m2;

İki alanı da örneklemeden yukarıdaki gibi mbildirirseniz , hata ayıklayıcıyı kırın, boş olur, ancak m2olmaz. Bu göz önüne alındığında, parametresiz bir kurucu hiçbir anlam ifade etmeyecektir, aslında bir yapıdaki herhangi bir kurucu değer atamaktır, şey sadece kendisini bildirerek var olur. Gerçekten de m2, yukarıdaki örnekte oldukça mutlu bir şekilde kullanılabilir ve eğer varsa yöntemleri ve alanları ve özellikleri manipüle edilebilir!


3
Birinin sana neden oy verdiğinden emin değilim. Buradaki en doğru cevap gibi görünüyorsun.
pipTheGeek

12
C ++ davranışı, bir türün varsayılan bir yapıcısı varsa, bu tür bir nesne açık bir kurucu olmadan oluşturulduğunda kullanılır. Bu, C # 'da m2'yi varsayılan kurucu ile başlatmak için kullanılmış olabilir, bu yüzden bu cevap faydalı değildir.
Motti

3
onester: Yapıların bildirildiğinde kendi kurucularını çağırmasını istemiyorsanız, böyle bir varsayılan kurucu tanımlamayın! :) Motti'nin sözleri
Stefan Monov

8
@Tarik. Katılmıyorum. Aksine, parametresiz bir kurucu tam anlamıyla mantıklı olacaktır: Her zaman varsayılan değer olarak bir kimlik matrisine sahip bir "Matrix" yapısı oluşturmak istersem, bunu başka yollarla nasıl yapabilirsiniz?
Elo

1
"Gerçekten m2 oldukça mutlu bir şekilde kullanılabilir .." ile tamamen katılıyorum emin değilim . Önceki bir C # için doğru olabilirdi, ancak bir yapıyı bildirmek için bir derleyici hatası, değil , daha sonra üyelerini kullanmaya çalışınnew
Caius Jard

18

CLR buna izin verse de, C # yapıların parametresiz varsayılan yapıcısına sahip olmasına izin vermez. Bunun nedeni, bir değer türü için derleyicilerin varsayılan olarak ne varsayılan bir kurucu ne de varsayılan kurucuya çağrı oluşturmalarıdır. Bu nedenle, varsayılan bir kurucu tanımlasanız bile, çağrılmaz ve bu sadece sizi karıştırır.

Bu tür sorunları önlemek için, C # derleyicisi kullanıcı tarafından varsayılan bir yapıcı tanımına izin vermez. Varsayılan bir kurucu oluşturmadığı için alanları tanımlarken başlatamazsınız.

Ya da en büyük neden, bir yapının bir değer türü olması ve değer türlerinin varsayılan bir değerle başlatılması ve yapıcı, başlatma için kullanılmasıdır.

newAnahtar kelimeyle yapınızı somutlaştırmanız gerekmez . Bunun yerine bir int gibi çalışır; doğrudan erişebilirsiniz.

Yapılar açık parametresiz kurucular içeremez. Yapı üyeleri otomatik olarak varsayılan değerlerine sıfırlanır.

Bir yapı için varsayılan (parametresiz) bir yapıcı, beklenmeyen davranış olan tamamen sıfırlanmış durumdan farklı değerler ayarlayabilir. .NET çalışma zamanı bu nedenle bir yapı için varsayılan kurucuları yasaklar.


Bu cevap açık ara en iyisidir. Sonuçta kısıtlamanın amacı, MyStruct s;sağladığınız varsayılan kurucuyu çağırmamak gibi sürprizlerden kaçınmaktır .
talles

1
Açıklama için teşekkürler. Bu yüzden iyileştirilmesi gereken sadece bir derleyici eksikliğidir, parametresiz kurucuları yasaklamak için teorik olarak iyi bir neden yoktur (sadece erişim özellikleriyle kısıtlanabildiği anda).
Elo

16

Varsayılan bir "rasyonel" sayı başlatan ve döndüren statik bir özellik yapabilirsiniz:

public static Rational One => new Rational(0, 1); 

Ve şöyle kullanın:

var rat = Rational.One;

24
Bu durumda, Rational.Zerobiraz daha az kafa karıştırıcı olabilir.
Kevin

13

Daha kısa açıklama:

C ++ 'da, yapı ve sınıf aynı madalyonun sadece iki yüzüdür. Tek gerçek fark, birinin varsayılan olarak herkese açık ve diğerinin özel olmasıdır.

In .NET , bir yapı ve bir sınıf arasındaki çok daha büyük bir fark vardır. Ana şey, yapı değer türü anlambilim sağlarken sınıf referans türü anlambilim sağlar. Bu değişikliğin sonuçları hakkında düşünmeye başladığınızda, tanımladığınız yapıcı davranışı da dahil olmak üzere diğer değişiklikler de daha anlamlı olmaya başlar.


7
Bunun, değer ve referans türü ayrımı ile nasıl ima edildiği konusunda biraz daha açık olmalısınız ...
Motti

Değer türlerinin varsayılan bir değeri vardır; bir kurucu tanımlamasanız bile bunlar null değildir. İlk bakışta bu, varsayılan bir kurucu tanımlamayı da engellemese de, yapılar hakkında belirli varsayımlar yapmak için bu özelliği dahili olarak kullanan çerçeve.
Joel Coehoorn

@annakata: Yansıtma ile ilgili bazı senaryolarda diğer kurucular muhtemelen yararlıdır. Ayrıca, jenerikler parametreli bir "yeni" kısıtlamaya izin verecek şekilde geliştirildiyse, bunlara uygun yapılara sahip olmak yararlı olacaktır.
supercat

@annakata C # newbir yapıcı çağırmak için gerçekten yazılması gereken belirli bir güçlü gereksinimi olduğundan inanıyorum . C ++ 'da yapıcıları, dizilerin bildiriminde veya örneğinde gizli şekillerde çağrılır. C # 'da her şey bir işaretçidir, bu yüzden null ile başlayın, ya bir yapıdır ve bir şeyle başlamalıdır, ancak yazamadığınızda new... (dizi init gibi), bu güçlü bir C # kuralını kırar.
v.oddou

3

Vereceğim geç çözüme eşdeğer görmedim, işte burada.

değerleri varsayılan 0'dan istediğiniz herhangi bir değere taşımak için ofsetleri kullanın. burada alanlara doğrudan erişmek yerine özellikler kullanılmalıdır. (belki olası c # 7 özelliğiyle, özellik kapsamındaki alanları daha iyi tanımlarsınız, böylece kodda doğrudan erişilmeye karşı korunurlar.)

Bu çözüm, yalnızca değer türlerine sahip basit yapılar için çalışır (ref türü veya boş değerli yapı yoktur).

public struct Tempo
{
    const double DefaultBpm = 120;
    private double _bpm; // this field must not be modified other than with its property.

    public double BeatsPerMinute
    {
        get => _bpm + DefaultBpm;
        set => _bpm = value - DefaultBpm;
    }
}

Bu farklı daha bu yaklaşım özel kasa ama artık burası aralıkları için çalışacak olan ofset kullanarak değil, bu cevap.

alan olarak numaralandırmalarla örnek.

public struct Difficaulty
{
    Easy,
    Medium,
    Hard
}

public struct Level
{
    const Difficaulty DefaultLevel = Difficaulty.Medium;
    private Difficaulty _level; // this field must not be modified other than with its property.

    public Difficaulty Difficaulty
    {
        get => _level + DefaultLevel;
        set => _level = value - DefaultLevel;
    }
}

Dediğim gibi, bu hile her durumda çalışmayabilir, yapı sadece değer alanlarına sahip olsa bile, sadece sizin durumunuzda çalışıyorsa ya da çalışmıyorsa bunu bilirsiniz. sadece inceleyin. ama genel fikri anlıyorsunuz.


Bu verdiğim örnek için iyi bir çözüm ama gerçekten sadece bir örnek olması gerekiyordu, soru genel.
Motti

2

Sadece özel durum. 0 rakamı ve 0 paydası görürseniz, gerçekten istediğiniz değerlere sahipmiş gibi davranın.


5
Şahsen sınıflarımın / yapılarımın bu tür davranışlara sahip olmasını istemiyorum. Sessizce başarısız olmak (veya geliştiricinin sizin için en iyi olduğu şekilde iyileşmek) yakalanmamış hatalara giden yoldur.
Boris Callens

2
+1 Bu iyi bir yanıttır, çünkü değer türleri için varsayılan değerlerini dikkate almanız gerekir. Bu, davranışı ile varsayılan değeri "ayarlamanıza" izin verir.
IllidanS4 Monica'nın

Nullable<T>(Ör. int?) Gibi sınıfları tam olarak böyle uygularlar .
Jonathan Allen

Bu çok kötü bir fikir. 0/0 her zaman geçersiz bir kesir (NaN) olmalıdır. Birisi new Rational(x,y)x ve y'nin 0 olduğu yeri ararsa ne olur?
Mike Rosoft

Gerçek bir kurucunuz varsa, gerçek bir 0/0 oluşmasını önleyerek bir istisna atabilirsiniz. Ya da olmasını istiyorsanız, varsayılan ile 0/0 arasında ayrım yapmak için ekstra bir bool eklemeniz gerekir.
Jonathan Allen

2

Ne kullanıyorum böyle bir destek alanı ile birlikte null birleştirme operatörü (??) :

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

  public SomeRefType MyRefVariable {
    get { return m_MyRefVariableBackingField ?? (m_MyRefVariableBackingField = new SomeRefType()); }
  }
}

Bu yardımcı olur umarım ;)

Not: null birleştirme ataması şu anda C # 8.0 için bir özellik önerisidir.


1

C # kullandığınız için varsayılan bir yapıcı tanımlayamazsınız.

Yapılar .NET destekleyen varsayılan yapıcılar olabilir, ancak onu destekleyen herhangi bir dil bilmiyorum.


C # 'da, sınıflar ve yapılar anlamsal olarak farklıdır. Bir sınıf bir değer türüdür, sınıf ise bir referans türüdür.
Tom Sarduy

-1

İşte hiçbir varsayılan yapıcı ikilemi için benim çözüm. Bunun geç bir çözüm olduğunu biliyorum, ama bunun bir çözüm olduğunu belirtmeye değer.

public struct Point2D {
    public static Point2D NULL = new Point2D(-1,-1);
    private int[] Data;

    public int X {
        get {
            return this.Data[ 0 ];
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 0 ] = value;
            }
        }
    }

    public int Z {
        get {
            return this.Data[ 1 ];
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 1 ] = value;
            }
        }
    }

    public Point2D( int x , int z ) {
        this.Data = new int[ 2 ] { x , z };
    }

    public static Point2D operator +( Point2D A , Point2D B ) {
        return new Point2D( A.X + B.X , A.Z + B.Z );
    }

    public static Point2D operator -( Point2D A , Point2D B ) {
        return new Point2D( A.X - B.X , A.Z - B.Z );
    }

    public static Point2D operator *( Point2D A , int B ) {
        return new Point2D( B * A.X , B * A.Z );
    }

    public static Point2D operator *( int A , Point2D B ) {
        return new Point2D( A * B.Z , A * B.Z );
    }

    public override string ToString() {
        return string.Format( "({0},{1})" , this.X , this.Z );
    }
}

null adında statik bir yapıya sahip olduğum gerçeğini göz ardı ederek, (Not: Bu yalnızca tüm pozitif çeyrekler içindir) get; set kullanarak; C # 'da, belirli bir veri türünün varsayılan yapıcı Point2D () tarafından başlatılmadığı hatalarla uğraşmak için bir try / catch / son olarak sahip olabilirsiniz. Sanırım bu cevap bazı insanlara bir çözüm olarak anlaşılması zor. Bu çoğunlukla benimkini ekliyorum. C # içindeki alıcı ve ayarlayıcı işlevini kullanmak, bu varsayılan kurucu anlamsızlığı atlamanıza ve başlatmadıklarınızın etrafına bir deneme yakalamanıza olanak tanır. Benim için bu iyi çalışıyor, başka biri için bazı if ifadeleri eklemek isteyebilirsiniz. Yani, bir Pay / Payda kurulumu isteyeceğiniz durumda, bu kod yardımcı olabilir. Sadece bu çözümün hoş görünmediğini, verimlilik açısından muhtemelen daha da kötü çalıştığını tekrarlamak isterim, ancak, C # 'ın eski bir sürümünden gelen birisi için dizi veri türlerini kullanmak size bu işlevselliği sağlar. Sadece çalışan bir şey istiyorsanız şunu deneyin:

public struct Rational {
    private long[] Data;

    public long Numerator {
        get {
            try {
                return this.Data[ 0 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 0 ];
            }
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 0 ] = value;
            }
        }
    }

    public long Denominator {
        get {
            try {
                return this.Data[ 1 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 1 ];
            }
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 1 ] = value;
            }
        }
    }

    public Rational( long num , long denom ) {
        this.Data = new long[ 2 ] { num , denom };
        /* Todo: Find GCD etc. */
    }

    public Rational( long num ) {
        this.Data = new long[ 2 ] { num , 1 };
        this.Numerator = num;
        this.Denominator = 1;
    }
}

2
Bu çok kötü bir kod. Neden bir yapıda dizi başvurunuz var? Neden sadece X ve Y koordinatlarını alan olarak almıyorsunuz? Ve akış kontrolü için istisnalar kullanmak kötü bir fikirdir; kodunuzu genellikle NullReferenceException hiçbir zaman oluşmayacak şekilde yazmalısınız. Buna gerçekten ihtiyacınız varsa - böyle bir yapı bir yapı yerine bir sınıf için daha uygun olsa da - o zaman tembel başlatma kullanmalısınız. (Ve teknik olarak, - her bir koordinatın tamamen dışında her şeyde tamamen gereksizdir - her bir koordinatı iki kez ayarlarsınız.)
Mike Rosoft

-1
public struct Rational 
{
    private long numerator;
    private long denominator;

    public Rational(long num = 0, long denom = 1)   // This is allowed!!!
    {
        numerator   = num;
        denominator = denom;
    }
}

5
Bu izin verilen ancak herhangi bir parametre belirtilmemişse zaman kullanıldığını değil ideone.com/xsLloQ
Motti
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.