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();
}
}
}
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 .
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