Perché non riesco a definire un costruttore predefinito per una struttura in .NET?


261

In .NET, un tipo di valore (C # struct) non può avere un costruttore senza parametri. Secondo questo post, ciò è richiesto dalla specifica CLI. Quello che succede è che per ogni tipo di valore viene creato un costruttore predefinito (dal compilatore?) Che inizializza tutti i membri a zero (o null).

Perché non è consentito definire un tale costruttore predefinito?

Un uso banale è per i numeri razionali:

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

Usando la versione corrente di C #, un Rational predefinito è 0/0che non è così bello.

PS : I parametri predefiniti aiuteranno a risolverlo per C # 4.0 o verrà chiamato il costruttore predefinito definito da CLR?


Jon Skeet rispose:

Per usare il tuo esempio, cosa vorresti succedere quando qualcuno ha fatto:

 Rational[] fractions = new Rational[1000];

Dovrebbe correre attraverso il tuo costruttore 1000 volte?

Certo che dovrebbe, ecco perché ho scritto il costruttore predefinito in primo luogo. Il CLR dovrebbe usare il costruttore di azzeramento predefinito quando non è definito alcun costruttore predefinito esplicito; in questo modo paghi solo per quello che usi. Quindi, se voglio un contenitore di 1000 Rationals non predefiniti (e voglio ottimizzare le 1000 costruzioni) userò List<Rational>un array anziché un array.

Questo motivo, secondo me, non è abbastanza forte da impedire la definizione di un costruttore predefinito.


3
+1 ha avuto un problema simile una volta, alla fine ha convertito la struttura in una classe.
Dirk Vollmar,

4
I parametri predefiniti in C # 4 non possono essere d'aiuto perché Rational()invoca il ctor senza parametri anziché il Rational(long num=0, long denom=1).
LaTeX,

6
Si noti che in C # 6.0 fornito con Visual Studio 2015 sarà consentito scrivere costruttori di istanze con parametri zero per le strutture. Quindi new Rational()invocherà il costruttore se esiste, tuttavia se non esiste, new Rational()sarà equivalente a default(Rational). In ogni caso sei incoraggiato a usare la sintassi default(Rational)quando vuoi il "valore zero" della tua struttura (che è un numero "cattivo" con il tuo progetto proposto di Rational). Il valore predefinito per un tipo di valore Tè sempre default(T). Quindi new Rational[1000]non invocherà mai i costruttori di struct.
Jeppe Stig Nielsen,

6
Per risolvere questo problema specifico è possibile archiviare denominator - 1all'interno della struttura, in modo che il valore predefinito diventi 0/1
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.Perché dovresti aspettarti che un array invochi un costruttore diverso in un Elenco per una struttura?
mjwills,

Risposte:


197

Nota: la risposta di seguito è stata scritta molto tempo prima di C # 6, che sta pianificando di introdurre la possibilità di dichiarare costruttori senza parametri nelle strutture - ma non saranno comunque chiamati in tutte le situazioni (ad es. Per la creazione di array) (alla fine questa funzione non è stata aggiunta a C # 6 ).


EDIT: Ho modificato la risposta qui sotto a causa dell'intuizione di Grauenwolf sul CLR.

Il CLR consente ai tipi di valore di avere costruttori senza parametri, ma C # no. Credo che ciò sia dovuto al fatto che avrebbe introdotto un'aspettativa che il costruttore sarebbe stato chiamato quando non lo sarebbe. Ad esempio, considera questo:

MyStruct[] foo = new MyStruct[1000];

Il CLR è in grado di farlo in modo molto efficiente semplicemente allocando la memoria appropriata e azzerando tutto. Se dovesse eseguire il costruttore MyStruct 1000 volte, sarebbe molto meno efficiente. (In realtà, non è così - se fare avere un costruttore senza parametri, che non venga eseguito quando si crea un array, o quando si dispone di una variabile di istanza non inizializzata.)

La regola di base in C # è "il valore predefinito per qualsiasi tipo non può fare affidamento su alcuna inizializzazione". Ora avrebbero potuto consentire la definizione di costruttori senza parametri, ma non avrebbero richiesto l'esecuzione del costruttore in tutti i casi, ma ciò avrebbe portato a una maggiore confusione. (O almeno, quindi credo che l'argomento vada.)

EDIT: per usare il tuo esempio, cosa vorresti succedere quando qualcuno ha fatto:

Rational[] fractions = new Rational[1000];

Dovrebbe correre attraverso il tuo costruttore 1000 volte?

  • Altrimenti, finiamo con 1000 razionali non validi
  • In tal caso, abbiamo potenzialmente sprecato un sacco di lavoro se stiamo per riempire l'array con valori reali.

EDIT: (Rispondere un po 'di più alla domanda) Il costruttore senza parametri non è stato creato dal compilatore. I tipi di valore non devono avere costruttori per quanto riguarda il CLR, anche se si scopre che è possibile se lo si scrive in IL. Quando scrivi " new Guid()" in C # che emette IL diverso da quello che ottieni se chiami un costruttore normale. Vedi questa domanda SO per un po 'di più su questo aspetto.

Ho il sospetto che non ci siano tipi di valore nel framework con costruttori senza parametri. Non c'è dubbio che NDepend potrebbe dirmi se l'ho chiesto abbastanza bene ... Il fatto che C # lo proibisca è un suggerimento abbastanza grande per me pensare che sia probabilmente una cattiva idea.


9
Spiegazione più breve: in C ++, struct e class erano solo due facce della stessa medaglia. L'unica vera differenza è che uno era pubblico per impostazione predefinita e l'altro era privato. In .Net, c'è una differenza molto maggiore tra una struttura e una classe ed è importante capirla.
Joel Coehoorn,

39
@Joel: Questo non spiega davvero questa particolare restrizione, vero?
Jon Skeet,

6
Il CLR consente ai tipi di valore di avere costruttori senza parametri. E sì, lo eseguirà per ogni singolo elemento in un array. C # pensa che questa sia una cattiva idea e non lo consente, ma potresti scrivere un linguaggio .NET che lo fa.
Jonathan Allen,

2
Mi dispiace, sono un po 'confuso con quanto segue. Spreca Rational[] fractions = new Rational[1000];anche un sacco di lavoro se si Rationaltratta di una classe anziché di una struttura? In tal caso, perché le classi hanno un ctor predefinito?
Bacia la mia ascella il

5
@ FifaEarthCup2014: Dovresti essere più specifico su cosa intendi per "sprecare un carico di lavoro". Ma in entrambi i casi, non chiamerà il costruttore 1000 volte. Se Rationalè una classe, finirai con una matrice di 1000 riferimenti null.
Jon Skeet,

48

Una struttura è un tipo di valore e un tipo di valore deve avere un valore predefinito non appena viene dichiarato.

MyClass m;
MyStruct m2;

Se si dichiarano due campi come sopra senza creare un'istanza, quindi interrompere il debugger, msarà nullo ma m2non lo sarà. Detto questo, un costruttore senza parametri non avrebbe alcun senso, in effetti tutto ciò che un costruttore su una struttura fa è assegnare valori, la cosa stessa esiste già solo dichiarandola. In effetti, nell'esempio sopra potrebbe essere usato abbastanza tranquillamente m2 e potrebbero essere chiamati i suoi metodi, se possibile, e i suoi campi e proprietà manipolati!


3
Non sono sicuro del perché qualcuno ti abbia votato. Sembra che tu sia la risposta più corretta qui.
pipTheGeek,

12
Il comportamento in C ++ è che se un tipo ha un costruttore predefinito, questo viene usato quando un tale oggetto viene creato senza un costruttore esplicito. Questo avrebbe potuto essere usato in C # per inizializzare m2 con il costruttore predefinito, motivo per cui questa risposta non è utile.
Motti,

3
onester: se non si desidera che le strutture chiamino il proprio costruttore quando dichiarate, non definire tale costruttore predefinito! :) questo è di Motti dicendo
Stefan Monov

8
@Tarik. Non sono d'accordo. Al contrario, un costruttore senza parametri avrebbe pieno senso: se voglio creare una struttura "Matrix" che ha sempre una matrice identità come valore predefinito, come potresti farlo con altri mezzi?
Elo,

1
Non sono sicuro di essere pienamente d'accordo con "In effetti M2 potrebbe tranquillamente essere usato .." . Potrebbe essere stato vero in un C # precedente ma è un errore del compilatore dichiarare una struttura, non newIt, quindi provare a usare i suoi membri
Caius Jard

18

Sebbene CLR lo consenta, C # non consente alle strutture di avere un costruttore predefinito senza parametri. Il motivo è che, per un tipo di valore, i compilatori per impostazione predefinita non generano un costruttore predefinito, né generano una chiamata al costruttore predefinito. Quindi, anche se ti è capitato di definire un costruttore predefinito, non verrà chiamato e questo ti confonderà.

Per evitare tali problemi, il compilatore C # non consente la definizione di un costruttore predefinito da parte dell'utente. E poiché non genera un costruttore predefinito, non è possibile inizializzare i campi durante la definizione.

Oppure la ragione principale è che una struttura è un tipo di valore e che i tipi di valore sono inizializzati da un valore predefinito e il costruttore viene utilizzato per l'inizializzazione.

Non è necessario creare un'istanza di struct con la newparola chiave. Funziona invece come un int; puoi accedervi direttamente.

Le strutture non possono contenere costruttori espliciti senza parametri. I membri di Struct vengono inizializzati automaticamente ai loro valori predefiniti.

Un costruttore predefinito (senza parametri) per una struttura potrebbe impostare valori diversi dallo stato azzerato, il che sarebbe un comportamento imprevisto. Il runtime .NET pertanto proibisce i costruttori predefiniti per una struttura.


Questa risposta è di gran lunga la migliore. Alla fine il punto fondamentale della limitazione è evitare sorprese come MyStruct s;non chiamare il costruttore predefinito che hai fornito.
Talles,

1
Grazie per la spiegazione. Quindi è solo una mancanza del compilatore che dovrebbe essere migliorata, non vi è alcuna buona ragione teorica per vietare i costruttori senza parametri (non appena potrebbero essere limitati a solo accedere alle proprietà).
Elo,

16

È possibile creare una proprietà statica che inizializzi e restituisca un numero "razionale" predefinito:

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

E usalo come:

var rat = Rational.One;

24
In questo caso, Rational.Zeropotrebbe essere un po 'meno confuso.
Kevin,

13

Spiegazione più breve:

In C ++, struct e class erano solo due facce della stessa medaglia. L'unica vera differenza è che uno era pubblico per impostazione predefinita e l'altro era privato.

In .NET , esiste una differenza molto maggiore tra una struttura e una classe. La cosa principale è che struct fornisce una semantica di tipo valore, mentre la classe fornisce una semantica di tipo riferimento. Quando inizi a pensare alle implicazioni di questo cambiamento, anche altre modifiche hanno più senso, incluso il comportamento del costruttore che descrivi.


7
Dovrai essere un po 'più esplicito su come ciò sia implicito dalla suddivisione del valore rispetto al tipo di riferimento. Non capisco ...
Motti,

I tipi di valore hanno un valore predefinito: non sono nulli, anche se non si definisce un costruttore. A prima vista ciò non preclude anche la definizione di un costruttore predefinito, il framework che utilizza questa funzione interna per fare alcune ipotesi sulle strutture.
Joel Coehoorn,

@annakata: altri costruttori sono probabilmente utili in alcuni scenari che coinvolgono Reflection. Inoltre, se i generici fossero mai stati migliorati per consentire un "nuovo" vincolo parametrizzato, sarebbe utile disporre di strutture in grado di rispettarli.
supercat

@annakata Credo che sia perché C # ha un requisito particolarmente forte che newdeve essere scritto per chiamare un costruttore. In C ++ i costruttori sono chiamati in modi nascosti, al momento della dichiarazione o istanziazione di array. In C # o tutto è un puntatore, quindi inizia da null, o è uno struct e deve iniziare da qualcosa, ma quando non puoi scrivere new... (come array init), ciò infrange una forte regola C #.
v.

3

Non ho visto una soluzione tardiva che fornirò, quindi eccola qui.

usa gli offset per spostare i valori dallo 0 predefinito in qualsiasi valore che ti piace. qui è necessario utilizzare le proprietà anziché accedere direttamente ai campi. (forse con la possibile funzione c # 7 puoi definire meglio i campi con ambito di proprietà in modo che rimangano protetti dall'accesso diretto nel codice.)

Questa soluzione funziona per semplici strutture con solo tipi di valore (nessun tipo di riferimento o struttura nullable).

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

Questo è diverso da questa risposta, questo approccio non è un involucro speciale ma il suo utilizzo dell'offset che funzionerà per tutte le gamme.

esempio con enumerazioni come campo.

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

Come ho detto, questo trucco potrebbe non funzionare in tutti i casi, anche se struct ha solo campi di valori, solo tu sai che se funziona nel tuo caso o no. basta esaminare. ma hai un'idea generale.


Questa è una buona soluzione per l'esempio che ho dato, ma in realtà doveva essere solo un esempio, la domanda è generale.
Motti,

2

Caso speciale. Se vedi un numeratore di 0 e un denominatore di 0, fai finta che abbia i valori che desideri davvero.


5
Personalmente non vorrei che le mie classi / strutture avessero questo tipo di comportamento. Fallire silenziosamente (o riprendersi nel modo in cui lo sviluppo indovina è la cosa migliore per te) è la strada per errori non rilevati.
Boris Callens,

2
+1 Questa è una buona risposta, perché per i tipi di valore, devi tener conto del loro valore predefinito. Questo ti permette di "impostare" il valore predefinito con il suo comportamento.
IllidanS4 vuole Monica indietro il

Questo è esattamente il modo in cui implementano classi come Nullable<T>(ad esempio int?).
Jonathan Allen,

È una pessima idea. 0/0 dovrebbe sempre essere una frazione non valida (NaN). Cosa succede se qualcuno chiama new Rational(x,y)dove xey è 0?
Mike Rosoft,

Se hai un vero costruttore, puoi lanciare un'eccezione, impedendo che si verifichi un vero 0/0. O se vuoi che accada, devi aggiungere un valore aggiuntivo per distinguere tra default e 0/0.
Jonathan Allen,

2

Quello che uso è l' operatore a coalescenza nulla (??) combinato con un campo di supporto come questo:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

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

Spero che questo ti aiuti ;)

Nota: l' assegnazione di coalescenza nulla è attualmente una proposta di funzionalità per C # 8.0.


1

Non è possibile definire un costruttore predefinito perché si utilizza C #.

Le strutture possono avere costruttori predefiniti in .NET, anche se non conosco alcun linguaggio specifico che lo supporti.


In C #, le classi e le strutture sono semanticamente diverse. Una struttura è un tipo di valore, mentre una classe è un tipo di riferimento.
Tom Sarduy,

-1

Ecco la mia soluzione al dilemma del costruttore non predefinito. So che questa è una soluzione tardiva, ma penso che valga la pena notare che questa è una soluzione.

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

ignorando il fatto che ho una struttura statica chiamata null, (Nota: questo è solo per tutti i quadranti positivi), usando get; set; in C #, puoi avere un tentativo / catch / infine, per gestire gli errori in cui un particolare tipo di dati non è inizializzato dal costruttore predefinito Point2D (). Immagino sia sfuggente come soluzione per alcune persone su questa risposta. Questo è principalmente perché sto aggiungendo il mio. L'uso della funzionalità getter e setter in C # ti consentirà di aggirare questo non-builder predefinito del costruttore e di provare ciò che non hai inizializzato. Per me questo funziona bene, per qualcun altro potresti voler aggiungere alcune dichiarazioni if. Quindi, nel caso in cui si desideri una configurazione di Numeratore / Denominatore, questo codice potrebbe essere di aiuto. Vorrei solo ribadire che questa soluzione non ha un bell'aspetto, probabilmente funziona anche peggio dal punto di vista dell'efficienza, ma, per qualcuno proveniente da una versione precedente di C #, l'utilizzo di tipi di dati array offre questa funzionalità. Se vuoi solo qualcosa che funzioni, prova questo:

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
Questo è un pessimo codice. Perché hai un riferimento di matrice in una struttura? Perché non hai semplicemente le coordinate X e Y come campi? E usare le eccezioni per il controllo del flusso è una cattiva idea; dovresti generalmente scrivere il tuo codice in modo tale che NullReferenceException non si verifichi mai. Se ne hai davvero bisogno, anche se un tale costrutto sarebbe più adatto a una classe piuttosto che a una struttura, allora dovresti usare l'inizializzazione lazy. (E tecnicamente, sei - completamente inutilmente in ogni tranne che nella prima impostazione di una coordinata - impostando ogni coordinata due volte.)
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
È consentito ma non viene utilizzato quando non vengono specificati parametri ideone.com/xsLloQ
Motti
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.