Dlaczego nie mogę zdefiniować domyślnego konstruktora dla struktury w .NET?

261

W .NET typ wartości (C # struct) nie może mieć konstruktora bez parametrów. Zgodnie z tym postem jest to wymagane przez specyfikację CLI. Co się dzieje, że dla każdego typu wartości tworzony jest domyślny konstruktor (przez kompilator?), Który inicjuje wszystkie elementy na zero (lub null).

Dlaczego nie wolno definiować takiego domyślnego konstruktora?

Jednym trywialnym zastosowaniem są liczby wymierne:

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

Używając bieżącej wersji C #, domyślnym produktem Rational jest to, 0/0co nie jest tak fajne.

PS : Czy domyślne parametry pomogą rozwiązać ten problem w C # 4.0, czy też zostanie wywołany domyślny konstruktor zdefiniowany w CLR?


Jon Skeet odpowiedział:

Na przykład, co chciałbyś zrobić, gdy ktoś to zrobił:

 Rational[] fractions = new Rational[1000];

Czy powinien przejść przez twój konstruktor 1000 razy?

Pewnie, że właśnie dlatego napisałem domyślnego konstruktora. CLR powinien używać domyślnego konstruktora zerowania, gdy nie zdefiniowano żadnego jawnego domyślnego konstruktora; w ten sposób płacisz tylko za to, czego używasz. Następnie, jeśli chcę kontener 1000 domyślnych Rational(i chcę zoptymalizować 1000 konstrukcji), użyję List<Rational>raczej tablicy niż tablicy.

Moim zdaniem ten powód nie jest wystarczająco silny, aby zapobiec definicji domyślnego konstruktora.

Motti
źródło
3
+1 miał kiedyś podobny problem, w końcu przekształcił strukturę w klasę.
Dirk Vollmar,
4
Domyślne parametry w C # 4 nie mogą pomóc, ponieważ Rational()wywołuje raczej parametryczny ctor niż Rational(long num=0, long denom=1).
LaTeX,
6
Zauważ, że w C # 6.0, który jest dostarczany z Visual Studio 2015, będzie dozwolone pisanie konstruktorów instancji o zerowym parametrze dla struktur. Tak więc new Rational()wywoła konstruktor, jeśli istnieje, jednak jeśli nie istnieje, new Rational()będzie równoważny default(Rational). W każdym razie zachęca się do użycia składni, default(Rational)gdy chce się „zerowej wartości” struktury (która jest „złą” liczbą w proponowanym projekcie Rational). Domyślną wartością dla typu wartości Tjest zawsze default(T). Dlatego new Rational[1000]nigdy nie będzie wywoływał konstruktorów struktur.
Jeppe Stig Nielsen,
6
Aby rozwiązać ten konkretny problem, możesz zapisać go denominator - 1w strukturze, aby domyślna wartość
wynosiła
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.Dlaczego miałbyś oczekiwać, że tablica wywoła inny konstruktor do listy dla struktury?
mjwills 18.10.17

Odpowiedzi:

197

Uwaga: poniższa odpowiedź została napisana na długo przed C # 6, który planuje wprowadzić możliwość deklarowania konstruktorów bez parametrów w strukturach - ale nadal nie będą wywoływane we wszystkich sytuacjach (np. Do tworzenia tablicy) (na końcu ta funkcja nie została dodana do C # 6 ).


EDYCJA: Zedytowałem odpowiedź poniżej ze względu na wgląd Grauenwolfa w CLR.

CLR pozwala typom wartości mieć konstruktory bez parametrów, ale C # nie. Wierzę, że dzieje się tak, ponieważ wprowadziłoby oczekiwanie, że konstruktor zostanie wywołany, gdy nie będzie. Rozważmy na przykład:

MyStruct[] foo = new MyStruct[1000];

CLR jest w stanie to zrobić bardzo skutecznie, po prostu przydzielając odpowiednią pamięć i zerując wszystko. Gdyby musiał uruchomić konstruktor MyStruct 1000 razy, byłoby to znacznie mniej wydajne. (W rzeczywistości tak nie jest - jeśli zrobić mieć konstruktora bez parametrów, to nie ma się uruchomić po utworzeniu tablicy, albo gdy masz niezainicjowanej zmiennej instancji).

Podstawową zasadą w języku C # jest „domyślna wartość dla dowolnego typu nie może polegać na żadnej inicjalizacji”. Teraz mogliby zezwolić na zdefiniowanie konstruktorów bez parametrów, ale wtedy nie wymagali wykonywania tego konstruktora we wszystkich przypadkach - ale doprowadziłoby to do większego zamieszania. (A przynajmniej tak mi się wydaje, że argument jest taki.)

EDYCJA: Aby użyć Twojego przykładu, co chciałbyś zrobić, gdy ktoś to zrobił:

Rational[] fractions = new Rational[1000];

Czy powinien przejść przez twój konstruktor 1000 razy?

  • Jeśli nie, otrzymamy 1000 nieprawidłowych uzasadnień
  • Jeśli tak, to potencjalnie zmarnujemy mnóstwo pracy, jeśli mamy zamiar wypełnić tablicę prawdziwymi wartościami.

EDYCJA: (Odpowiadając nieco na pytanie) Konstruktor nie tworzy parametrów. Typy wartości nie muszą mieć konstruktorów, jeśli chodzi o CLR - chociaż okazuje się, że może, jeśli napiszesz je w IL. Gdy piszesz new Guid()w języku C #, który emituje inną IL do tego, co otrzymujesz, jeśli wywołasz normalnego konstruktora. Zobacz to SO pytanie, aby uzyskać więcej informacji na ten temat.

I podejrzewam, że nie ma żadnych typy wartości w ramach z konstruktorów bez parametrów. Bez wątpienia NDepend mógłby mi powiedzieć, gdybym zadał to wystarczająco ładnie ... Fakt, że C # zabrania tego jest wystarczająco dużą wskazówką, aby pomyśleć, że to prawdopodobnie zły pomysł.

Jon Skeet
źródło
9
Krótsze wyjaśnienie: w C ++ struktury i klasa były tylko dwiema stronami tej samej monety. Jedyną prawdziwą różnicą jest to, że jedna była domyślnie publiczna, a druga była prywatna. W .Net istnieje znacznie większa różnica między strukturą a klasą i ważne jest, aby ją zrozumieć.
Joel Coehoorn,
38
@Jel: To tak naprawdę nie wyjaśnia tego konkretnego ograniczenia, prawda?
Jon Skeet,
6
CLR pozwala typom wartości mieć konstruktory bez parametrów. I tak, uruchomi go dla każdego elementu w tablicy. C # uważa, że ​​to zły pomysł i nie pozwala na to, ale można napisać, że tak.
Jonathan Allen,
2
Przepraszam, jestem trochę mylony z następującymi. Czy Rational[] fractions = new Rational[1000];marnuje również obciążenie pracą, jeśli Rationaljest strukturą zamiast struktury? Jeśli tak, to dlaczego klasy mają domyślny ctor?
pocałuj mnie pod pachą
5
@ FifaEarthCup2014: Musisz sprecyzować, co rozumiesz przez „marnowanie ciężaru pracy”. Ale gdy tak czy inaczej, nie wywoła konstruktora 1000 razy. Jeśli Rationaljest to klasa, otrzymasz tablicę 1000 referencji zerowych.
Jon Skeet
48

Struktur jest typem wartości, a typ wartości musi mieć wartość domyślną, gdy tylko zostanie zadeklarowany.

MyClass m;
MyStruct m2;

Jeśli zadeklarujesz dwa pola jak powyżej bez tworzenia instancji, zepsuj debugger, mbędzie miał wartość zerową, ale m2nie będzie. Biorąc to pod uwagę, bezparametrowy konstruktor nie miałby sensu, w rzeczywistości każdy konstruktor w strukturach przypisuje wartości, sama rzecz istnieje już po prostu poprzez jej zadeklarowanie. Rzeczywiście m2 można z powodzeniem wykorzystać w powyższym przykładzie i wywołać jej metody, jeśli w ogóle, a pola i właściwości zmanipulować!

użytkownik42467
źródło
3
Nie jestem pewien, dlaczego ktoś cię przegłosował. Wydajesz się być najbardziej poprawną odpowiedzią tutaj.
pipTheGeek,
12
Zachowanie w C ++ polega na tym, że jeśli typ ma domyślny konstruktor, to jest on używany, gdy taki obiekt jest tworzony bez jawnego konstruktora. Można to wykorzystać w języku C # do zainicjowania m2 za pomocą domyślnego konstruktora, dlatego ta odpowiedź nie jest pomocna.
Motti,
3
onester: jeśli nie chcesz, aby struktury wywoływały własnego konstruktora, gdy zostały zadeklarowane, nie definiuj takiego domyślnego konstruktora! :) tak mówi Motti
Stefan Monov
8
@Tarik. Nie zgadzam się. Wręcz przeciwnie, konstruktor bez parametrów miałby pełny sens: jeśli chcę stworzyć strukturę „Matrix”, która zawsze ma domyślną macierz tożsamości, to jak możesz to zrobić w inny sposób?
Elo
1
Nie jestem pewien, czy w pełni zgadzam się z „Rzeczywiście m2 można z powodzeniem wykorzystać…” . Być może było to prawdą w poprzednim C #, ale błąd kompilatora polega na zadeklarowaniu struktury, a nie newIt, a następnie próbie użycia jej członków
Caius Jard
18

Chociaż CLR na to pozwala, C # nie pozwala strukturom na domyślny konstruktor bez parametrów. Powodem jest to, że dla typu wartości kompilatory domyślnie nie generują domyślnego konstruktora ani nie generują wywołania do domyślnego konstruktora. Tak więc, nawet jeśli zdarzyło Ci się zdefiniować domyślny konstruktor, nie zostanie on wywołany, a to tylko dezorientuje.

Aby uniknąć takich problemów, kompilator C # nie zezwala użytkownikowi na zdefiniowanie domyślnego konstruktora. A ponieważ nie generuje domyślnego konstruktora, nie można inicjować pól podczas ich definiowania.

Lub głównym powodem jest to, że struktura jest typem wartości, a typy wartości są inicjowane przez wartość domyślną, a do inicjalizacji używany jest konstruktor.

Nie musisz tworzyć instancji swojej struktury za pomocą newsłowa kluczowego. Zamiast tego działa jak int; możesz uzyskać do niego bezpośredni dostęp.

Struktury nie mogą zawierać jawnych konstruktorów bez parametrów. Elementy konstrukcyjne są automatycznie inicjowane do wartości domyślnych.

Domyślny konstruktor (bez parametrów) dla struktury może ustawić inne wartości niż stan zerowania, co byłoby nieoczekiwanym zachowaniem. Środowisko wykonawcze .NET zabrania zatem domyślnych konstruktorów dla struktury.

Adii
źródło
Ta odpowiedź jest zdecydowanie najlepsza. W końcu sednem ograniczenia jest unikanie niespodzianek, takich jak MyStruct s;nie wywoływanie domyślnego konstruktora, który podałeś.
wznosi się
1
Dziękuję za wyjaśnienie. Tak więc tylko brak kompilatora wymagałby poprawy, nie ma teoretycznie dobrego powodu, aby zabronić konstruktorów bez parametrów (jak tylko można by ograniczyć je tylko do właściwości dostępu).
Elo
16

Możesz utworzyć właściwość statyczną, która inicjuje i zwraca domyślną liczbę „wymierną”:

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

I używaj go w następujący sposób:

var rat = Rational.One;
AustinWBryan
źródło
24
W takim przypadku Rational.Zeromoże być nieco mniej mylące.
Kevin
13

Krótsze wyjaśnienie:

W C ++ struktury i klasa były tylko dwiema stronami tej samej monety. Jedyną prawdziwą różnicą jest to, że jedna była domyślnie publiczna, a druga prywatna.

W .NET istnieje znacznie większa różnica między strukturą a klasą. Najważniejsze jest to, że struct zapewnia semantykę typu wartości, podczas gdy klasa zapewnia semantykę typu odniesienia. Kiedy zaczynasz myśleć o konsekwencjach tej zmiany, inne zmiany również stają się bardziej sensowne, w tym opisywane zachowanie konstruktora.

Joel Coehoorn
źródło
7
Musisz być trochę bardziej precyzyjny na temat tego, jak implikuje to podział wartości na typ odniesienia. Nie rozumiem ...
Motti,
Typy wartości mają wartość domyślną - nie są one zerowe, nawet jeśli nie zdefiniujesz konstruktora. Choć na pierwszy rzut oka nie wyklucza to również zdefiniowania domyślnego konstruktora, środowisko wykorzystujące tę funkcję wewnętrznie przyjmuje pewne założenia dotyczące struktur.
Joel Coehoorn,
@annakata: Inne konstruktory są prawdopodobnie przydatne w niektórych scenariuszach dotyczących Reflection. Ponadto, gdyby generyczne kiedykolwiek zostały ulepszone w celu umożliwienia sparametryzowanego „nowego” ograniczenia, przydatne byłyby struktury, które mogłyby być z nimi zgodne.
supercat
@annakata Wierzę, że to dlatego, że C # ma szczególne silne wymaganie, które newnaprawdę musi być napisane, aby wywołać konstruktora. W C ++ konstruktory są wywoływane w ukryty sposób, przy deklaracji lub instancji tablic. W języku C # albo wszystko jest wskaźnikiem, więc zacznij od zera, albo jest strukturą i musi zaczynać od czegoś, ale gdy nie możesz napisać new... (np. Init tablicy), złamałoby to silną regułę C #.
v.oddou
3

Nie widziałem odpowiednika spóźnionego rozwiązania, które dam, więc oto jest.

użyj przesunięć, aby przenieść wartości z domyślnego 0 na dowolną wartość, którą lubisz. tutaj należy użyć właściwości zamiast bezpośredniego dostępu do pól. (być może z możliwą funkcją c # 7 lepiej zdefiniujesz pola o zakresie właściwości, aby były chronione przed bezpośrednim dostępem do kodu).

To rozwiązanie działa dla prostych struktur z tylko typami wartości (bez typu ref lub struktury zerowalnej).

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

To różni się od tej odpowiedzi, to podejście nie jest specjalną obudową, ale wykorzystuje przesunięcie, które będzie działać dla wszystkich zakresów.

przykład z wyliczeniami jako polem.

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

Jak powiedziałem, ta sztuczka może nie działać we wszystkich przypadkach, nawet jeśli struct ma tylko pola wartości, tylko ty wiesz, że jeśli działa w twoim przypadku, czy nie. po prostu zbadaj. ale masz ogólny pomysł.

M.kazem Akhgary
źródło
To dobre rozwiązanie dla przykładu, który podałem, ale tak naprawdę miał to być tylko przykład, pytanie jest ogólne.
Motti
2

Tylko w specjalnym przypadku. Jeśli zobaczysz licznik 0 i mianownik 0, udawaj, że ma wartości, których naprawdę chcesz.

Jonathan Allen
źródło
5
Ja osobiście nie chciałbym, aby moje zajęcia / struktury miały takie zachowanie. Niepowodzenie po cichu (lub wyzdrowienie w sposób, w jaki domysły deweloperów są najlepsze dla ciebie) jest drogą do nieprzechwyconych błędów.
Boris Callens,
2
+1 To dobra odpowiedź, ponieważ w przypadku rodzajów wartości należy wziąć pod uwagę ich wartość domyślną. To pozwala ci „ustawić” wartość domyślną z jej zachowaniem.
IllidanS4 chce odzyskać Monikę
Dokładnie tak implementują klasy takie jak Nullable<T>(np int?.).
Jonathan Allen
To bardzo zły pomysł. 0/0 zawsze powinno być nieprawidłową frakcją (NaN). Co się stanie, jeśli ktoś zadzwoni, new Rational(x,y)gdzie xiy ma wartość 0?
Mike Rosoft,
Jeśli masz rzeczywistego konstruktora, możesz zgłosić wyjątek, zapobiegając wystąpieniu rzeczywistego 0/0. Lub jeśli chcesz, aby tak się stało, musisz dodać dodatkowy bool, aby odróżnić domyślny od 0/0.
Jonathan Allen
2

To, czego używam, to operator koalescencji zerowej (??) w połączeniu z polem podkładowym, takim jak to:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

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

Mam nadzieję że to pomoże ;)

Uwaga: zerowe przypisanie koalescencyjne jest obecnie propozycją funkcji dla C # 8.0.

Axel Samyn
źródło
1

Nie możesz zdefiniować domyślnego konstruktora, ponieważ używasz C #.

Struktury mogą mieć domyślne konstruktory w .NET, chociaż nie znam żadnego konkretnego języka, który by je wspierał.

Jonathan Allen
źródło
W języku C # klasy i struktury są semantycznie różne. Struktura jest typem wartości, podczas gdy klasa jest typem odniesienia.
Tom Sarduy
-1

Oto moje rozwiązanie dylematu braku domyślnego konstruktora. Wiem, że to późne rozwiązanie, ale myślę, że warto zauważyć, że to rozwiązanie.

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

ignorując fakt, że mam strukturę statyczną o nazwie null, (uwaga: dotyczy tylko wszystkich dodatnich ćwiartek), używając get; set; w języku C # możesz spróbować try / catch / wreszcie, aby poradzić sobie z błędami, w których określony typ danych nie jest inicjowany przez domyślny konstruktor Point2D (). Wydaje mi się, że jest to nieuchwytne rozwiązanie dla niektórych osób z tą odpowiedzią. Właśnie dlatego dodaję moje. Użycie funkcji pobierających i ustawiających w języku C # pozwoli ci ominąć ten domyślny konstrukt bez sensu i spróbować złapać to, czego nie zainicjowałeś. Dla mnie to działa dobrze, dla kogoś innego możesz chcieć dodać kilka instrukcji if. Tak więc w przypadku, gdy chcesz skonfigurować licznik / mianownik, ten kod może pomóc. Chciałbym tylko powtórzyć, że to rozwiązanie nie wygląda dobrze, prawdopodobnie działa jeszcze gorzej z punktu widzenia wydajności, ale, dla kogoś, kto pochodzi ze starszej wersji C #, użycie tablicowych typów danych daje tę funkcjonalność. Jeśli chcesz tylko czegoś, co działa, spróbuj tego:

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;
    }
}
G1xb17
źródło
2
To bardzo zły kod. Dlaczego masz odwołanie do tablicy w strukturze? Dlaczego po prostu nie masz współrzędnych X i Y jako pól? A stosowanie wyjątków do kontroli przepływu jest złym pomysłem; ogólnie powinieneś napisać swój kod w taki sposób, aby NullReferenceException nigdy nie wystąpił. Jeśli naprawdę tego potrzebujesz - choć taka konstrukcja byłaby bardziej odpowiednia dla klasy niż dla struktury - powinieneś użyć leniwej inicjalizacji. (I technicznie rzecz biorąc, jesteś - zupełnie niepotrzebnie w każdym oprócz pierwszego ustawienia współrzędnych - ustawiasz każdą współrzędną dwa razy.)
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;
    }
}
eMeL
źródło
5
Jest to dozwolone, ale nie jest używane, gdy nie podano żadnych parametrów ideone.com/xsLloQ
Motti