82 votes

Pourquoi l'appel récursif d'un constructeur fait-il compiler du code C# invalide ?

Après avoir regardé le webinaire Jon Skeet inspecte ReSharper J'ai commencé à jouer un peu avec et j'ai trouvé que le code suivant est du code C# valide (par valide, je veux dire qu'il compile).

class Foo
{
    int a = null;
    int b = AppDomain.CurrentDomain;
    int c = "string to int";
    int d = NonExistingMethod();
    int e = Invalid<Method>Name<<Indeeed();

    Foo()       :this(0)  { }
    Foo(int v)  :this()   { }
}

Comme nous le savons tous probablement, l'initialisation des champs est déplacée dans le constructeur par le compilateur. Donc si vous avez un champ comme int a = 42; vous aurez a = 42 sur todo constructeurs. Mais si vous avez un constructeur qui appelle un autre constructeur, vous aurez du code d'initialisation uniquement dans le constructeur appelé.

Par exemple, si vous avez un constructeur avec des paramètres appelant le constructeur par défaut, vous aurez une affectation a = 42 uniquement dans le constructeur par défaut.

Pour illustrer le deuxième cas, le code suivant :

class Foo
{
    int a = 42;

    Foo() :this(60)  { }
    Foo(int v)       { }
}

Se compile en :

internal class Foo
{
    private int a;

    private Foo()
    {
        this.ctor(60);
    }

    private Foo(int v)
    {
        this.a = 42;
        base.ctor();
    }
}

Le problème principal est donc que mon code, donné au début de cette question, est compilé en :

internal class Foo
{
    private int a;
    private int b;
    private int c;
    private int d;
    private int e;

    private Foo()
    {
        this.ctor(0);
    }

    private Foo(int v)
    {
        this.ctor();
    }
}

Comme vous pouvez le voir, le compilateur ne peut pas décider où placer l'initialisation des champs et, par conséquent, ne la place nulle part. Notez également qu'il n'y a pas de base appels au constructeur. Bien sûr, aucun objet ne peut être créé, et vous vous retrouverez toujours avec StackOverflowException si vous essayez de créer une instance de Foo .

J'ai deux questions :

Pourquoi le compilateur autorise-t-il les appels récursifs aux constructeurs ?

Pourquoi observons-nous un tel comportement du compilateur pour les champs, initialisés dans une telle classe ?


Quelques notes : ReSharper vous avertit avec Possible cyclic constructor calls . De plus, en Java, de tels appels de constructeurs ne seront pas compilés, le compilateur Java est donc plus restrictif dans ce scénario (Jon a mentionné cette information lors du webinaire).

Cela rend ces questions plus intéressantes, car avec tout le respect dû à la communauté Java, le compilateur C# est au moins plus moderne.

Il a été compilé en utilisant C# 4.0 y C# 5.0 et décompilés à l'aide de dotPeek .

11voto

Jeppe Stig Nielsen Points 17887

Une découverte intéressante.

Il semble qu'il n'existe réellement que deux types de constructeurs d'instance :

  1. Un constructeur d'instance qui enchaîne un autre constructeur d'instance du même type avec le : this( ...) la syntaxe.
  2. Un constructeur d'instance qui enchaîne un constructeur d'instance de la classe de base . Cela inclut les constructeurs d'instance où aucun chainig n'est spécifié, puisque : base() est la valeur par défaut.

(Je n'ai pas tenu compte du constructeur d'instance de System.Object qui est un cas particulier. System.Object n'a pas de classe de base ! Mais System.Object n'a pas non plus de champs).

Les initialisateurs de champs d'instance qui pourraient être présents dans la classe, doivent être copiés au début du corps de tous les constructeurs d'instance de type 2. ci-dessus, alors qu'aucun constructeur d'instance de type 1. ont besoin du code d'affectation du champ.

Donc, apparemment, il n'y a pas besoin pour le compilateur C# de faire une analyse des constructeurs de type 1. pour voir s'il y a des cycles ou non.

Maintenant, votre exemple donne une situation où todo Les constructeurs d'instance sont de type 1. . Dans cette situation, le code d'initialisation du champ ne doit être placé nulle part. Il semble donc qu'il ne soit pas analysé très profondément.

Il s'avère que lorsque tous les constructeurs d'instance sont de type 1. Vous pouvez même dériver d'une classe de base qui n'a pas de constructeur accessible. La classe de base doit cependant être non scellée. Par exemple, si vous écrivez une classe avec seulement private les personnes peuvent toujours dériver de votre classe si elles font en sorte que tous les constructeurs d'instance de la classe dérivée soient de type 1. ci-dessus. Cependant, l'expression de création d'un nouvel objet ne se terminera jamais, bien entendu. Pour créer des instances de la classe dérivée, il faudrait "tricher" et utiliser des trucs comme la fonction System.Runtime.Serialization.FormatterServices.GetUninitializedObject méthode.

Un autre exemple : Le site System.Globalization.TextInfo a seulement un internal constructeur d'instance. Mais vous pouvez toujours dériver de cette classe dans une autre assemblée que la suivante mscorlib.dll avec cette technique.

Enfin, en ce qui concerne le

Invalid<Method>Name<<Indeeed()

la syntaxe. Selon les règles du C#, cela doit se lire comme suit

(Invalid < Method) > (Name << Indeeed())

car l'opérateur de décalage à gauche << a une priorité plus élevée que l'opérateur "moins que". < et l'opérateur plus grand que > . Ces deux derniers opérateurs ont la même précédence, et sont donc évalués par la règle d'association à gauche. Si les types étaient

MySpecialType Invalid;
int Method;
int Name;
int Indeed() { ... }

et si le MySpecialType a introduit un (MySpecialType, int) surcharge de la operator < alors l'expression

Invalid < Method > Name << Indeeed()

serait légale et significative.


À mon avis, il serait préférable que le compilateur émette un avertissement dans ce scénario. Par exemple, il pourrait dire unreachable code detected et indiquer le numéro de ligne et de colonne de l'initialisateur de champ qui n'est jamais traduit en IL.

5voto

Damien_The_Unbeliever Points 102139

Je pense que parce que le spécification du langage n'exclut que l'invocation directe du même constructeur que celui qui est défini.

De 10.11.1 :

Tous les constructeurs d'instance (sauf ceux de la classe object ) incluent implicitement une invocation d'un autre constructeur d'instance immédiatement avant le corps du constructeur. Le constructeur à invoquer implicitement est déterminé par le constructeur-initialisateur

...

  • Un initialisateur de constructeur d'instance de la forme this(argument-list<code>opt</code>) provoque l'appel d'un constructeur d'instance de la classe elle-même ... Si une déclaration de constructeur d'instance inclut un initialisateur de constructeur qui invoque le constructeur lui-même, une erreur de compilation se produit.

Cette dernière phrase semble seulement exclure l'appel direct lui-même comme produisant une erreur de compilation, par exemple

Foo() : this() {}

est illégale.


J'admets cependant que je ne vois pas de raison spécifique de l'autoriser. Bien sûr, au niveau de l'IL, de telles constructions sont autorisées parce que différents constructeurs d'instance peuvent être sélectionnés au moment de l'exécution, je crois - vous pouvez donc avoir une récursion à condition qu'elle se termine.


Je pense que l'autre raison pour laquelle il ne signale pas ou n'avertit pas est qu'il n'en a pas besoin. détecter cette situation. Imaginez parcourir des centaines de constructeurs différents, juste pour voir si un cycle fait existe - alors que toute tentative d'utilisation va rapidement (comme nous le savons) exploser au moment de l'exécution, pour un cas assez marginal.

Lorsqu'il génère du code pour chaque constructeur, il ne prend en compte que les éléments suivants constructor-initializer les initialisateurs de champs et le corps du constructeur - il ne tient compte d'aucun autre code :

  • Si constructor-initializer est un constructeur d'instance pour la classe elle-même, il n'émet pas les initialisateurs de champ - il émet le constructeur d'instance de la classe. constructor-initializer l'appel et ensuite le corps.

  • Si constructor-initializer est un constructeur d'instance pour la classe de base directe, il émet les initialisateurs de champs, puis la classe constructor-initializer l'appel, puis le corps.

Dans les deux cas, il n'a pas besoin d'aller chercher ailleurs - il n'est donc pas "incapable" de décider où placer les initialisateurs de champ - il suit simplement des règles simples qui ne prennent en compte que le constructeur actuel.

2voto

Stochastically Points 4305

Votre exemple

class Foo
{
    int a = 42;

    Foo() :this(60)  { }
    Foo(int v)       { }
}

fonctionnera bien, dans le sens où vous pourrez instancier cet objet Foo sans problème. Cependant, le code suivant ressemblerait davantage à celui que vous demandez

class Foo
{
    int a = 42;

    Foo() :this(60)     { }
    Foo(int v) : this() { }
}

Cela et votre code vont créer un stackoverflow ( !), parce que la récursion ne s'arrête jamais. Votre code est donc ignoré car il ne s'exécute jamais.

En d'autres termes, le compilateur ne peut pas décider où placer le code défectueux parce qu'il peut dire que la récursion ne culmine jamais. Je pense que c'est parce qu'il doit le placer là où il ne sera appelé qu'une seule fois, mais la nature récursive des constructeurs rend cela impossible.

Récursion dans le sens où un constructeur crée des instances de lui-même. dans le corps du constructeur me semble logique, car il pourrait être utilisé, par exemple, pour instancier des arbres où chaque nœud pointe vers d'autres nœuds. Mais la récursion via les préconstructeurs du type illustré par cette question ne peut jamais atteindre le fond, donc il serait logique pour moi que cela soit interdit.

0voto

Jens Timmerman Points 1448

Je pense que cela est autorisé parce que vous pouvez (pourriez) toujours attraper l'exception et en faire quelque chose de significatif.

L'initialisation ne sera jamais exécutée, et il est presque certain qu'une StackOverflowException sera levée. Mais cela peut toujours être un comportement souhaité, et ne signifie pas toujours que le processus doit se planter.

Comme expliqué ici https://stackoverflow.com/a/1599236/869482

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X