Sélection de la méthode générique C #

9

J'essaie d'écrire des algorithmes génériques en C # qui peuvent fonctionner avec des entités géométriques de différentes dimensions.

Dans l'exemple artificiel suivant, j'ai Point2et Point3, tous deux implémentant une IPointinterface simple .

Maintenant, j'ai une fonction GenericAlgorithmqui appelle une fonction GetDim. Il existe plusieurs définitions de cette fonction en fonction du type. Il existe également une fonction de secours qui est définie pour tout ce qui est implémenté IPoint.

Au départ, je m'attendais à ce que la sortie du programme suivant soit 2, 3. Cependant, c'est 0, 0.

interface IPoint {
    public int NumDims { get; } 
}

public struct Point2 : IPoint {
    public int NumDims => 2;
}

public struct Point3 : IPoint {
    public int NumDims => 3;
}

class Program
{
    static int GetDim<T>(T point) where T: IPoint => 0;
    static int GetDim(Point2 point) => point.NumDims;
    static int GetDim(Point3 point) => point.NumDims;

    static int GenericAlgorithm<T>(T point) where T : IPoint => GetDim(point);

    static void Main(string[] args)
    {
        Point2 p2;
        Point3 p3;
        int d1 = GenericAlgorithm(p2);
        int d2 = GenericAlgorithm(p3);
        Console.WriteLine("{0:d}", d1);        // returns 0 !!
        Console.WriteLine("{0:d}", d2);        // returns 0 !!
    }
}

OK, donc pour une raison quelconque, les informations de type concret sont perdues GenericAlgorithm. Je ne comprends pas vraiment pourquoi cela se produit, mais très bien. Si je ne peux pas le faire de cette façon, quelles autres alternatives ai-je?

mohamedmoussa
la source
2
"Il y a aussi une fonction de secours" Quel est le but de cela, exactement? L'intérêt de l'implémentation d'une interface est de garantir la NumDimsdisponibilité de la propriété. Pourquoi l'ignorez-vous dans certains cas?
John Wu
Donc, il compile, essentiellement. Initialement, je pensais que la fonction de repli est nécessaire si au moment de l'exécution le compilateur JIT ne peut pas trouver une implémentation spécialisée pour GetDim(c'est-à-dire que je passe un Point4mais GetDim<Point4>n'existe pas). Cependant, il ne semble pas que le compilateur se soucie de rechercher une implémentation spécialisée.
mohamedmoussa
1
@woggy: Vous dites "il ne semble pas que le compilateur se soucie de rechercher une implémentation spécialisée" comme si c'était une question de paresse de la part des concepteurs et des implémenteurs. Ce n'est pas. C'est une question de représentation des génériques dans .NET. Ce n'est tout simplement pas le même type de spécialisation que les modèles en C ++. Une méthode générique n'est pas compilée séparément pour chaque argument de type - elle est compilée une fois. Il y a certes des avantages et des inconvénients, mais il ne s'agit pas de "déranger".
Jon Skeet
@jonskeet Excuses si mon choix de langue était mauvais, je suis sûr qu'il y a des complexités ici que je n'ai pas considérées. Ma compréhension était que le compilateur ne compile pas de fonctions séparées pour les types de référence, mais il le fait pour les types / structures de valeur, est-ce correct?
mohamedmoussa
@woggy: C'est le compilateur JIT , qui est une question entièrement distincte du compilateur C # - et c'est le compilateur C # qui effectue la résolution de surcharge. L'IL pour la méthode générique n'est généré qu'une seule fois - pas une fois par spécialisation.
Jon Skeet

Réponses:

10

Cette méthode:

static int GenericAlgorithm<T>(T point) where T : IPoint => GetDim(point);

... sera toujours appeler GetDim<T>(T point). La résolution de surcharge est effectuée au moment de la compilation , et à ce stade, il n'y a pas d'autre méthode applicable.

Si vous souhaitez que la résolution de surcharge soit appelée au moment de l'exécution , vous devez utiliser le typage dynamique, par exemple

static int GenericAlgorithm<T>(T point) where T : IPoint => GetDim((dynamic) point);

Mais c'est généralement une meilleure idée d'utiliser l'héritage pour cela - dans votre exemple, vous pouvez évidemment avoir une seule méthode et retourner point.NumDims. Je suppose que dans votre vrai code, il y a une raison pour laquelle l'équivalent est plus difficile à faire, mais sans plus de contexte, nous ne pouvons pas vous conseiller sur la façon d'utiliser l'héritage pour effectuer la spécialisation. Ce sont vos options cependant:

  • Héritage (préféré) pour la spécialisation en fonction du type de temps d'exécution de la cible
  • Typage dynamique pour la résolution de surcharge au moment de l'exécution
Jon Skeet
la source
La vraie situation est que j'ai un AxisAlignedBoundingBox2et AxisAlignedBoundingBox3. J'ai une Containsméthode statique qui est utilisée pour déterminer si une collection de boîtes contient un Line2ou Line3(lequel dépend du type de boîtes). La logique de l'algorithme entre les deux types est exactement la même, sauf que le nombre de dimensions est différent. Il existe également des appels à l' Intersectinterne qui doivent être spécialisés dans le bon type. Je veux éviter les appels de fonction virtuels / dynamiques, c'est pourquoi j'utilise des génériques ... bien sûr, je peux simplement copier / coller le code et continuer.
mohamedmoussa le
1
@woggy: Il est assez difficile de visualiser cela à partir d'une simple description. Si vous voulez de l'aide pour essayer de le faire en utilisant l'héritage, je vous suggère de créer une nouvelle question avec un exemple minimal mais complet.
Jon Skeet
OK, ça va, j'accepte cette réponse pour l'instant car il semble que je n'ai pas donné un bon exemple.
mohamedmoussa
6

À partir de C # 8.0, vous devriez être en mesure de fournir une implémentation par défaut pour votre interface, plutôt que d'exiger la méthode générique.

interface IPoint {
    int NumDims { get => 0; }
}

L'implémentation d'une méthode générique et les surcharges par IPointimplémentation violent également le principe de substitution de Liskov (le L dans SOLID). Il serait préférable de pousser l' algorithme dans chaque IPointimplémentation, ce qui signifie que vous ne devriez avoir besoin que d'un seul appel de méthode:

static int GetDim(IPoint point) => point.NumDims;
Matthew Layton
la source
3

Modèle de visiteur

comme alternative à l' dynamicutilisation, vous pouvez utiliser un modèle de visiteur comme ci-dessous:

interface IPoint
{
    public int NumDims { get; }
    public int Accept(IVisitor visitor);
}

public struct Point2 : IPoint
{
    public int NumDims => 2;

    public int Accept(IVisitor visitor)
    {
        return visitor.Visit(this);
    }
}

public struct Point3 : IPoint
{
    public int NumDims => 3;

    public int Accept(IVisitor visitor)
    {
        return visitor.Visit(this);
    }
}

public class Visitor : IVisitor
{
    public int Visit(Point2 toVisit)
    {
        return toVisit.NumDims;
    }

    public int Visit(Point3 toVisit)
    {
        return toVisit.NumDims;
    }
}

public interface IVisitor<T>
{
    int Visit(T toVisit);
}

public interface IVisitor : IVisitor<Point2>, IVisitor<Point3> { }

class Program
{
    static int GetDim<T>(T point) where T : IPoint => 0;
    static int GetDim(Point2 point) => point.NumDims;
    static int GetDim(Point3 point) => point.NumDims;

    static int GenericAlgorithm<T>(T point) where T : IPoint => point.Accept(new Visitor());

    static void Main(string[] args)
    {
        Point2 p2;
        Point3 p3;
        int d1 = GenericAlgorithm(p2);
        int d2 = GenericAlgorithm(p3);
        Console.WriteLine("{0:d}", d1);        // returns 2
        Console.WriteLine("{0:d}", d2);        // returns 3
    }
}
Fab
la source
1

Pourquoi ne définissez-vous pas la fonction GetDim dans la classe et l'interface? En fait, vous n'avez pas besoin de définir la fonction GetDim, utilisez simplement la propriété NumDims.

player2135
la source