web-dev-qa-db-fra.com

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

Dans .NET, un type de valeur (C # struct) ne peut pas avoir un constructeur sans paramètre. Selon cet article ceci est requis par la spécification CLI. Qu'est-ce qui se passe 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 concerne 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 #, Rational par défaut est 0/0, ce qui n’est pas très cool.

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


Jon Skeet a répondu:

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

 Rational[] fractions = new Rational[1000];

Devrait-il passer par votre constructeur 1000 fois?

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

Dans mon esprit, cette raison n’est pas assez forte pour empêcher la définition d’un constructeur par défaut.

218
Motti

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 des 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 modifié la réponse ci-dessous en raison de la perspicacité de Grauenwolf dans le CLR.

Le CLR permet aux types de valeur d'avoir des constructeurs sans paramètre, mais pas C #. Je pense que c'est parce que cela créerait une attente selon laquelle le constructeur serait appelé alors qu'il ne le ferait pas. Par exemple, considérons ceci:

MyStruct[] foo = new MyStruct[1000];

Le CLR est capable de le faire très efficacement 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, ce n'est pas le cas - si vous do avez un constructeur sans paramètre, il ne sera pas exécuté 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 compter sur aucune initialisation". Maintenant, ils pourraient ont permis la définition de constructeurs sans paramètre, mais n'exigent donc pas que ce constructeur soit exécuté dans tous les cas - mais cela aurait créé davantage 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 le fait:

Rational[] fractions = new Rational[1000];

Devrait-il passer par votre constructeur 1000 fois?

  • Sinon, nous aboutissons à 1000 rationnels invalides
  • Si tel est le cas, nous risquons de perdre beaucoup de travail si nous allons 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 ne doivent pas obligatoirement avoir de constructeur en ce qui concerne le CLR - bien qu'il s'avère que peut si vous l'écrivez en IL. Lorsque vous écrivez "new Guid()" en C #, il émet un IL différent de ce que vous obtenez si vous appelez un constructeur normal. Voir cette SO question pour un peu plus sur cet aspect.

I suspect qu'il n'y a pas de types de valeur dans la structure avec des constructeurs sans paramètre. NDepend pourrait sans doute me dire si je le lui ai demandé assez gentiment ... Le fait que C # l'interdit, c'est un indice assez important pour que je pense que c'est probablement une mauvaise idée.

173
Jon Skeet

Une structure est un type de valeur et un type de valeur doit avoir une valeur par défaut dès sa déclaration.

MyClass m;
MyStruct m2;

Si vous déclarez deux champs comme ci-dessus sans instancier l'un ou l'autre, casser le débogueur, m sera null mais m2 ne le sera pas. Dans ce cas, un constructeur sans paramètre n’aurait aucun sens. En fait, tout constructeur d’une structure attribue des valeurs, la chose elle-même existe déjà simplement 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!

40
user42467

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

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

Et utilisez-le comme:

var rat = Rational.One;
14
Skasel

Bien que le CLR le permette, C # n'autorise pas les constructs à avoir un constructeur par défaut sans paramètre. La raison en est que, pour un type de valeur, les compilateurs ne génèrent par défaut ni un constructeur par défaut, ni un appel au constructeur par défaut. Ainsi, même s'il vous arrivait de définir un constructeur par défaut, celui-ci ne sera pas appelé et cela ne ferait que vous dérouter.

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.

La principale 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 mot clé new. Cela fonctionne plutôt comme un int; vous pouvez y accéder directement.

Les structures ne peuvent pas contenir de constructeurs explicites sans paramètre. Les membres de la 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 tout à zéro, ce qui constituerait un comportement inattendu. Le runtime .NET interdit donc les constructeurs par défaut pour une structure.

14
Adiii

Explication plus courte: 

En C++, struct et class n'étaient que deux faces d'une même pièce. La seule différence réelle est que l'une était publique par défaut et l'autre était privée.

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

13
Joel Coehoorn

Juste un cas particulier. 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.

3
Jonathan Allen

Vous ne pouvez pas définir de 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 les supporte.

1
Jonathan Allen

Voici ma solution au dilemme du constructeur sans défaut. Je sais que c'est une solution tardive, mais je pense qu'il est intéressant 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 );
    }
}

en ignorant le fait que j'ai une structure statique appelée null, (Remarque: ceci concerne uniquement tous les quadrants positifs), en utilisant get; set; en C #, vous pouvez avoir un essai/attraper/enfin, pour traiter les erreurs lorsqu'un type de données particulier n'est pas initialisé par le constructeur par défaut Point2D (). Je suppose que cette solution est insaisissable pour certaines personnes. C’est surtout la raison pour laquelle j’ajoute le mien. Utiliser les fonctionnalités getter et setter de C # vous permettra d’éviter ce non-sens du constructeur par défaut et d’essayer ce que vous n’avez pas initialisé. Pour moi, cela fonctionne bien, pour quelqu'un d'autre, vous voudrez peut-être ajouter certaines déclarations if. Donc, dans le cas où vous voudriez une configuration de numérateur/dénominateur, ce code pourrait vous aider. Je voudrais juste réitérer que cette solution n'a pas l'air agréable, qu'elle fonctionne probablement encore pire du point de vue de l'efficacité, mais que, pour quelqu'un venant d'une ancienne version de C #, l'utilisation de types de données de type 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;
    }
}
1
G1xb17

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

utilisez des décalages pour déplacer les valeurs de 0 par défaut vers la valeur de votre choix. 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 propriété afin qu'ils restent protégés contre les accès directs au code.)

Cette solution fonctionne pour des structures simples avec uniquement des types valeur (pas de type ref ou 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 que cette réponse, cette approche n’est pas un cas spécial, mais son décalage d’utilisation qui fonctionnera pour toutes les gammes.

exemple avec enums 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 ou non dans votre cas. juste examiner. mais vous avez l'idée générale.

1
M.kazem Akhgary
public struct Rational 
{
    private long numerator;
    private long denominator;

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