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


364

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

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

Je connais le wheremot clé, mais je ne trouve pas d'interface uniquement pour ces types,

Quelque chose comme:

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

3
Il existe maintenant diverses propositions C # qui permettraient d'accomplir cela, mais AFAIK aucune d'entre elles n'est plus loin que des explorations / discussions préliminaires. Voir Exploration: Formes et extensions , Exploration: Rôles, interfaces d'extension et membres d'interface statique , Champion "Classes de types (aka Concepts, Structural Generic Constraints)" , et Proposition: Les types génériques devraient prendre en charge les opérateurs
Chris Yungmann

Réponses:


140

C # ne prend pas en charge cela. Hejlsberg a décrit les raisons de ne pas mettre en œuvre la fonctionnalité dans une interview avec Bruce Eckel :

Et il n'est pas clair que la complexité supplémentaire vaut le petit rendement que vous obtenez. Si quelque chose que vous voulez faire n'est pas directement pris en charge dans le système de contraintes, vous pouvez le faire avec un modèle d'usine. Vous pourriez avoir un Matrix<T>, par exemple, et en ce que Matrixvous souhaitez définir une méthode de produit scalaire. Cela bien sûr cela signifie que vous finalement besoin de comprendre comment multiplier deux Ts, mais vous ne pouvez pas dire que comme une contrainte, au moins pas si Test int, doubleou float. Mais ce que vous pourriez faire, c'est d'avoir votre Matrixargument comme argument a Calculator<T>, et dans Calculator<T>, d'avoir une méthode appelée multiply. Vous allez implémenter cela et vous le transmettez au Matrix.

Cependant, cela conduit à un code assez alambiqué, où l'utilisateur doit fournir sa propre Calculator<T>implémentation, pour chacun Tqu'il souhaite utiliser. Tant qu'il ne doit pas être extensible, c'est-à-dire si vous souhaitez simplement prendre en charge un nombre fixe de types, tels que intet double, vous pouvez vous en sortir avec une interface relativement simple:

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

( Implémentation minimale dans un GitHub Gist. )

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 Calculatorinstances. Par exemple, pour instancier une matrice qui utilise une implémentation décimale décimale flottante personnalisée DFP, vous devez écrire ce code:

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

… Et implémentez tous les membres de DfpCalculator : ICalculator<DFP>.

Une alternative, qui partage malheureusement les mêmes limites, est de travailler avec des classes de politique, comme discuté dans la réponse de Sergey Shandar .


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

1
@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 une solution au problème comme vous le faites dans le Operator<T>code (puisque l'interview a été donnée bien avant l'existence du Expressionscadre, même si l'on pouvait utilisation du cours Reflection.Emit) - et je serais vraiment intéressé par sa solution.
Konrad Rudolph

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

14
Je ne suis pas d'accord avec la phrase de Heijsberg "Donc, dans un sens, les modèles C ++ sont en fait non typés, ou typés de manière lâche. Alors que les génériques C # sont fortement typés.". C'est vraiment Marketing BS pour promouvoir C #. Le typage fort / faible n'a rien à voir avec la qualité des diagnostics. Sinon: trouvaille intéressante.
Sebastian Mach

100

Compte tenu de la popularité de cette question et de l'intérêt derrière une telle fonction, je suis surpris de voir qu'il n'y a pas encore de réponse concernant T4.

Dans cet exemple de code, je vais dé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 à peu près en arrière-plan avec des génériques.

Au lieu de passer par des cercles 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éez un nouveau fichier de modèle de texte appelé GenericNumberMethodTemplate.tt .
  • Supprimez le code généré automatiquement (vous en conserverez la plupart, mais certains ne sont pas nécessaires).
  • Ajoutez l'extrait de code 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 ça. Vous avez terminé maintenant.

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 mainméthode, 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();
        }
    }
}

entrez la description de l'image ici

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

Ce n'est pas du tout le cas ici: si vous voulez un changement, vous pouvez simplement changer le modèle (une seule source pour toute votre génération!) Et c'est fait.

Afin de l'utiliser avec vos propres définitions personnalisées, ajoutez une déclaration d'espace de noms (assurez-vous qu'elle 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 afin qu'il soit inclus dans la compilation éventuelle:

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

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

Avertissement: cet échantillon a été fortement influencé par la métaprogrammation en .NET par Kevin Hazzard et Jason Bock, Manning Publications .


C'est assez cool, mais serait-il possible de modifier cette solution pour que les méthodes acceptent un type générique Tqui est ou hérite des différentes IntXclasses? J'aime cette solution car elle fait gagner du temps, mais pour qu'elle résout à 100% le problème (bien qu'elle ne soit pas aussi agréable que si C # avait un support pour ce type de contrainte, intégré), chacune des méthodes générées devrait toujours être générique pour que ils peuvent renvoyer un objet d'un type qui hérite d'une des IntXXclasses.
Zachary Kniebel

1
@ZacharyKniebel: les IntXXtypes sont des structures, ce qui signifie qu'ils ne prennent pas en charge l'héritage en premier lieu . Et même si c'était le cas, le principe de substitution de Liskov (que vous connaissez peut-être dans l'idiome SOLID) s'applique: si la méthode est définie comme Xet Yest un enfant de, Xpar définition, tout Ydevrait pouvoir être transmis à cette méthode comme substitut de son type de base.
Jeroen Vannevel

1
Cette solution de contournement utilisant des stratégies stackoverflow.com/questions/32664/… utilise T4 pour générer des classes.
Sergey Shandar

2
+1 pour cette solution car elle préserve l'efficacité de fonctionnement des types intégrés intégrés, contrairement aux solutions basées sur des règles. L'appel d'opérateurs CLR intégrés (comme Ajouter) via une méthode supplémentaire (éventuellement virtuelle) peut gravement affecter les performances s'il est utilisé plusieurs fois (comme dans les bibliothèques mathématiques). Et comme le nombre de types intégraux est constant (et ne peut pas être hérité de), il vous suffit de régénérer le code pour corriger les bogues.
Attila Klenik

1
Très cool et j'étais sur le point de commencer à l'utiliser, puis je me suis souvenu à quel point je dépend de Resharper pour le refactoring et vous ne pouvez pas renommer le refactor via le modèle T4. Ce n'est pas critique mais mérite d'être considéré.
bradgonesurfing

86

Il n'y a aucune contrainte pour cela. C'est un vrai problème pour quiconque souhaite utiliser des génériques pour les calculs numériques.

J'irais plus loin et je dirais que nous avons besoin

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

Ou même

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

Malheureusement, vous n'avez que des interfaces, des classes de base et les 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 pouvez envelopper le nombre dans quelque chose d'autre (similaire à INullable<T>) comme ici sur codeproject .


Vous pouvez appliquer la restriction lors de l'exécution (en réfléchissant pour les opérateurs ou en recherchant les types), mais cela perd l'avantage d'avoir le générique en premier lieu.


2
Je me demande si vous avez vu le support de MiscUtil pour les opérateurs génériques ... yoda.arachsys.com/csharp/miscutil/usage/genericoperators.html
Marc Gravell

10
Ouais - Jon Skeet m'a pointé sur eux pour quelque chose d'autre il y a quelque temps (mais après cette réponse de cette année) - c'est une idée intelligente, mais j'aimerais quand même un support de contrainte approprié.
Keith

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

@kdbanman Je ne pense pas. Keith dit que C # ne supporte pas ce que demande OP, et suggère que nous devrions être en mesure de le faire where T : operators( +, -, /, * ), mais pas.
AMTerp à 6h42

62

Solution de contournement à l'aide de stratégies:

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;
    }

}

Usage:

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. CityLizard Framework 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 la structure CityLizard.Policy.I .


J'ai eu 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/…
xvan

une raison pour laquelle utiliser struct? que se passe-t-il si j'utilise plutôt la classe singleton et que je change l'instance en public static NumericPolicies Instance = new NumericPolicies();puis ajoute ce constructeur private NumericPolicies() { }.
M.kazem Akhgary

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

J'allais dire que vous avez trouvé une solution très intelligente, mais la solution est trop limitée pour moi: j'allais l'utiliser dans T Add<T> (T t1, T t2), mais Sum()ne fonctionne que lorsqu'elle peut récupérer son propre type de T à partir de ses paramètres, ce qui n'est pas possible quand il est intégré dans une autre fonction générique.
Tobias Knauss

16

Cette question est un peu une FAQ, donc je poste ceci en tant que wiki (puisque j'ai posté similaire auparavant, mais c'est une ancienne); en tous cas...

Quelle version de .NET utilisez-vous? Si vous utilisez .NET 3.5, j'ai une implémentation d'opérateurs génériques dans MiscUtil (gratuit, etc.).

Cela a des méthodes comme T Add<T>(T x, T y), et d'autres variantes pour l'arithmétique sur différents types (comme DateTime + TimeSpan).

De plus, cela fonctionne pour tous les opérateurs intégrés, levés et sur mesure, et met en cache le délégué pour les performances.

Un peu d' histoire supplémentaire pourquoi est - ce délicat est ici .

Vous voudrez peut-être également savoir que dynamic(4.0) résout ce problème indirectement aussi - c.-à-d.

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

14

Malheureusement, vous ne pouvez spécifier la structure que dans la clause where dans cette instance. Il semble étrange que vous ne puissiez pas spécifier spécifiquement Int16, Int32, etc., mais je suis sûr qu'il existe une raison profonde d'implémentation qui sous-tend la décision de ne pas autoriser les types de valeur dans une clause where.

Je suppose que la seule solution consiste à effectuer une vérification de l'exécution, ce qui empêche malheureusement le problème d'être détecté au moment de la compilation. Cela irait quelque chose comme: -

static bool IntegerFunction<T>(T value) where T : struct {
  if (typeof(T) != typeof(Int16)  &&
      typeof(T) != typeof(Int32)  &&
      typeof(T) != typeof(Int64)  &&
      typeof(T) != typeof(UInt16) &&
      typeof(T) != typeof(UInt32) &&
      typeof(T) != typeof(UInt64)) {
    throw new ArgumentException(
      string.Format("Type '{0}' is not valid.", typeof(T).ToString()));
  }

  // Rest of code...
}

Ce qui est un peu moche je sais, mais au moins fournit les contraintes requises.

J'examinerais également les implications possibles en termes de performances pour cette implémentation, il existe peut-être un moyen plus rapide.


13
+1, Cependant, // Rest of code...peut ne pas compiler si cela dépend des opérations définies par les contraintes.
Nick

1
Convert.ToIntXX (valeur) peut aider à compiler "// Rest of code" - au moins jusqu'à ce que le type de retour de IntegerFunction soit également de type T, alors vous êtes entouré. :-p
yoyo

-1; cela ne fonctionne pas pour la raison donnée par @Nick. Au moment où vous essayez d'effectuer des opérations arithmétiques // Rest of code...comme value + valueou value * value, vous avez une erreur de compilation.
Mark Amery du

13

Probablement le plus proche que vous puissiez faire est

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

Je ne sais pas si vous pourriez faire ce qui suit

static bool IntegerFunction<T>(T value) where T: struct, IComparable
, IFormattable, IConvertible, IComparable<T>, IEquatable<T>

Pour quelque chose de si spécifique, pourquoi ne pas simplement avoir des surcharges pour chaque type, la liste est si courte et elle aurait peut-être moins d'espace mémoire.


6

À partir de C # 7.3, vous pouvez utiliser une approximation plus étroite - la contrainte non managée pour spécifier qu'un paramètre de type est un type non managé non pointeur, non nullable .

class SomeGeneric<T> where T : unmanaged
{
//...
}

La contrainte non gérée implique la contrainte struct et ne peut pas être combinée avec la contrainte struct ou new ().

Un type est un type non géré s'il s'agit de l'un des types suivants:

  • sbyte, octet, court, ushort, int, uint, long, ulong, char, float, double, decimal ou bool
  • Tout type d'énumération
  • Tout type de pointeur
  • Tout type de structure défini par l'utilisateur qui contient uniquement des champs de types non gérés et, en C # 7.3 et versions antérieures, n'est pas un type construit (un type qui inclut au moins un argument de type)

Pour restreindre davantage et éliminer les types de pointeur et définis par l'utilisateur qui n'implémentent pas IComparable, ajoutez IComparable (mais l'énumération est toujours dérivée de IComparable, alors restreignez l'énumération en ajoutant IEquatable <T>, vous pouvez aller plus loin en fonction de vos circonstances et ajouter des interfaces supplémentaires. non managé permet de garder cette liste plus courte):

    class SomeGeneric<T> where T : unmanaged, IComparable, IEquatable<T>
    {
    //...
    }

Sympa, mais pas assez ... Par exemple, DateTimetombe sous unmanaged, IComparable, IEquatable<T>contrainte ..
Adam Calvet Bohl

Je sais mais vous pouvez aller plus loin selon vos circonstances et ajouter des interfaces supplémentaires. non managé permet de garder cette liste plus courte. je viens de montrer l'approche, l'approximation en utilisant non managé. Dans la plupart des cas, cela suffit
Vlad Novakovsky

4

Il n'y a aucun moyen de restreindre les modèles aux types, mais vous pouvez définir différentes actions en fonction du type. Dans le cadre d'un package numérique générique, j'avais besoin d'une classe générique pour ajouter deux valeurs.

    class Something<TCell>
    {
        internal static TCell Sum(TCell first, TCell second)
        {
            if (typeof(TCell) == typeof(int))
                return (TCell)((object)(((int)((object)first)) + ((int)((object)second))));

            if (typeof(TCell) == typeof(double))
                return (TCell)((object)(((double)((object)first)) + ((double)((object)second))));

            return second;
        }
    }

Notez que les types de fichiers sont évalués au moment de la compilation, donc les instructions if seront supprimées par le compilateur. Le compilateur supprime également les transtypages parasites. Donc, quelque chose résoudrait dans le compilateur pour

        internal static int Sum(int first, int second)
        {
            return first + second;
        }

Merci d'avoir fourni une solution empirique!
zsf222

N'est-ce pas la même chose que de créer la même méthode pour chaque type?
Luis

3

J'ai créé une petite fonctionnalité de bibliothèque pour résoudre ces problèmes:

Au lieu de:

public T DifficultCalculation<T>(T a, T b)
{
    T result = a * b + a; // <== WILL NOT COMPILE!
    return result;
}
Console.WriteLine(DifficultCalculation(2, 3)); // Should result in 8.

Vous pourriez écrire:

public T DifficultCalculation<T>(Number<T> a, Number<T> b)
{
    Number<T> result = a * b + a;
    return (T)result;
}
Console.WriteLine(DifficultCalculation(2, 3)); // Results in 8.

Vous pouvez trouver le code source ici: /codereview/26022/improvement-requested-for-generic-calculator-and-generic-number


2

Je me demandais la même chose que samjudson, pourquoi seulement aux entiers? et si c'est le cas, vous voudrez peut-être créer une classe d'assistance ou quelque chose comme ça pour contenir tous les types que vous voulez.

Si vous ne voulez que des entiers, n'utilisez pas de générique, ce n'est pas générique; ou mieux encore, rejetez tout autre type en vérifiant son type.


2

Il n'y a pas encore de «bonne» solution pour cela. Cependant, vous pouvez restreindre considérablement l'argument type pour exclure de nombreuses inadaptations pour votre contrainte hypotétique 'INumeric' comme Haacked l'a montré ci-dessus.

static bool IntegerFunction <T> (valeur T) où T: IComparable, IFormattable, IConvertible, IComparable <T>, IEquatable <T>, struct {...


2

Si vous utilisez .NET 4.0 et versions ultérieures, vous pouvez simplement utiliser dynamic comme argument de méthode et vérifier au moment de l'exécution que le type d'argument dynamique transmis est de type numérique / entier.

Si le type de la dynamique transmise n'est pas de type numérique / entier, lève une exception.

Un exemple de code court qui implémente l'idée est quelque chose comme:

using System;
public class InvalidArgumentException : Exception
{
    public InvalidArgumentException(string message) : base(message) {}
}
public class InvalidArgumentTypeException : InvalidArgumentException
{
    public InvalidArgumentTypeException(string message) : base(message) {}
}
public class ArgumentTypeNotIntegerException : InvalidArgumentTypeException
{
    public ArgumentTypeNotIntegerException(string message) : base(message) {}
}
public static class Program
{
    private static bool IntegerFunction(dynamic n)
    {
        if (n.GetType() != typeof(Int16) &&
            n.GetType() != typeof(Int32) &&
            n.GetType() != typeof(Int64) &&
            n.GetType() != typeof(UInt16) &&
            n.GetType() != typeof(UInt32) &&
            n.GetType() != typeof(UInt64))
            throw new ArgumentTypeNotIntegerException("argument type is not integer type");
        //code that implements IntegerFunction goes here
    }
    private static void Main()
    {
         Console.WriteLine("{0}",IntegerFunction(0)); //Compiles, no run time error and first line of output buffer is either "True" or "False" depends on the code that implements "Program.IntegerFunction" static method.
         Console.WriteLine("{0}",IntegerFunction("string")); //Also compiles but it is run time error and exception of type "ArgumentTypeNotIntegerException" is thrown here.
         Console.WriteLine("This is the last Console.WriteLine output"); //Never reached and executed due the run time error and the exception thrown on the second line of Program.Main static method.
    }

Bien sûr, cette solution ne fonctionne qu'en exécution mais jamais en compilation.

Si vous voulez une solution qui fonctionne toujours au moment de la compilation et jamais au moment de l'exécution, vous devrez encapsuler la dynamique avec une structure / classe publique dont les constructeurs publics surchargés acceptent uniquement les arguments des types souhaités et donnent à la structure / classe le nom approprié.

Il est logique que la dynamique encapsulée soit toujours un membre privé de la classe / struct et qu'elle soit le seul membre de la struct / classe et que le nom du seul membre de la struct / classe soit "value".

Vous devrez également définir et implémenter des méthodes publiques et / ou des opérateurs qui fonctionnent avec les types souhaités pour le membre dynamique privé de la classe / struct si nécessaire.

Il est également logique que la structure / classe ait un constructeur spécial / unique qui accepte la dynamique comme argument qui initialise ce n'est qu'un membre dynamique privé appelé "valeur" mais le modificateur de ce constructeur est privé bien sûr.

Une fois que la classe / structure est prête, définissez le type d'argument IntegerFunction comme étant la classe / structure qui a été définie.

Un exemple de code long qui implémente l'idée est quelque chose comme:

using System;
public struct Integer
{
    private dynamic value;
    private Integer(dynamic n) { this.value = n; }
    public Integer(Int16 n) { this.value = n; }
    public Integer(Int32 n) { this.value = n; }
    public Integer(Int64 n) { this.value = n; }
    public Integer(UInt16 n) { this.value = n; }
    public Integer(UInt32 n) { this.value = n; }
    public Integer(UInt64 n) { this.value = n; }
    public Integer(Integer n) { this.value = n.value; }
    public static implicit operator Int16(Integer n) { return n.value; }
    public static implicit operator Int32(Integer n) { return n.value; }
    public static implicit operator Int64(Integer n) { return n.value; }
    public static implicit operator UInt16(Integer n) { return n.value; }
    public static implicit operator UInt32(Integer n) { return n.value; }
    public static implicit operator UInt64(Integer n) { return n.value; }
    public static Integer operator +(Integer x, Int16 y) { return new Integer(x.value + y); }
    public static Integer operator +(Integer x, Int32 y) { return new Integer(x.value + y); }
    public static Integer operator +(Integer x, Int64 y) { return new Integer(x.value + y); }
    public static Integer operator +(Integer x, UInt16 y) { return new Integer(x.value + y); }
    public static Integer operator +(Integer x, UInt32 y) { return new Integer(x.value + y); }
    public static Integer operator +(Integer x, UInt64 y) { return new Integer(x.value + y); }
    public static Integer operator -(Integer x, Int16 y) { return new Integer(x.value - y); }
    public static Integer operator -(Integer x, Int32 y) { return new Integer(x.value - y); }
    public static Integer operator -(Integer x, Int64 y) { return new Integer(x.value - y); }
    public static Integer operator -(Integer x, UInt16 y) { return new Integer(x.value - y); }
    public static Integer operator -(Integer x, UInt32 y) { return new Integer(x.value - y); }
    public static Integer operator -(Integer x, UInt64 y) { return new Integer(x.value - y); }
    public static Integer operator *(Integer x, Int16 y) { return new Integer(x.value * y); }
    public static Integer operator *(Integer x, Int32 y) { return new Integer(x.value * y); }
    public static Integer operator *(Integer x, Int64 y) { return new Integer(x.value * y); }
    public static Integer operator *(Integer x, UInt16 y) { return new Integer(x.value * y); }
    public static Integer operator *(Integer x, UInt32 y) { return new Integer(x.value * y); }
    public static Integer operator *(Integer x, UInt64 y) { return new Integer(x.value * y); }
    public static Integer operator /(Integer x, Int16 y) { return new Integer(x.value / y); }
    public static Integer operator /(Integer x, Int32 y) { return new Integer(x.value / y); }
    public static Integer operator /(Integer x, Int64 y) { return new Integer(x.value / y); }
    public static Integer operator /(Integer x, UInt16 y) { return new Integer(x.value / y); }
    public static Integer operator /(Integer x, UInt32 y) { return new Integer(x.value / y); }
    public static Integer operator /(Integer x, UInt64 y) { return new Integer(x.value / y); }
    public static Integer operator %(Integer x, Int16 y) { return new Integer(x.value % y); }
    public static Integer operator %(Integer x, Int32 y) { return new Integer(x.value % y); }
    public static Integer operator %(Integer x, Int64 y) { return new Integer(x.value % y); }
    public static Integer operator %(Integer x, UInt16 y) { return new Integer(x.value % y); }
    public static Integer operator %(Integer x, UInt32 y) { return new Integer(x.value % y); }
    public static Integer operator %(Integer x, UInt64 y) { return new Integer(x.value % y); }
    public static Integer operator +(Integer x, Integer y) { return new Integer(x.value + y.value); }
    public static Integer operator -(Integer x, Integer y) { return new Integer(x.value - y.value); }
    public static Integer operator *(Integer x, Integer y) { return new Integer(x.value * y.value); }
    public static Integer operator /(Integer x, Integer y) { return new Integer(x.value / y.value); }
    public static Integer operator %(Integer x, Integer y) { return new Integer(x.value % y.value); }
    public static bool operator ==(Integer x, Int16 y) { return x.value == y; }
    public static bool operator !=(Integer x, Int16 y) { return x.value != y; }
    public static bool operator ==(Integer x, Int32 y) { return x.value == y; }
    public static bool operator !=(Integer x, Int32 y) { return x.value != y; }
    public static bool operator ==(Integer x, Int64 y) { return x.value == y; }
    public static bool operator !=(Integer x, Int64 y) { return x.value != y; }
    public static bool operator ==(Integer x, UInt16 y) { return x.value == y; }
    public static bool operator !=(Integer x, UInt16 y) { return x.value != y; }
    public static bool operator ==(Integer x, UInt32 y) { return x.value == y; }
    public static bool operator !=(Integer x, UInt32 y) { return x.value != y; }
    public static bool operator ==(Integer x, UInt64 y) { return x.value == y; }
    public static bool operator !=(Integer x, UInt64 y) { return x.value != y; }
    public static bool operator ==(Integer x, Integer y) { return x.value == y.value; }
    public static bool operator !=(Integer x, Integer y) { return x.value != y.value; }
    public override bool Equals(object obj) { return this == (Integer)obj; }
    public override int GetHashCode() { return this.value.GetHashCode(); }
    public override string ToString() { return this.value.ToString(); }
    public static bool operator >(Integer x, Int16 y) { return x.value > y; }
    public static bool operator <(Integer x, Int16 y) { return x.value < y; }
    public static bool operator >(Integer x, Int32 y) { return x.value > y; }
    public static bool operator <(Integer x, Int32 y) { return x.value < y; }
    public static bool operator >(Integer x, Int64 y) { return x.value > y; }
    public static bool operator <(Integer x, Int64 y) { return x.value < y; }
    public static bool operator >(Integer x, UInt16 y) { return x.value > y; }
    public static bool operator <(Integer x, UInt16 y) { return x.value < y; }
    public static bool operator >(Integer x, UInt32 y) { return x.value > y; }
    public static bool operator <(Integer x, UInt32 y) { return x.value < y; }
    public static bool operator >(Integer x, UInt64 y) { return x.value > y; }
    public static bool operator <(Integer x, UInt64 y) { return x.value < y; }
    public static bool operator >(Integer x, Integer y) { return x.value > y.value; }
    public static bool operator <(Integer x, Integer y) { return x.value < y.value; }
    public static bool operator >=(Integer x, Int16 y) { return x.value >= y; }
    public static bool operator <=(Integer x, Int16 y) { return x.value <= y; }
    public static bool operator >=(Integer x, Int32 y) { return x.value >= y; }
    public static bool operator <=(Integer x, Int32 y) { return x.value <= y; }
    public static bool operator >=(Integer x, Int64 y) { return x.value >= y; }
    public static bool operator <=(Integer x, Int64 y) { return x.value <= y; }
    public static bool operator >=(Integer x, UInt16 y) { return x.value >= y; }
    public static bool operator <=(Integer x, UInt16 y) { return x.value <= y; }
    public static bool operator >=(Integer x, UInt32 y) { return x.value >= y; }
    public static bool operator <=(Integer x, UInt32 y) { return x.value <= y; }
    public static bool operator >=(Integer x, UInt64 y) { return x.value >= y; }
    public static bool operator <=(Integer x, UInt64 y) { return x.value <= y; }
    public static bool operator >=(Integer x, Integer y) { return x.value >= y.value; }
    public static bool operator <=(Integer x, Integer y) { return x.value <= y.value; }
    public static Integer operator +(Int16 x, Integer y) { return new Integer(x + y.value); }
    public static Integer operator +(Int32 x, Integer y) { return new Integer(x + y.value); }
    public static Integer operator +(Int64 x, Integer y) { return new Integer(x + y.value); }
    public static Integer operator +(UInt16 x, Integer y) { return new Integer(x + y.value); }
    public static Integer operator +(UInt32 x, Integer y) { return new Integer(x + y.value); }
    public static Integer operator +(UInt64 x, Integer y) { return new Integer(x + y.value); }
    public static Integer operator -(Int16 x, Integer y) { return new Integer(x - y.value); }
    public static Integer operator -(Int32 x, Integer y) { return new Integer(x - y.value); }
    public static Integer operator -(Int64 x, Integer y) { return new Integer(x - y.value); }
    public static Integer operator -(UInt16 x, Integer y) { return new Integer(x - y.value); }
    public static Integer operator -(UInt32 x, Integer y) { return new Integer(x - y.value); }
    public static Integer operator -(UInt64 x, Integer y) { return new Integer(x - y.value); }
    public static Integer operator *(Int16 x, Integer y) { return new Integer(x * y.value); }
    public static Integer operator *(Int32 x, Integer y) { return new Integer(x * y.value); }
    public static Integer operator *(Int64 x, Integer y) { return new Integer(x * y.value); }
    public static Integer operator *(UInt16 x, Integer y) { return new Integer(x * y.value); }
    public static Integer operator *(UInt32 x, Integer y) { return new Integer(x * y.value); }
    public static Integer operator *(UInt64 x, Integer y) { return new Integer(x * y.value); }
    public static Integer operator /(Int16 x, Integer y) { return new Integer(x / y.value); }
    public static Integer operator /(Int32 x, Integer y) { return new Integer(x / y.value); }
    public static Integer operator /(Int64 x, Integer y) { return new Integer(x / y.value); }
    public static Integer operator /(UInt16 x, Integer y) { return new Integer(x / y.value); }
    public static Integer operator /(UInt32 x, Integer y) { return new Integer(x / y.value); }
    public static Integer operator /(UInt64 x, Integer y) { return new Integer(x / y.value); }
    public static Integer operator %(Int16 x, Integer y) { return new Integer(x % y.value); }
    public static Integer operator %(Int32 x, Integer y) { return new Integer(x % y.value); }
    public static Integer operator %(Int64 x, Integer y) { return new Integer(x % y.value); }
    public static Integer operator %(UInt16 x, Integer y) { return new Integer(x % y.value); }
    public static Integer operator %(UInt32 x, Integer y) { return new Integer(x % y.value); }
    public static Integer operator %(UInt64 x, Integer y) { return new Integer(x % y.value); }
    public static bool operator ==(Int16 x, Integer y) { return x == y.value; }
    public static bool operator !=(Int16 x, Integer y) { return x != y.value; }
    public static bool operator ==(Int32 x, Integer y) { return x == y.value; }
    public static bool operator !=(Int32 x, Integer y) { return x != y.value; }
    public static bool operator ==(Int64 x, Integer y) { return x == y.value; }
    public static bool operator !=(Int64 x, Integer y) { return x != y.value; }
    public static bool operator ==(UInt16 x, Integer y) { return x == y.value; }
    public static bool operator !=(UInt16 x, Integer y) { return x != y.value; }
    public static bool operator ==(UInt32 x, Integer y) { return x == y.value; }
    public static bool operator !=(UInt32 x, Integer y) { return x != y.value; }
    public static bool operator ==(UInt64 x, Integer y) { return x == y.value; }
    public static bool operator !=(UInt64 x, Integer y) { return x != y.value; }
    public static bool operator >(Int16 x, Integer y) { return x > y.value; }
    public static bool operator <(Int16 x, Integer y) { return x < y.value; }
    public static bool operator >(Int32 x, Integer y) { return x > y.value; }
    public static bool operator <(Int32 x, Integer y) { return x < y.value; }
    public static bool operator >(Int64 x, Integer y) { return x > y.value; }
    public static bool operator <(Int64 x, Integer y) { return x < y.value; }
    public static bool operator >(UInt16 x, Integer y) { return x > y.value; }
    public static bool operator <(UInt16 x, Integer y) { return x < y.value; }
    public static bool operator >(UInt32 x, Integer y) { return x > y.value; }
    public static bool operator <(UInt32 x, Integer y) { return x < y.value; }
    public static bool operator >(UInt64 x, Integer y) { return x > y.value; }
    public static bool operator <(UInt64 x, Integer y) { return x < y.value; }
    public static bool operator >=(Int16 x, Integer y) { return x >= y.value; }
    public static bool operator <=(Int16 x, Integer y) { return x <= y.value; }
    public static bool operator >=(Int32 x, Integer y) { return x >= y.value; }
    public static bool operator <=(Int32 x, Integer y) { return x <= y.value; }
    public static bool operator >=(Int64 x, Integer y) { return x >= y.value; }
    public static bool operator <=(Int64 x, Integer y) { return x <= y.value; }
    public static bool operator >=(UInt16 x, Integer y) { return x >= y.value; }
    public static bool operator <=(UInt16 x, Integer y) { return x <= y.value; }
    public static bool operator >=(UInt32 x, Integer y) { return x >= y.value; }
    public static bool operator <=(UInt32 x, Integer y) { return x <= y.value; }
    public static bool operator >=(UInt64 x, Integer y) { return x >= y.value; }
    public static bool operator <=(UInt64 x, Integer y) { return x <= y.value; }
}
public static class Program
{
    private static bool IntegerFunction(Integer n)
    {
        //code that implements IntegerFunction goes here
        //note that there is NO code that checks the type of n in rum time, because it is NOT needed anymore 
    }
    private static void Main()
    {
        Console.WriteLine("{0}",IntegerFunction(0)); //compile error: there is no overloaded METHOD for objects of type "int" and no implicit conversion from any object, including "int", to "Integer" is known.
        Console.WriteLine("{0}",IntegerFunction(new Integer(0))); //both compiles and no run time error
        Console.WriteLine("{0}",IntegerFunction("string")); //compile error: there is no overloaded METHOD for objects of type "string" and no implicit conversion from any object, including "string", to "Integer" is known.
        Console.WriteLine("{0}",IntegerFunction(new Integer("string"))); //compile error: there is no overloaded CONSTRUCTOR for objects of type "string"
    }
}

Notez que pour utiliser la dynamique dans votre code, vous devez ajouter une référence à Microsoft.CSharp

Si la version du framework .NET est inférieure / inférieure / inférieure à 4.0 et que la dynamique n'est pas définie dans cette version, vous devrez utiliser un objet à la place et effectuer une conversion vers le type entier, ce qui est un problème, donc je vous recommande d'utiliser à moins .NET 4.0 ou plus récent si vous le pouvez pour pouvoir utiliser dynamique au lieu d' objet .


2

Malheureusement .NET ne fournit pas un moyen de le faire en mode natif.

Pour résoudre ce problème, j'ai créé la bibliothèque OSS Genumerics qui fournit la plupart des opérations numériques standard pour les types numériques intégrés suivants et leurs équivalents annulables avec la possibilité d'ajouter la prise en charge d'autres types numériques.

sbyte, byte, short, ushort, int, uint, long, ulong, float, double, decimalEtBigInteger

Les performances sont équivalentes à une solution spécifique au type numérique vous permettant de créer des algorithmes numériques génériques efficaces.

Voici un exemple d'utilisation du code.

public static T Sum(T[] items)
{
    T sum = Number.Zero<T>();
    foreach (T item in items)
    {
        sum = Number.Add(sum, item);
    }
    return sum;
}
public static T SumAlt(T[] items)
{
    // implicit conversion to Number<T>
    Number<T> sum = Number.Zero<T>();
    foreach (T item in items)
    {
        // operator support
        sum += item;
    }
    // implicit conversion to T
    return sum;
}

1

Quel est l'intérêt de l'exercice?

Comme les gens l'ont déjà fait remarquer, vous pourriez avoir une fonction non générique prenant le plus gros élément, et le compilateur convertira automatiquement les plus petits pouces pour vous.

static bool IntegerFunction(Int64 value) { }

Si votre fonction est sur un chemin critique en termes de performances (très peu probable, IMO), vous pouvez fournir des surcharges pour toutes les fonctions nécessaires.

static bool IntegerFunction(Int64 value) { }
...
static bool IntegerFunction(Int16 value) { }

1
Je travaille beaucoup avec des méthodes numériques. Parfois, je veux des entiers et parfois je veux virgule flottante. Les deux ont des versions 64 bits qui sont optimales pour la vitesse de traitement. La conversion entre ceux-ci est une idée terrible car il y a des pertes dans chaque sens. Bien que j'aie tendance à utiliser des doubles, je trouve parfois préférable d'utiliser des entiers en raison de la façon dont ils sont utilisés ailleurs. Mais ce serait très bien quand j'écris un algorithme pour le faire une fois et laisser la décision de type aux exigences de l'instance.
VoteCoffee

1

J'utiliserais un générique que vous pourriez gérer en externe ...

/// <summary>
/// Generic object copy of the same type
/// </summary>
/// <typeparam name="T">The type of object to copy</typeparam>
/// <param name="ObjectSource">The source object to copy</param>
public T CopyObject<T>(T ObjectSource)
{
    T NewObject = System.Activator.CreateInstance<T>();

    foreach (PropertyInfo p in ObjectSource.GetType().GetProperties())
        NewObject.GetType().GetProperty(p.Name).SetValue(NewObject, p.GetValue(ObjectSource, null), null);

    return NewObject;
}

1

Cette limitation m'a affecté lorsque j'ai essayé de surcharger les opérateurs pour les types génériques; comme il n'y avait pas de contrainte "INumeric", et pour une multitude d'autres raisons que les bonnes personnes sur stackoverflow sont heureuses de fournir, les opérations ne peuvent pas être définies sur des types génériques.

Je voulais quelque chose comme

public struct Foo<T>
{
    public T Value{ get; private set; }

    public static Foo<T> operator +(Foo<T> LHS, Foo<T> RHS)
    {
        return new Foo<T> { Value = LHS.Value + RHS.Value; };
    }
}

J'ai contourné ce problème en utilisant la saisie dynamique de runtime .net4.

public struct Foo<T>
{
    public T Value { get; private set; }

    public static Foo<T> operator +(Foo<T> LHS, Foo<T> RHS)
    {
        return new Foo<T> { Value = LHS.Value + (dynamic)RHS.Value };
    }
}

Les deux choses sur l'utilisation dynamicsont

  1. Performance. Tous les types de valeurs sont encadrés.
  2. Erreurs d'exécution. Vous "battez" le compilateur, mais vous perdez la sécurité du type. Si le type générique n'a pas d'opérateur défini, une exception sera levée lors de l'exécution.

1

Les types primitifs numériques .NET ne partagent aucune interface commune qui leur permettrait d'être utilisés pour les calculs. Il serait possible de définir vos propres interfaces (par exemple ISignedWholeNumber) qui effectuer de telles opérations, définir des structures qui contiennent un seul Int16, Int32etc. et mettre en œuvre ces interfaces, et ont des méthodes qui acceptent les types génériques de contraintesISignedWholeNumber , mais avoir à convertir des valeurs numériques à vos types de structure serait probablement une nuisance.

Une autre approche serait de définir la classe statique Int64Converter<T>avec une propriété statique bool Available {get;};et les délégués statiques pour Int64 GetInt64(T value), T FromInt64(Int64 value), bool TryStoreInt64(Int64 value, ref T dest). Le constructeur de classe pourrait être codé en dur pour charger les délégués pour les types connus, et éventuellement utiliser Reflection pour tester si le type Timplémente des méthodes avec les noms et signatures appropriés (au cas où il s'agirait d'une structure qui contient un Int64et représente un nombre, mais qui a une ToString()méthode personnalisée ). Cette approche perdrait les avantages associés à la vérification de type au moment de la compilation, mais réussirait à éviter les opérations de boxe et chaque type n'aurait à être "vérifié" qu'une seule fois. Après cela, les opérations associées à ce type seraient remplacées par une répartition des délégués.


@KenKin: IConvertible fournit un moyen par lequel n'importe quel entier pourrait être ajouté à un autre type entier pour donner par exemple un Int64résultat, mais ne fournit pas un moyen par lequel par exemple un entier de type arbitraire pourrait être incrémenté pour produire un autre entier du même type .
supercat

1

J'ai eu une situation similaire où j'avais besoin de gérer les types et les chaînes numériques; semble un peu bizarre, mais voilà.

Encore une fois, comme beaucoup de gens, j'ai examiné les contraintes et trouvé un tas d'interfaces qu'il devait prendre en charge. Cependant, a) ce n'était pas étanche à 100% et b), toute personne qui regarde cette longue liste de contraintes serait immédiatement très confuse.

Donc, mon approche était de mettre toute ma logique dans une méthode générique sans contraintes, mais de rendre cette méthode générique privée. Je l'ai ensuite exposé avec des méthodes publiques, une gérant explicitement le type que je voulais gérer - à mon avis, le code est propre et explicite, par exemple

public static string DoSomething(this int input, ...) => DoSomethingHelper(input, ...);
public static string DoSomething(this decimal input, ...) => DoSomethingHelper(input, ...);
public static string DoSomething(this double input, ...) => DoSomethingHelper(input, ...);
public static string DoSomething(this string input, ...) => DoSomethingHelper(input, ...);

private static string DoSomethingHelper<T>(this T input, ....)
{
    // complex logic
}

0

Si vous ne souhaitez utiliser qu'un seul type numérique , vous pouvez envisager de créer quelque chose de similaire à un alias en C ++ avec using.

Donc, au lieu d'avoir le très générique

T ComputeSomething<T>(T value1, T value2) where T : INumeric { ... }

tu aurais pu

using MyNumType = System.Double;
T ComputeSomething<MyNumType>(MyNumType value1, MyNumType value2) { ... }

Cela pourrait vous permettre d'accéder facilement doubleà intou à d'autres si nécessaire, mais vous ne pourriez pas l'utiliser ComputeSomethingavec doubleet intdans le même programme.

Mais pourquoi pas remplacer tout doubleà intalors? Parce que votre méthode peut vouloir utiliser doublesi l'entrée est doubleou int. L'alias vous permet de savoir exactement quelle variable utilise le type dynamique .


0

Le sujet est ancien mais pour les futurs lecteurs:

Cette fonctionnalité est étroitement liée à celle Discriminated Unionsqui n'est pas implémentée en C # jusqu'à présent. J'ai trouvé son problème ici:

https://github.com/dotnet/csharplang/issues/113

Ce problème est toujours ouvert et une fonctionnalité est prévue pour C# 10

Nous devons donc encore attendre un peu plus, mais après la sortie, vous pouvez le faire de cette façon:

static bool IntegerFunction<T>(T value) where T : Int16 | Int32 | Int64 | ...

-11

Je pense que vous comprenez mal les génériques. Si l'opération que vous essayez d'effectuer n'est valable que pour des types de données spécifiques, vous ne faites pas quelque chose de "générique".

De plus, étant donné que vous souhaitez uniquement autoriser la fonction à fonctionner sur les types de données int, vous ne devriez pas avoir besoin d'une fonction distincte pour chaque taille spécifique. Le simple fait de prendre un paramètre dans le plus grand type spécifique permettra au programme de lui transférer automatiquement les types de données plus petits. (c'est-à-dire que passer un Int16 se convertira automatiquement en Int64 lors de l'appel).

Si vous effectuez différentes opérations en fonction de la taille réelle de l'int. Passé dans la fonction, je pense que vous devriez sérieusement reconsidérer même essayer de faire ce que vous faites. Si vous devez tromper la langue, vous devriez penser un peu plus à ce que vous essayez d'accomplir plutôt qu'à la façon de faire ce que vous voulez.

À défaut de quoi, un paramètre de type Object pourrait être utilisé et vous devrez alors vérifier le type du paramètre et prendre les mesures appropriées ou lever une exception.


10
Considérons un histogramme de classe <T>. Il est logique de le laisser prendre un paramètre générique, afin que le compilateur puisse l'optimiser pour les octets, les entiers, les doubles, les décimales, les BigInt, ... mais en même temps, vous devez éviter de créer un, disons, Histogramme <Hashset >, parce que - en parlant avec Tron - cela ne calcule pas. (littéralement :))
Sunside

15
Vous êtes celui qui comprend mal les génériques. La métaprogrammation ne fonctionne pas seulement sur des valeurs qui pourraient être de n'importe quel type possible , c'est pour fonctionner sur des types qui correspondent à diverses contraintes .
Jim Balter
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.