Pourquoi ne puis-je pas définir un constructeur par défaut pour une structure dans .NET?

261

Dans .NET, un type de valeur (C # struct) ne peut pas avoir de constructeur sans paramètres. Selon cet article, cela est rendu obligatoire par la spécification CLI. Ce qui se passe, c'est que pour chaque type de valeur, un constructeur par défaut est créé (par le compilateur?) Qui initialise tous les membres à zéro (ou null).

Pourquoi est-il interdit de définir un tel constructeur par défaut?

Une utilisation triviale est pour les nombres rationnels:

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

En utilisant la version actuelle de C #, un Rational par défaut est 0/0ce qui n'est pas si cool.

PS : les paramètres par défaut aideront-ils à résoudre ce problème pour C # 4.0 ou le constructeur par défaut défini par CLR sera-t-il appelé?


Jon Skeet a répondu:

Pour utiliser votre exemple, que voudriez-vous qu'il se passe lorsque quelqu'un:

 Rational[] fractions = new Rational[1000];

Doit-il parcourir 1000 fois votre constructeur?

Bien sûr, c'est pourquoi j'ai écrit le constructeur par défaut en premier lieu. Le CLR doit utiliser le constructeur de mise à zéro par défaut quand aucun constructeur par défaut explicite n'est défini; de cette façon, vous ne payez que ce que vous utilisez. Ensuite, si je veux un conteneur de 1000 Rationals par défaut (et que je souhaite optimiser les 1000 constructions), j'utiliserai List<Rational>plutôt qu'un tableau.

Cette raison, à mon avis, n'est pas assez forte pour empêcher la définition d'un constructeur par défaut.

Motti
la source
3
+1 a eu un problème similaire une fois, a finalement converti la structure en classe.
Dirk Vollmar
4
Les paramètres par défaut en C # 4 ne peuvent pas aider car Rational()invoque le ctor sans paramètre plutôt que le Rational(long num=0, long denom=1).
LaTeX
6
Notez que dans C # 6.0 fourni avec Visual Studio 2015, il sera autorisé d'écrire des constructeurs d'instance à paramètre zéro pour les structures. Donc new Rational(), invoquera le constructeur s'il existe, mais s'il n'existe pas, new Rational()sera équivalent à default(Rational). Dans tous les cas, nous vous encourageons à utiliser la syntaxe default(Rational)lorsque vous voulez la "valeur zéro" de votre structure (qui est un "mauvais" nombre avec votre conception proposée de Rational). La valeur par défaut pour un type de valeur Test toujours default(T). N'invoquera donc new Rational[1000]jamais les constructeurs struct.
Jeppe Stig Nielsen
6
Pour résoudre ce problème spécifique, vous pouvez stocker denominator - 1à l'intérieur de la structure, de sorte que la valeur par défaut devienne 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.Pourquoi attendriez-vous qu'un tableau appelle un constructeur différent à une liste pour une structure?
mjwills

Réponses:

197

Remarque: la réponse ci-dessous a été écrite longtemps avant C # 6, qui prévoit d'introduire la possibilité de déclarer des constructeurs sans paramètre dans les structures - mais ils ne seront toujours pas appelés dans toutes les situations (par exemple pour la création de tableaux) (à la fin cette fonctionnalité n'a pas été ajoutée à C # 6 ).


EDIT: J'ai édité la réponse ci-dessous en raison de la perspicacité de Grauenwolf dans le CLR.

Le CLR permet aux types de valeurs d'avoir des constructeurs sans paramètres, mais pas C #. Je crois que c'est parce que cela introduirait une attente que le constructeur soit appelé alors qu'il ne le ferait pas. Par exemple, considérez ceci:

MyStruct[] foo = new MyStruct[1000];

Le CLR est capable de le faire très efficacement simplement en allouant la mémoire appropriée et en mettant tout à zéro. S'il devait exécuter le constructeur MyStruct 1000 fois, ce serait beaucoup moins efficace. (En fait, il ne fonctionne pas - si vous n'avez un constructeur parameterless, il ne se tourne pas lorsque vous créez un tableau, ou lorsque vous avez une variable d'instance non initialisée.)

La règle de base en C # est "la valeur par défaut pour tout type ne peut pas compter sur une initialisation". Maintenant, ils auraient pu permettre de définir des constructeurs sans paramètres, mais ils n'auraient alors pas exigé que ce constructeur soit exécuté dans tous les cas - mais cela aurait conduit à plus de confusion. (Ou du moins, donc je crois que l'argument va.)

EDIT: Pour utiliser votre exemple, que voudriez-vous qu'il se passe quand quelqu'un:

Rational[] fractions = new Rational[1000];

Doit-il parcourir 1000 fois votre constructeur?

  • Sinon, on se retrouve avec 1000 justifications invalides
  • Si c'est le cas, nous avons potentiellement gaspillé une charge de travail si nous sommes sur le point de remplir le tableau avec des valeurs réelles.

EDIT: (Répondant un peu plus à la question) Le constructeur sans paramètre n'est pas créé par le compilateur. Les types de valeur n'ont pas besoin d'avoir de constructeurs en ce qui concerne le CLR - bien qu'il s'avère que cela peut être si vous l'écrivez en IL. Lorsque vous écrivez " new Guid()" en C #, cela émet un IL différent de ce que vous obtenez si vous appelez un constructeur normal. Voir cette question SO pour un peu plus sur cet aspect.

Je soupçonne qu'il n'y a aucun type de valeur dans le framework avec des constructeurs sans paramètres. Nul doute que NDepend pourrait me dire si je le lui ai demandé assez gentiment ... Le fait que C # l'interdise est un indice assez important pour que je pense que c'est probablement une mauvaise idée.

Jon Skeet
la source
9
Explication plus courte: en C ++, struct et classe n'étaient que les deux faces d'une même médaille. La seule vraie différence est que l'un était public par défaut et l'autre privé. Dans .Net, il existe une différence beaucoup plus grande entre une structure et une classe, et il est important de la comprendre.
Joel Coehoorn
39
@ Joel: Cela n'explique pas vraiment cette restriction particulière, n'est-ce pas?
Jon Skeet
6
Le CLR permet aux types de valeurs d'avoir des constructeurs sans paramètres. Et oui, il l'exécutera pour chaque élément d'un tableau. C # pense que c'est une mauvaise idée et ne le permet pas, mais vous pouvez écrire un langage .NET qui le fait.
Jonathan Allen
2
Désolé, je suis un peu confus avec ce qui suit. Est-ce Rational[] fractions = new Rational[1000];aussi une perte de travail si Rationalc'est une classe au lieu d'une structure? Si oui, pourquoi les classes ont-elles un ctor par défaut?
embrasse mon aisselle
5
@ FifaEarthCup2014: Vous devez être plus précis sur ce que vous entendez par «gaspiller une charge de travail». Mais dans un cas comme dans l'autre, il ne va pas appeler le constructeur 1000 fois. Si Rationalest une classe, vous vous retrouverez avec un tableau de 1000 références nulles.
Jon Skeet
48

Un struct est un type de valeur et un type de valeur doit avoir une valeur par défaut dès qu'il est déclaré.

MyClass m;
MyStruct m2;

Si vous déclarez deux champs comme ci-dessus sans instancier non plus, alors le débogueur msera cassé, sera nul mais m2ne le sera pas. Compte tenu de cela, un constructeur sans paramètre n'aurait aucun sens, en fait, tout constructeur sur une structure ne fait qu'affecter des valeurs, la chose elle-même existe déjà juste en la déclarant. En effet m2 pourrait très bien être utilisé dans l'exemple ci-dessus et avoir ses méthodes appelées, le cas échéant, et ses champs et propriétés manipulés!

user42467
la source
3
Je ne sais pas pourquoi quelqu'un vous a rejeté. Vous semblez être la réponse la plus correcte ici.
pipTheGeek
12
Le comportement en C ++ est que si un type a un constructeur par défaut, celui-ci est utilisé lorsqu'un tel objet est créé sans constructeur explicite. Cela aurait pu être utilisé en C # pour initialiser m2 avec le constructeur par défaut, c'est pourquoi cette réponse n'est pas utile.
Motti
3
onester: si vous ne voulez pas que les structures appellent leur propre constructeur une fois déclarées, alors ne définissez pas un tel constructeur par défaut! :) c'est le mot de Motti
Stefan Monov
8
@Tarik. Je ne suis pas d'accord. Au contraire, un constructeur sans paramètre aurait tout son sens: si je veux créer une structure "Matrix" qui aura toujours une matrice d'identité comme valeur par défaut, comment pourriez-vous le faire par d'autres moyens?
Elo
1
Je ne suis pas sûr d'être entièrement d'accord avec "En effet, le m2 pourrait très bien être utilisé .." . Cela peut avoir été vrai dans un C # précédent, mais c'est une erreur du compilateur de déclarer une structure, pas newIt, puis d'essayer d'utiliser ses membres
Caius Jard
18

Bien que le CLR le permette, C # ne permet pas aux structures d'avoir un constructeur sans paramètre par défaut. La raison en est que, pour un type de valeur, les compilateurs par défaut ne génèrent pas de constructeur par défaut, ni ne génèrent d'appel au constructeur par défaut. Donc, même s'il vous arrivait de définir un constructeur par défaut, il ne sera pas appelé, et cela ne fera que vous embrouiller.

Pour éviter de tels problèmes, le compilateur C # interdit la définition d'un constructeur par défaut par l'utilisateur. Et comme il ne génère pas de constructeur par défaut, vous ne pouvez pas initialiser les champs lors de leur définition.

Ou la grande raison est qu'une structure est un type de valeur et que les types de valeur sont initialisés par une valeur par défaut et que le constructeur est utilisé pour l'initialisation.

Vous n'avez pas à instancier votre structure avec le newmot clé. Il fonctionne à la place comme un int; vous pouvez y accéder directement.

Les structures ne peuvent pas contenir de constructeurs explicites sans paramètre. Les membres de structure sont automatiquement initialisés à leurs valeurs par défaut.

Un constructeur par défaut (sans paramètre) pour une structure pourrait définir des valeurs différentes de l'état de remise à zéro, ce qui serait un comportement inattendu. Le runtime .NET interdit donc les constructeurs par défaut pour une structure.

Adiii
la source
Cette réponse est de loin la meilleure. En fin de compte, le but de la restriction est d'éviter des surprises comme de MyStruct s;ne pas appeler le constructeur par défaut que vous avez fourni.
talles
1
Merci pour l'explication. Il ne s'agit donc que d'un manque de compilateur qui devrait être amélioré, il n'y a aucune bonne raison théorique d'interdire les constructeurs sans paramètres (dès lors qu'ils pourraient être limités aux seules propriétés d'accès).
Elo
16

Vous pouvez créer une propriété statique qui initialise et renvoie un nombre "rationnel" par défaut:

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

Et utilisez-le comme:

var rat = Rational.One;
AustinWBryan
la source
24
Dans ce cas, cela Rational.Zeropourrait être un peu moins déroutant.
Kevin
13

Explication plus courte:

En C ++, struct et class n'étaient que les deux faces d'une même médaille. La seule vraie différence est que l'un était public par défaut et l'autre privé.

Dans .NET , il existe une différence beaucoup plus grande entre une structure et une classe. L'essentiel est que struct fournit une sémantique de type valeur, tandis que class fournit une sémantique de type référence. Lorsque vous commencez à réfléchir aux implications de ce changement, d'autres changements commencent également à avoir plus de sens, y compris le comportement du constructeur que vous décrivez.

Joel Coehoorn
la source
7
Vous devrez être un peu plus explicite sur la façon dont cela est impliqué par la séparation valeur / type de référence Je ne comprends pas ...
Motti
Les types de valeur ont une valeur par défaut - ils ne sont pas nuls, même si vous ne définissez pas de constructeur. Bien qu'à première vue cela n'empêche pas de définir également un constructeur par défaut, le framework utilisant cette fonctionnalité interne pour faire certaines hypothèses sur les structures.
Joel Coehoorn
@annakata: D'autres constructeurs sont probablement utiles dans certains scénarios impliquant la réflexion. De plus, si des génériques étaient jamais améliorés pour permettre une "nouvelle" contrainte paramétrée, il serait utile d'avoir des structures qui pourraient s'y conformer.
supercat
@annakata Je crois que c'est parce que C # a une exigence particulièrement forte qui newdoit vraiment être écrite pour appeler un constructeur. En C ++, les constructeurs sont appelés de manière cachée, lors de la déclaration ou de l'instanciation des tableaux. En C #, soit tout est un pointeur, alors commencez à null, soit c'est une structure et devez commencer à quelque chose, mais quand vous ne pouvez pas écrire new... (comme le tableau init), cela briserait une règle C # forte.
v.oddou
3

Je n'ai pas vu d'équivalent à une solution tardive que je vais donner, alors voici.

utilisez les décalages pour déplacer les valeurs de 0 par défaut dans n'importe quelle valeur que vous aimez. ici, les propriétés doivent être utilisées au lieu d'accéder directement aux champs. (peut-être qu'avec la fonctionnalité c # 7 possible, vous feriez mieux de définir les champs de portée de propriété afin qu'ils restent protégés contre l'accès direct dans le code.)

Cette solution fonctionne pour les structures simples avec uniquement des types de valeur (pas de type ref ou de structure 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;
    }
}

Ceci est différent de cette réponse, cette approche n'est pas un boîtier spécial mais son utilisation de l'offset qui fonctionnera pour toutes les gammes.

exemple avec des énumérations comme champ.

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

Comme je l'ai dit, cette astuce peut ne pas fonctionner dans tous les cas, même si struct n'a que des champs de valeur, vous seul savez que cela fonctionne dans votre cas ou non. il suffit d'examiner. mais vous avez l'idée générale.

M.kazem Akhgary
la source
C'est une bonne solution pour l'exemple que j'ai donné mais ce n'était vraiment censé être qu'un exemple, la question est générale.
Motti
2

Juste cas spécial. Si vous voyez un numérateur de 0 et un dénominateur de 0, faites comme s'il avait les valeurs que vous voulez vraiment.

Jonathan Allen
la source
5
Personnellement, je n'aimerais pas que mes classes / structures aient ce genre de comportement. Échouer en silence (ou récupérer de la façon dont le développeur le devine est le mieux pour vous) est le chemin vers des erreurs inattendues.
Boris Callens
2
+1 C'est une bonne réponse, car pour les types de valeur, vous devez prendre en compte leur valeur par défaut. Cela vous permet de "définir" la valeur par défaut avec son comportement.
IllidanS4 veut que Monica revienne le
C'est exactement ainsi qu'ils implémentent des classes telles que Nullable<T>(par exemple int?).
Jonathan Allen
C'est une très mauvaise idée. 0/0 doit toujours être une fraction non valide (NaN). Que faire si quelqu'un appelle new Rational(x,y)où x et y se trouvent être 0?
Mike Rosoft
Si vous avez un constructeur réel, vous pouvez lever une exception, empêchant un vrai 0/0 de se produire. Ou si vous voulez que cela se produise, vous devez ajouter un booléen supplémentaire pour distinguer le défaut et le 0/0.
Jonathan Allen
2

Ce que j'utilise est l' opérateur de coalescence nulle (??) combiné avec un champ de support comme celui-ci:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

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

J'espère que cela t'aides ;)

Remarque: l' affectation de coalescence nulle est actuellement une proposition de fonctionnalité pour C # 8.0.

Axel Samyn
la source
1

Vous ne pouvez pas définir un constructeur par défaut car vous utilisez C #.

Les structures peuvent avoir des constructeurs par défaut dans .NET, bien que je ne connaisse aucun langage spécifique qui le supporte.

Jonathan Allen
la source
En C #, les classes et les structures sont sémantiquement différentes. Une structure est un type de valeur, tandis qu'une classe est un type de référence.
Tom Sarduy
-1

Voici ma solution au dilemme du constructeur par défaut. Je sais que c'est une solution tardive, mais je pense qu'il vaut la peine de noter que c'est une solution.

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

ignorant le fait que j'ai une structure statique appelée null, (Remarque: Ceci est pour tous les quadrants positifs uniquement), en utilisant get; set; en C #, vous pouvez avoir un try / catch / enfin, pour traiter les erreurs où un type de données particulier n'est pas initialisé par le constructeur par défaut Point2D (). Je suppose que cela est insaisissable comme solution à certaines personnes sur cette réponse. C'est surtout pourquoi j'ajoute le mien. L'utilisation de la fonctionnalité getter et setter en C # vous permettra de contourner ce constructeur par défaut non-sens et de tenter de rattraper ce que vous n'avez pas initialisé. Pour moi, cela fonctionne bien, pour quelqu'un d'autre, vous voudrez peut-être ajouter des instructions if. Donc, dans le cas où vous souhaiteriez une configuration Numérateur / Dénominateur, ce code pourrait vous aider. Je voudrais simplement répéter que cette solution n'a pas l'air bien, fonctionne probablement encore pire du point de vue de l'efficacité, mais, pour quelqu'un provenant d'une ancienne version de C #, l'utilisation de types de données de tableau vous donne cette fonctionnalité. Si vous voulez juste quelque chose qui fonctionne, essayez ceci:

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
la source
2
C'est un très mauvais code. Pourquoi avez-vous une référence de tableau dans une structure? Pourquoi n'avez-vous pas simplement les coordonnées X et Y comme champs? Et l'utilisation d'exceptions pour le contrôle de flux est une mauvaise idée; vous devez généralement écrire votre code de manière à ce que NullReferenceException ne se produise jamais. Si vous en avez vraiment besoin - bien qu'une telle construction convienne mieux à une classe plutôt qu'à une structure - alors vous devriez utiliser l'initialisation paresseuse. (Et techniquement, vous êtes - complètement inutilement dans chaque, sauf le premier réglage d'une coordonnée - définissant chaque coordonnée deux fois.)
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
la source
5
C'est autorisé mais il n'est pas utilisé quand aucun paramètre n'est spécifié ideone.com/xsLloQ
Motti