Appel du constructeur de type de base n'importe où

J'ai eu une interview récemment, et entre autres il y avait une question sur l'ordre d'appel des constructeurs en C #. Après avoir répondu, la personne interrogée a décidé de démontrer son érudition et a déclaré qu'en Java, un constructeur de type de base peut être appelé n'importe où dans un constructeur de type dérivé, et C #, bien sûr, y perd.



La déclaration s'est avérée être un mensonge, un mensonge et une provocation
image



Mais cela n'avait plus d'importance, car le défi était accepté.



image



Avertissement
. . . .



Entraînement



Nous créons une chaîne d'héritage. Pour plus de simplicité, nous utiliserons des constructeurs sans paramètre. Dans le constructeur, nous afficherons des informations sur le type et l'identifiant de l'objet sur lequel il est appelé.



public class A
{
    public A()
    {
        Console.WriteLine($"Type '{nameof(A)}' .ctor called on object #{GetHashCode()}");
    }
}

public class B : A
{
    public B()
    {
        Console.WriteLine($"Type '{nameof(B)}' .ctor called on object #{GetHashCode()}");
    }
}

public class C : B
{
    public C()
    {
        Console.WriteLine($"Type '{nameof(C)}' .ctor called on object #{GetHashCode()}");
    }
}


Exécutez le programme:



class Program
{
    static void Main()
    {
        new C();
    }
}


Et nous obtenons la sortie:



Type 'A' .ctor called on object #58225482

Type 'B' .ctor called on object #58225482

Type 'C' .ctor called on object #58225482



Digression lyrique
, . , . , . :



public A() : this() { } // CS0516  Constructor 'A.A()' cannot call itself


:



public A() : this(new object()) { }
public A(object _) : this(0) { }
public A(int _) : this() { } // CS0768  Constructor 'A.A(int)' cannot call itself through another constructor


Suppression du code en double



Ajoutez une classe d'assistance:



internal static class Extensions
{
    public static void Trace(this object obj) =>
        Console.WriteLine($"Type '{obj.GetType().Name}' .ctor called on object #{obj.GetHashCode()}");
}


Et nous remplaçons dans tous les constructeurs



Console.WriteLine($"Type '{nameof(...)}' .ctor called on object #{GetHashCode()}");


sur



this.Trace();


Cependant, le programme sort maintenant: Dans notre cas, l'astuce suivante peut être utilisée. Qui connaît les types de temps de compilation? Compilateur. Il sélectionne également les surcharges de méthode en fonction de ces types. Et pour les types et méthodes génériques, il génère également des entités construites. Par conséquent, nous retournons l'inférence de type correcte en réécrivant la méthode Trace comme suit:



Type 'C' .ctor called on object #58225482

Type 'C' .ctor called on object #58225482

Type 'C' .ctor called on object #58225482







public static void Trace<T>(this T obj) =>
    Console.WriteLine($"Type '{typeof(T).Name}' .ctor called on object #{obj.GetHashCode()}");


Accéder à un constructeur de type de base



C'est là que la réflexion vient à la rescousse. Ajoutez une méthode aux extensions :



public static Action GetBaseConstructor<T>(this T obj) =>
    () => typeof(T)
          .BaseType
          .GetConstructor(Type.EmptyTypes)
          .Invoke(obj, Array.Empty<object>());


Ajoutez la propriété aux types B et C :



private Action @base => this.GetBaseConstructor();


Appeler un constructeur de type de base n'importe où



Modifiez le contenu des constructeurs B et C en:



this.Trace();
@base();


Maintenant, la sortie ressemble à ceci:



Type 'A' .ctor called on object #58225482

Type 'B' .ctor called on object #58225482

Type 'A' .ctor called on object #58225482

Type 'C' .ctor called on object #58225482

Type 'A' .ctor called on object #58225482

Type 'B' .ctor called on object #58225482

Type 'A' .ctor called on object #58225482



Modification de l'ordre d'appel des constructeurs de type de base



À l'intérieur du type A, créez un type d'assistance:



protected class CtorHelper
{
    private CtorHelper() { }
}


Puisque seule la sémantique est importante ici, il est conseillé de rendre le constructeur de type privé. L'instanciation n'a pas de sens. Le type est uniquement destiné à faire la distinction entre les surcharges des constructeurs de type A et celles qui en dérivent. Pour la même raison, le type doit être placé à l'intérieur de A et protégé.



Ajoutez les constructeurs appropriés à A , B et C :



protected A(CtorHelper _) { }
protected B(CtorHelper _) { }
protected C(CtorHelper _) { }


Pour les types B et C, ajoutez un appel à tous les constructeurs:



: base(null)


En conséquence, les classes devraient ressembler à ceci
internal static class Extensions
{
    public static Action GetBaseConstructor<T>(this T obj) =>
        () => typeof(T)
        .BaseType
        .GetConstructor(Type.EmptyTypes)
        .Invoke(obj, Array.Empty<object>());

    public static void Trace<T>(this T obj) =>
        Console.WriteLine($"Type '{typeof(T).Name}' .ctor called on object #{obj.GetHashCode()}");
}

public class A
{
    protected A(CtorHelper _) { }

    public A()
    {
        this.Trace();
    }

    protected class CtorHelper
    {
        private CtorHelper() { }
    }
}

public class B : A
{
    private Action @base => this.GetBaseConstructor();

    protected B(CtorHelper _) : base(null) { }

    public B() : base(null)
    {
        this.Trace();
        @base();
    }
}

public class C : B
{
    private Action @base => this.GetBaseConstructor();

    protected C(CtorHelper _) : base(null) { }

    public C() : base(null)
    {
        this.Trace();
        @base();
    }
}


Et la sortie devient:



Type 'C' .ctor called on object #58225482

Type 'B' .ctor called on object #58225482

Type 'A' .ctor called on object #58225482



Le naïf naïf pense que le compilateur a triché
image



Comprendre le résultat



En ajoutant une méthode aux extensions :



public static void TraceSurrogate<T>(this T obj) =>
    Console.WriteLine($"Type '{typeof(T).Name}' surrogate .ctor called on object #{obj.GetHashCode()}");


et en l'appelant dans tous les constructeurs qui acceptent CtorHelper , nous obtenons la sortie suivante : L'ordre des constructeurs selon le principe de base / dérivé, bien sûr, n'a pas changé. Mais tout de même, l'ordre des constructeurs disponibles pour le code client qui portent la charge sémantique a été modifié grâce à la redirection via des appels aux constructeurs helper inaccessibles au client.



Type 'A' surrogate .ctor called on object #58225482

Type 'B' surrogate .ctor called on object #58225482

Type 'C' .ctor called on object #58225482

Type 'A' surrogate .ctor called on object #58225482

Type 'B' .ctor called on object #58225482

Type 'A' .ctor called on object #58225482






All Articles