462 votes

Existe-t-il une contrainte qui limite ma méthode générique aux types numériques ?

Quelqu'un peut-il me dire s'il existe un moyen de limiter l'argument d'un type générique avec les génériques ? T à seulement :

  • Int16
  • Int32
  • Int64
  • UInt16
  • UInt32
  • UInt64

Je suis au courant de la where mais ne trouve pas d'interface pour le mot-clé sólo ces types,

Quelque chose comme :

static bool IntegerFunction<T>(T value) where T : INumeric

4 votes

Il existe actuellement plusieurs propositions en C# qui permettraient d'accomplir cela, mais AFAIK, aucune d'entre elles n'est allée plus loin que des explorations/discussions préliminaires. Voir Exploration : Formes et extensions , Exploration : Rôles, interfaces d'extension et membres d'interfaces statiques , Champion "Type Classes (aka Concepts, Structural Generic Constraints)" y Proposition : Les types génériques doivent prendre en charge les opérateurs

196voto

Konrad Rudolph Points 231505

C# ne prend pas cela en charge. Hejlsberg a décrit les raisons pour lesquelles cette fonctionnalité n'a pas été implémentée dans un entretien avec Bruce Eckel :

Et il n'est pas certain que la complexité accrue vaille la peine d'obtenir un faible rendement. Si quelque chose que vous voulez faire n'est pas directement pris en charge par le système de contraintes, vous pouvez le faire avec un modèle d'usine. Vous pourriez avoir un Matrix<T> par exemple, et dans ce Matrix vous souhaitez définir une méthode de produit de points. Bien entendu, cela signifie que vous devez comprendre comment multiplier deux T mais on ne peut pas dire que c'est une contrainte, du moins pas si les T es int , double ou float . Mais ce que vous pourriez faire, c'est d'avoir votre Matrix prend comme argument un Calculator<T> et en Calculator<T> ont une méthode appelée multiply . Vous mettez cela en œuvre et vous le transmettez au Matrix .

Cependant, cela conduit à un code assez alambiqué, dans lequel l'utilisateur doit fournir ses propres Calculator<T> pour chaque T qu'ils souhaitent utiliser. Tant qu'il n'est pas nécessaire qu'il soit extensible, c'est-à-dire si vous souhaitez simplement prendre en charge un nombre fixe de types, tels que int y double vous pouvez vous contenter d'une interface relativement simple :

var mat = new Matrix<int>(w, h);

( Mise en œuvre minimale dans une Gist GitHub. )

Cependant, dès que vous souhaitez que l'utilisateur puisse fournir ses propres types personnalisés, vous devez ouvrir cette implémentation afin que l'utilisateur puisse fournir ses propres Calculator instances. Par exemple, pour instancier une matrice qui utilise une implémentation personnalisée de la virgule flottante décimale, DFP vous devrez écrire ce code :

var mat = new Matrix<DFP>(DfpCalculator.Instance, w, h);

et mettre en œuvre tous les membres pour DfpCalculator : ICalculator<DFP> .

Une autre solution, qui présente malheureusement les mêmes limites, consiste à travailler avec des classes de politiques, comme indiqué dans la réponse de Sergey Shandar .

26 votes

Btw, MiscUtil fournit une classe générique qui fait exactement cela ; Operator / Operator<T> ; yoda.arachsys.com/csharp/miscutil/usage/genericoperators.html

1 votes

@Mark : bon commentaire. Cependant, juste pour être clair, je ne pense pas que Hejlsberg faisait référence à la génération de code comme solution au problème, comme vous le faites dans la section Operator<T> (puisque l'interview a été réalisée bien avant l'existence du code Expressions même si l'on peut bien sûr utiliser Reflection.Emit ) - et je serais vraiment intéressés par son solution de rechange.

0 votes

@Konrad Rudolph : Je pense que cette réponse à une question similaire explique la solution de Hejlsberg. L'autre classe générique est rendue abstraite. Étant donné que vous devez implémenter l'autre classe générique pour chaque type que vous souhaitez prendre en charge, le code sera dupliqué, mais cela signifie que vous ne pouvez instancier la classe générique d'origine qu'avec un type pris en charge.

125voto

Jeroen Vannevel Points 18676

Compte tenu de la popularité de cette question et de l'intérêt que suscite une telle fonction, je suis surpris de constater qu'il n'y a pas encore de réponse impliquant T4.

Dans cet exemple de code, je vais montrer un exemple très simple de la façon dont vous pouvez utiliser le puissant moteur de modélisation pour faire ce que le compilateur fait pratiquement en coulisses avec les génériques.

Au lieu de passer par des étapes et de sacrifier la certitude au moment de la compilation, vous pouvez simplement générer la fonction que vous voulez pour chaque type que vous aimez et l'utiliser en conséquence (au moment de la compilation !).

Pour ce faire :

  • Créer un nouveau Modèle de texte appelé GenericNumberMethodTemplate.tt .
  • Supprimez le code généré automatiquement (vous en conserverez la plus grande partie, mais une partie n'est pas nécessaire).
  • Ajoutez l'extrait suivant :

    <#@ template language="C#" #> <#@ output extension=".cs" #> <#@ assembly name="System.Core" #>

    <# Type[] types = new[] { typeof(Int16), typeof(Int32), typeof(Int64), typeof(UInt16), typeof(UInt32), typeof(UInt64) };

    >

    using System; public static class MaxMath { <# foreach (var type in types) {

    >

        public static <#= type.Name #> Max (<#= type.Name #> val1, <#= type.Name #> val2) {
            return val1 > val2 ? val1 : val2;
        }
    <#
    } #>

    }

C'est tout. Vous avez terminé.

L'enregistrement de ce fichier le compilera automatiquement dans ce fichier source :

using System;
public static class MaxMath {
    public static Int16 Max (Int16 val1, Int16 val2) {
        return val1 > val2 ? val1 : val2;
    }
    public static Int32 Max (Int32 val1, Int32 val2) {
        return val1 > val2 ? val1 : val2;
    }
    public static Int64 Max (Int64 val1, Int64 val2) {
        return val1 > val2 ? val1 : val2;
    }
    public static UInt16 Max (UInt16 val1, UInt16 val2) {
        return val1 > val2 ? val1 : val2;
    }
    public static UInt32 Max (UInt32 val1, UInt32 val2) {
        return val1 > val2 ? val1 : val2;
    }
    public static UInt64 Max (UInt64 val1, UInt64 val2) {
        return val1 > val2 ? val1 : val2;
    }
}

Dans votre main vous pouvez vérifier que vous avez une certitude au moment de la compilation :

namespace TTTTTest
{
    class Program
    {
        static void Main(string[] args)
        {
            long val1 = 5L;
            long val2 = 10L;
            Console.WriteLine(MaxMath.Max(val1, val2));
            Console.Read();
        }
    }
}

enter image description here

Je vais devancer une remarque : non, il ne s'agit pas d'une violation du principe DRY. Le principe DRY est là pour empêcher les gens de dupliquer le code à plusieurs endroits, ce qui rendrait l'application difficile à maintenir.

Ce n'est pas du tout le cas ici : si vous souhaitez un changement, il vous suffit de modifier le modèle (une source unique pour toute votre génération !) et le tour est joué.

Afin de l'utiliser avec vos propres définitions, ajoutez une déclaration d'espace de noms (assurez-vous que c'est la même que celle où vous définirez votre propre implémentation) à votre code généré et marquez la classe comme partial . Ensuite, ajoutez ces lignes à votre fichier modèle pour qu'il soit inclus dans la compilation éventuelle :

<#@ import namespace="TheNameSpaceYouWillUse" #>
<#@ assembly name="$(TargetPath)" #>

Soyons honnêtes : c'est plutôt cool.

Clause de non-responsabilité : cet échantillon a été fortement influencé par Metaprogramming in .NET par Kevin Hazzard et Jason Bock, Manning Publications .

0 votes

C'est très bien, mais serait-il possible de modifier cette solution pour que les méthodes acceptent un type générique ? T qui est ou hérite des différents IntX classes ? J'aime cette solution parce qu'elle permet de gagner du temps, mais pour qu'elle résolve le problème à 100% (bien qu'elle ne soit pas aussi agréable que si C# avait pris en charge ce type de contrainte, intégrée), chacune des méthodes générées devrait toujours être générique de sorte qu'elle puisse renvoyer un objet d'un type qui hérite de l'une des classes IntXX classes.

0 votes

@ZacharyKniebel : toute l'idée derrière cette solution est que l'on ne crée pas de type générique. Ce qu'une méthode générique fait en coulisses, c'est (grosso modo) créer une méthode exactement comme celle-ci : elle remplace le paramètre générique par le type réel et l'utilise. Une méthode générique voudrait imposer des contraintes sur le type qui lui est transmis pour les types numériques et créer ensuite des implémentations réelles avec le type concret. Ici, nous sautons cette partie et créons immédiatement les types concrets : tout ce que vous avez à faire est d'ajouter les types que vous voulez à votre liste dans le fichier modèle et de le laisser générer les types concrets.

0 votes

J'approche de la limite de mes connaissances sur la façon dont les types génériques sont compilés, donc je m'excuse si je me trompe, mais si un type générique génère toutes ces méthodes dans les coulisses, alors ne doit-il pas le faire en trouvant toutes les classes qui héritent des types contraints, et en générant une méthode similaire pour chacune d'entre elles ? En d'autres termes, ne devrait-il pas rechercher toutes les classes qui héritent de chacun des types contraints ? IntXX types ? Si j'ai raison, votre solution ne serait-elle pas en deçà de la solution souhaitée puisqu'elle ne tient pas compte, dans son état actuel, des types hérités ?

101voto

Keith Points 46288

Il n'y a pas de contrainte pour cela. C'est un vrai problème pour tous ceux qui veulent utiliser des génériques pour des calculs numériques.

J'irais même jusqu'à dire que nous avons besoin

static bool GenericFunction<T>(T value) 
    where T : operators( +, -, /, * )

Ou encore

static bool GenericFunction<T>(T value) 
    where T : Add, Subtract

Malheureusement, vous ne disposez que d'interfaces, de classes de base et des mots clés struct (doit être de type valeur), class (doit être de type référence) et new() (doit avoir un constructeur par défaut)

Vous pourriez envelopper le nombre dans quelque chose d'autre (similaire à INullable<T> ) comme ici sur codeproject .


Il est possible d'appliquer la restriction au moment de l'exécution (en reflétant les opérateurs ou en vérifiant les types), mais cela fait perdre l'avantage d'avoir le générique en premier lieu.

2 votes

Je me demande si vous avez vu la prise en charge des opérateurs génériques par MiscUtil... yoda.arachsys.com/csharp/miscutil/usage/genericoperators.html

11 votes

Oui - Jon Skeet m'a indiqué qu'ils étaient utilisés pour autre chose il y a quelque temps (mais après cette réponse qui date d'un an) - c'est une idée intelligente, mais j'aimerais quand même avoir un support de contrainte approprié.

2 votes

Attendez, where T : operators( +, -, /, * ) est légal en C# ? Désolé pour cette question de débutant.

68voto

Sergey Shandar Points 1123

Solution de contournement à l'aide de politiques :

interface INumericPolicy<T>
{
    T Zero();
    T Add(T a, T b);
    // add more functions here, such as multiplication etc.
}

struct NumericPolicies:
    INumericPolicy<int>,
    INumericPolicy<long>
    // add more INumericPolicy<> for different numeric types.
{
    int INumericPolicy<int>.Zero() { return 0; }
    long INumericPolicy<long>.Zero() { return 0; }
    int INumericPolicy<int>.Add(int a, int b) { return a + b; }
    long INumericPolicy<long>.Add(long a, long b) { return a + b; }
    // implement all functions from INumericPolicy<> interfaces.

    public static NumericPolicies Instance = new NumericPolicies();
}

Algorithmes :

static class Algorithms
{
    public static T Sum<P, T>(this P p, params T[] a)
        where P: INumericPolicy<T>
    {
        var r = p.Zero();
        foreach(var i in a)
        {
            r = p.Add(r, i);
        }
        return r;
    }

}

Utilisation :

int i = NumericPolicies.Instance.Sum(1, 2, 3, 4, 5);
long l = NumericPolicies.Instance.Sum(1L, 2, 3, 4, 5);
NumericPolicies.Instance.Sum("www", "") // compile-time error.

La solution est sûre au moment de la compilation. Cadre CityLizard fournit une version compilée pour .NET 4.0. Le fichier est lib/NETFramework4.0/CityLizard.Policy.dll.

Il est également disponible dans Nuget : https://www.nuget.org/packages/CityLizard/ . Voir Politique du lézard des villes.I structure.

0 votes

J'ai rencontré des problèmes avec ce modèle lorsqu'il y a moins d'arguments de fonction que de paramètres génériques. Ouvert stackoverflow.com/questions/36048248/

0 votes

Une raison pour laquelle l'utilisation de struct ? que se passe-t-il si j'utilise une classe singleton à la place et que je change l'instance en public static NumericPolicies Instance = new NumericPolicies(); et ajoutez ce constructeur private NumericPolicies() { } .

0 votes

@M.kazemAkhgary vous pouvez utiliser le singleton. Je préfère la structure. En théorie, elle peut être optimisée par le compilateur/CLR car la structure ne contient aucune information. Dans le cas d'un singleton, vous passerez toujours une référence, ce qui peut ajouter une pression supplémentaire sur le GC. Un autre avantage est que la structure ne peut pas être nulle :-) .

18voto

Marc Gravell Points 482669

Cette question est un peu une FAQ, donc je la poste en tant que wiki (puisque j'ai déjà posté des questions similaires, mais celle-ci est plus ancienne) ; de toute façon...

Quelle version de .NET utilisez-vous ? Si vous utilisez .NET 3.5, j'ai une question à vous poser. mise en œuvre des opérateurs génériques en MiscUtil (gratuit, etc.).

Il dispose de méthodes telles que T Add<T>(T x, T y) et d'autres variantes pour l'arithmétique sur différents types (comme DateTime + TimeSpan ).

En outre, cela fonctionne pour tous les opérateurs intégrés, soulevés et sur mesure, et met en cache le délégué pour des raisons de performance.

Voici quelques informations supplémentaires sur les raisons de cette difficulté aquí .

Vous pouvez également savoir que dynamic (4.0) résout en quelque sorte ce problème indirectement aussi - c'est-à-dire

dynamic x = ..., y = ...
dynamic result = x + y; // does what you expect

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