Codingly

Optimisation des invocations dynamiques de méthodes en C#

Posted in Articles by Romain Verdier on mai 2, 2008

Je travaille actuellement en tant que consultant .NET sur un projet d’une certaine taille.

Travailler sur un projet d’une certaine taille ne signifie pas forcément que l’on travaille sur un projet intéressant, mais ça augmente sensiblement les chances de rencontrer de nouveaux problèmes. Il n’est pas question aujourd’hui de définir ce qu’est un projet d’une certaine taille, ni même de démontrer le postulat précédent ; il s’agit plutôt de parler d’une des dernières problématiques auxquelles j’ai dû faire face :

Comment éviter que les invocations dynamiques de méthodes via la réflexion rendent les performances d’une application ou d’un module catastrophiques ?

Réponse : En minimisant l’utilisation de la réflexion. C’est ce que je vais tenter de développer à travers un exemple directement inspiré du projet réel, quoiqu’adapté pour les besoins de l’article. Le langage utilisé sera C# 3.0, mais rien n’empêche d’utiliser C# 2.0.

Invocation dynamique de méthodes

Le langage C# supporte la réflexion. Dès lors il est envisageable d’appeler dynamiquement des méthodes. La puissance de ce mécanisme est souvent contrebalancée par son coût. Prenons un exemple basique :

public void CallMethod(object target, string methodName)
{
    Type type = target.GetType();
    MethodInfo methodInfo = type.GetMethod(methodName);
    if (methodInfo != null)
    {
        methodInfo.Invoke(target, null);
    }
}

La fonction précédente est capable d’exécuter une méthode (sans valeur de retour ni paramètre) à partir de son nom, sur un objet donné. Cette implémentation est un désastre mais là n’est pas le vrai problème. Le vrai problème est inhérent aux performances. Essayons de nous concentrer sur ce qui coûte :

  • L’appel à GetType pour récupérer le type de l’objet ne coûte quasiment rien. Nous pouvons considérer que la durée de son exécution est négligeable.
  • L’appel à GetMethod pour récupérer les métadonnées relatives à la méthode peut également être ignoré. Attention toutefois : le coût peut ici varier en fonction de la surcharge choisie pour GetMethod, et du type sur lequel on l’appelle.
  • L’appel à Invoke pour exécuter dynamiquement la méthode prend beaucoup de temps. En fait, c’est ce qui est le plus coûteux. Dans un simple test consistant à invoquer dynamiquement la méthode Clone sur une chaine de caractères, la durée d’exécution s’est avérée être plus de 400 fois supérieure à celle nécessaire à un appel classique.

Mais relativisons. Si vous êtes dans un scénario impliquant une seule invocation dynamique, vous n’allez sans doute jamais avoir à vous préoccuper des performances. Qu’un appel de méthode vous coûte 2 microsecondes au lieu de 4 nanosecondes n’est pas forcément dramatique ; en vérité, ça l’est rarement dans la plupart des applications.

Les scénarios critiques sont ceux qui vous forcent à utiliser la réflexion et l’invocation dynamique pour des tâches cruciales et récurrentes. Les petites microsecondes font les grandes décennies, etc.

Essayons donc de se mettre en situation en prenant un exemple un peu plus sérieux.

Il nous faut la réflexion

Admettons que le rôle d’un module clé de notre application soit de traiter un flux de données ; ces données étant des DTO quelconques. Une des tâches du module consiste à inspecter les propriétés de ces objets, ou plus précisément, à déterminer si la valeur courante de chaque propriété correspond à la valeur par défaut du type de la propriété.

Un test unitaire vaut 1000 mots. Voici celui qui valide (presque) le fonctionnement d’un composant capable d’effectuer la vérification dont nous venons de parler :

[Test]
public void ShouldBeAbleToFindDefaultValues()
{
    IDefaultValueTester tester = CreateDefaultValueTester();

    Assert.IsFalse(tester.IsDefaultValue(typeof(int), 12));
    Assert.IsTrue(tester.IsDefaultValue(typeof(int), 0));
    Assert.IsTrue(tester.IsDefaultValue(typeof(double?), null));
    Assert.IsTrue(tester.IsDefaultValue(typeof(string), null));
    Assert.IsFalse(tester.IsDefaultValue(typeof(string), "test"));
    Assert.IsFalse(tester.IsDefaultValue(typeof(DateTime), DateTime.Now));
    Assert.IsTrue(tester.IsDefaultValue(typeof(DateTime), new DateTime()));
}

L’interface IDefaultValueTester est simple :

public interface IDefaultValueTester
{
    bool IsDefaultValue(Type type, object value);
}

L’objet de cet article n’est pas vraiment de trouver un moyen de faire passer ce test. Il existe d’ailleurs une solution évidente, bien que peu élégante, qui consisterait à utiliser un switch-like. Mais il ne faut pas oublier qu’en .NET, un type peut être :

  • Un type référence : class, interface.
  • Un type valeur : type de base (int, double, bool, etc.), enum, struct.
  • Un type nullable : implémentation de la classe générique Nullable<T> (int?, double?, bool?, etc.)

Dès lors, il est assez facile d’imaginer que le switch sera immonde, et pas forcément performant puisqu’il faudrait dans certains cas – je pense aux structures – instancier des objets « templates » servant de base aux comparaisons.

J’ai pensé à une solution différente, basée sur les generics. Il est possible qu’il y ait encore plus malin, mais celle-ci a pour avantage de constituer un bon support pour cet article :

  • Elle nécessite que l’on recoure à l’invocation dynamique de méthode.
  • Elle est lente, donc bonne candidate à l’optimisation.

Le principe consiste à tirer partie du mot clé default (capable de retourner la valeur par défaut d’un type) que l’on utilise souvent dans le contexte de la généricité. Voyons donc comment l’exploiter pour implémenter le cœur de cette solution :

public static bool IsDefaultValue<T>(object value)
{
    var type = typeof(T);

    if (!type.IsValueType)
        return value == null;

    if (value != null)
        return value.Equals(default(T));

    if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
        return true;

    var message = "The value type '{0}' can't be null.";
    throw new ArgumentException(string.Format(message, type.Name));
}

Tout le monde aura noté la subtile différence qu’il y a entre le prototype de cette méthode et celui de l’unique méthode IsDefaultValue de l’interface IDefaultValueTester : le type qui était passé sous la forme d’un objet de type Type se retrouve à présent en paramètre de type T d’une méthode générique. Et c’est là qu’interviennent la réflexion et l’invocation dynamique de méthode.

Examinons donc l’implémentation de IDefaultValueTester que je propose :

public class DefaultValueTester : IDefaultValueTester
{
    public bool IsDefaultValue(Type type, object value)
    {
        // On récupère les métadonnées de la méthode générique statique
        // IsDefaultValue présente dans la classe courante.
        var thisType = typeof(DefaultValueTester);
        var methodInfo = thisType.GetMethod("IsDefaultValue", 
                                            BindingFlags.Static 
                                            | BindingFlags.Public);

        // On lui spécifie au runtime son paramètre de type générique
        methodInfo = methodInfo.MakeGenericMethod(type);

        // On invoque dynamiquement la méthode générique fermée
        return (bool) methodInfo.Invoke(null, new [] {value});
    }
    
    public static bool IsDefaultValue<T>(object value)
    {
        // cf. extrait de code précédent.
    }
}

La réflexion est donc nécessaire pour spécifier lors de l’exécution le paramètre de type de la méthode générique. Une fois la méthode fermée, on utilise Invoke pour l’exécuter dynamiquement. Résultat : le test passe.

Résultats des tests unitaires

Peut-on optimiser ?

Oui, et heureusement. Car si on s’en tient à la description du contexte, il serait assez irresponsable d’utiliser ce DefaultValueTester au niveau du composant clé de notre architecture. Rappelons que ce dernier est censé inspecter les propriétés de tous les objets arrivant sur le flux. Si nous considérons qu’il s’agit d’un flux faisant parvenir au module des milliers d’objets par seconde, l’optimisation n’est plus une option.

Voici un nouveau test qui nous permet d’avoir une idée à propos des performances de la solution actuelle :

[Test]
public void Test()
{
    var stopwatch = new Stopwatch();
    const int iterationCount = 100000;
    stopwatch.Start();

    IDefaultValueTester tester = CreateDefaultValueTester();
    for (var i = 0; i < iterationCount; i++)
    {
        tester.IsDefaultValue(typeof(int), 12);
        tester.IsDefaultValue(typeof(int), 0);
        tester.IsDefaultValue(typeof(double?), null);
        tester.IsDefaultValue(typeof(string), null);
        tester.IsDefaultValue(typeof(string), "test");
        tester.IsDefaultValue(typeof(DateTime), DateTime.Now);
        tester.IsDefaultValue(typeof(DateTime), new DateTime());
    }

    stopwatch.Stop();
    Console.WriteLine(string.Format("{0} iteration(s) in {1} ms.", 
                                    iterationCount, 
                                    stopwatch.ElapsedMilliseconds));
}

Les 100000 itérations ont été effectuées en 10625 ms. C’est loin d’être terrible.

Il existe au moins deux façons différentes d’optimiser les invocations dynamiques de méthodes en C# dans un tel scénario. Les deux introduisent la notion de caching et se basent sur les délégués.

  1. Première solution :
    • Utiliser la Lightweight Code Generation (LCG) via Reflection.Emit pour générer dynamiquement le code CIL correspondant à l’appel de la méthode générique IsDefaultValue<T> fermée sur le bon type.
    • Utiliser la méthode CreateDelegate de DynamicMethod pour récupérer un délégué pointant sur la méthode que l’on vient de générer.
  2. Seconde solution :
    • Conserver l’usage de la réflexion pour fermer le type de la méthode générique (appel à la méthode MakeGenericMethod) et récupérer le MethodInfo correspondant.
    • Utiliser la méthode statique CreateDelegate de la classe abstraite Delegate pour récupérer un délégué à partir du MethodInfo précédent.

Les délégués ainsi obtenus ont deux particularités qui les rendent précieux dans le contexte de cette optimisation :

  • Le coût de leur invocation est quasiment nul, contrairement à l’appel à la méthode Invoke sur un MethodInfo.
  • Ils peuvent être mis en cache, et indexés intelligemment de façon à ce qu’ils ne soient pas recréés à chaque fois. Ici, il suffit d’utiliser le type des propriétés comme clé.

Retenons ici la seconde solution, qui est plus simple à développer, maintenir et tester, et voyons ce que cela donne :

using TesterMethodDelegate = Func<object, bool>;

public class OptimizedDefaultValueTester : IDefaultValueTester
{
    private readonly Dictionary<Type, TesterMethodDelegate> cache = new Dictionary<Type, TesterMethodDelegate>();

    public bool IsDefaultValue(Type type, object value)
    {
        var tester = GetTesterMethodDelegate(type, value);
        return tester(value);
    }

    private TesterMethodDelegate GetTesterMethodDelegate(Type type, object value)
    {
        TesterMethodDelegate tester;
        if(!this.cache.TryGetValue(type, out tester))
        {
            var thisType = typeof(OptimizedDefaultValueTester);
            var methodInfo = thisType.GetMethod("IsDefaultValue", BindingFlags.Static | BindingFlags.Public);
            methodInfo = methodInfo.MakeGenericMethod(type);
            tester = (TesterMethodDelegate) Delegate.CreateDelegate(typeof(TesterMethodDelegate), methodInfo);
            this.cache.Add(type, tester);
        }
        return tester;
    }

    public static bool IsDefaultValue<T>(object obj)
    {
            // cf. extraits de code précédents. 
    }
}

Les points remarquables :

  • Ligne 1 : On utilise le type de délégué générique Func<T,TReturn> du Framework 3.5, derrière un alias (TesterMethodDelegate).
  • Ligne 5 : Les instances de TesterMethodDelegate sont mises en cache grâce à un champ d’instance de type Dictionary<TKey,TValue> et sont indexées par type.
  • Ligne 16 : A chaque appel à IsDefaultValue, on regarde si un TesterMethodDelegate a déjà été créé pour le type passé en paramètre. Si c’est le cas, on récupère l’instance dans le cache, sinon, on la crée avant de l’ajouter au cache.
  • Ligne 10 : L’appel dynamique via la méthode Invoke a disparu, on invoque directement l’instance de TesterMethodDelegate récupérée.

Toujours selon le même test, les performances sont améliorées. On passe de 10625 ms pour 700000 appels, à 506 ms. C’est environ 20 fois mieux, hourra.

Peut-on aller plus loin ?

Pas vraiment, si on ne considère que les performances. Par contre, il est possible d’encapsuler le mécanisme d’optimisation précédent pour favoriser la réutilisabilité. Et pour cela, nous pouvons utiliser la memoization. Sans le savoir c’est un peu ce que nous avons imaginé jusqu’ici.

Cependant, il est possible en utilisant les méthodes anonymes (ou les expressions lambda en C# 3.0) de mettre en place une solution plus élégante, et autorisant la réutilisation du caching comme s’il s’agissait en quelque sorte d’un aspect. J’ai découvert cela en tombant sur ce post et j’ai été séduit.

Le principe consiste à créer une méthode capable de retourner une version mémoizée d’un délégué. On peut même en faire une méthode d’extension générique en C# 3.0:

public static class Memoization
{
    public static Func<T, TResult> Memoize<T, TResult>(this Func<T, TResult> function)
    {
        var cache = new Dictionary<T, TResult>();
        var nullCache = default(TResult);
        var isNullCacheSet = false;
        return  parameter => 
                {
                    TResult value;

                    if (parameter == null && isNullCacheSet)
                    {
                        return nullCache;
                    }

                    if (parameter == null)
                    {
                        nullCache = function(parameter);
                        isNullCacheSet = true;
                        return nullCache;
                    }

                    if (cache.TryGetValue(parameter, out value))
                    {
                        return value;
                    }

                    value = function(parameter);
                    cache.Add(parameter, value);
                    return value;
                };
    }
}

Décortiquons cette méthode :

  • Elle prend en paramètre un délégué de type Func<T,TResult> et retourne un délégué du même type. Pour simplifier, on peut dire que la méthode d’extension prend en paramètre et retourne une fonction dont le prototype est le suivant : TResult Function(T param)
  • La fonction retournée est construite via une expression lambda qui se charge d’encapsuler la logique de caching. Elle a pour rôle de mémoriser le résultat (de type TResult) de la fonction à chaque valeur différente de T pour laquelle on l’appelle.
  • C’est la valeur sauvegardée qui est retournée lorsqu’elle est présente dans le cache. En effet, le résultat de la fonction pour un paramètre donné ayant déjà été déterminé, il ne sert à rien d’exécuter de nouveau la fonction avec le même paramètre.
  • Dans cet exemple, l’expression lambda qui sert à créer la fonction retournée est une closure. C’est ici que réside toute l’ingéniosité de cette technique. Le dictionnaire est une variable locale à la méthode Memoize référencée par la méthode lambda, donc du point de vue de cette dernière l’état du cache sera conservé entre chaque appel.
  • La méthode doit traiter un cas particulier : Si la valeur null est passée en argument de la fonction, il n’est plus possible d’utiliser un dictionnaire pour mettre en cache le résultat puisque les clés de ce dernier ne peuvent être nulles. Nous utilisons donc une variable spécialement dédiée : nullCache.

Bon, la memoization, c’est classe. Mais revenons à notre besoin. Quelle fonction a besoin d’être mémoizée ? Et bien tout simplement celle qui pour un type donné est capable de nous retourner le délégué pointant sur la bonne version fermée de IsDefaultValue<T>.

Une telle fonction peut avoir le prototype suivant :

Func<object, bool> GetTesterMethodDelegate(Type type);

Le type du délégué correspondant est le suivant :

Func<Type, Func<object, bool>>

En effet, il s’agit bien d’une fonction qui prend un type en argument, et qui retourne une autre fonction prenant un objet en argument et retournant un booléen.

Il nous reste plus qu’à examiner la nouvelle implémentation de IDefaultValueTester qui se base sur le principe :

using TesterMethodDelegate = Func<object, bool>;
using TesterMethodLocatorDelegate = Func<Type, Func<object, bool>>;

public class MemoizedDefaultValueTester : IDefaultValueTester
{
    private readonly TesterMethodLocatorDelegate testerLocator;

    public MemoizedDefaultValueTester()
    {
        // On crée une fonction capable de retourner la version fermée 
        // de la méthode IsDefaultValue<T> pour un type donné.
        this.testerLocator = type =>
                             {
                                 var thisType = typeof (OptimizedDefaultValueTester);
                                 var methodInfo = thisType.GetMethod("IsDefaultValue", BindingFlags.Static | BindingFlags.Public);
                                 methodInfo = methodInfo.MakeGenericMethod(type);
                                 var tester = (TesterMethodDelegate)Delegate.CreateDelegate(typeof(TesterMethodDelegate), methodInfo);
                                 return tester;
                             };

        // On mémomize cette fonction en appelant notre méthode d'extension
        this.testerLocator = this.testerLocator.Memoize();
    }

    public bool IsDefaultValue(Type type, object value)
    {
        var tester = this.testerLocator(type);
        return tester(value);
    }

    public static bool IsDefaultValue<T>(object obj)
    {
         // cf. extraits de code précédents. 
    }
}

Nous pouvons constater que :

  • Un nouvel alias (TesterMethodLocatorDelegate) est introduit pour le type générique Func<Type,Func<object,bool>>
  • La classe possède un champ d’instance (testerLocator) de type TesterMethodLocatorDelegate qui est initialisé dans le constructeur.
  • La méthode d’extension Memoize définie plus tôt est utilisée pour mémoizer le TesterMethodLocatorDelegate, toujours au niveau du constructeur.
  • L’implémentation de la méthode IsDefaultValue est extrêmement simplifiée. Via la version mémoizée du TesterMethodLocatorDelegate, on récupère un TesterMethodDelegate qui peut être invoqué directement afin d’effectuer le test sur la valeur passée en paramètre.

Les performances de cette solution sont les mêmes que celles mesurées pour la précédente : environ 510 ms. Mais à présent, nous disposons d’une méthode Memoize pouvant être réutilisée.

Hum. Attendez… Pourquoi ne pas la réutiliser alors, pour memoizer aussi les instances de TesterMethodDelegate retournées par le TesterMethodLocatorDelegate ? Il y aurait ainsi deux niveaux de mémoization, et peut-être à la clé un petit gain de performance supplémentaire.

La modification se limite donc à ajouter un nouvel appel à la méthode Memoize dans le constructeur du MemoizedDefaultValueTester :

public MemoizedDefaultValueTester()
{
    // On crée une fonction capable de retourner la version fermée 
    // de la méthode IsDefaultValue<T> pour un type donné.
    this.testerLocator = type =>
                         {
                             var thisType = typeof (OptimizedDefaultValueTester);
                             var methodInfo = thisType.GetMethod("IsDefaultValue", BindingFlags.Static | BindingFlags.Public);
                             methodInfo = methodInfo.MakeGenericMethod(type);
                             var tester = (TesterMethodDelegate)Delegate.CreateDelegate(typeof(TesterMethodDelegate), methodInfo);

                             // On retourne à présent une version mémoizée du TesterMethodDelegate :
                             return tester.Memoize();
                         };

    // On mémoize cette fonction en appelant notre méthode d'extension
    this.testerLocator = this.testerLocator.Memoize();
}

Le test des performances indique à présent que les 100000 itérations ont eu lieu en 320 ms ! Ce n’est pas aussi efficace que la première étape d’optimisation, mais proportionnellement cela conduit tout de même à une amélioration significative : environ 30%.

Attention quand même : Si notre premier usage de la memoization était justifié, le second peut être très dangereux. Nous gagnons des millisecondes, mais nous perdons des octets. Et dans le scénario actuel, il est même probable que la memoization des TesterMethodDelegate conduise à une OutOfMemoryException rapidement…

Conclusion

Nous nous sommes contentés d’évoquer la solution d’optimisation impliquant la génération de code IL. Dans le contexte de la problématique discutée, elle n’offrait aucun avantage par rapport à celle que nous avons exposée. Pire, elle imposait une étape inutile. Toutefois, certains besoins plus complexes dépassent le cadre des invocations dynamiques de méthodes et peuvent tout de même être adressés efficacement en recourant à la génération de bytecode. Je pense que cela sera le sujet d’un prochain article.

Quant à la technique détaillée dans celui-ci, elle est simple et permet d’obtenir des performances acceptables dans la majorité des scénarios pour lesquels l’utilisation de la réflexion est nécessaire.

Cela fait presque deux raisons de ne plus avoir systématiquement peur de la réflexion. Par contre, si vous n’en avez pas peur du tout, il serait peut-être temps de s’y mettre doucement…

Edit : Un autre article complète à présent celui-ci.
Tagged with: , , , ,

9 Réponses

Subscribe to comments with RSS.

  1. grozeille said, on mai 3, 2008 at 9:15

    La première solution (Utiliser la Lightweight Code Generation (LCG) via Reflection.Emit) m’intéresse.
    Si t’as un autre exemple dans un prochain poste…

  2. Romain Verdier said, on mai 5, 2008 at 11:04

    La solution à base de LCG était juste citée pour qu’on garde en tête qu’il s’agit d’une des techniques d’optimisation possible lorsqu’on fait appel à la réflexion. Dans l’exemple de l’article, il n’est pas judicieux de l’utiliser puisqu’elle nécessite toutes les étapes requises par la seconde solution. En gros, celle qui est décrite dans l’article peut-être résumée de la façon suivante :

    1. Appel à GetMethod pour récupérer les infos de la méthode générique
    2. Appel à MakeGenericMethod pour récupérer les infos de la méthode générique fermée
    3. Creation d’un délégué via l’appel à CreateDelegate sur la classe statique Delegate.

    Celle qui implique la LCG reprend les 2 premières étapes, puis ajoute :

    3. Création d’une DynamicMethod et génération de son corps en faisant référence au MethodInfo récupéré à l’étape 2.
    4. Creation d’un délégué via l’appel à CreateDelegate sur la classe statique DynamicMethod.

    La génération d’une méthode dynamique est ici inutile, puisqu’il n’y a qu’une invocation de méthode à faire. la LCG devient utile dans des scénarios plus complexes. Je vais essayer de faire un petit post à ce sujet.

  3. safia said, on mai 5, 2008 at 8:31

    Vous êtes des geeks.

  4. grozeille said, on mai 5, 2008 at 8:56

    Euh…fait un tour sur copine de geek pour avoir des conseils sur comment nous supporter :)
    CreateDelegate est aussi utilisé pour appeler des méthodes en « PInvoke dynamique ».
    Merci pour l’astuce de la Memoization, c’est malin et j’y ai jamais pensé. En même temps, j’en ai jamais vraiment eux besoin…
    En tout cas, on invente rien: soit on re-calcule toujours tout (réflection, etc.) soit on le mémorise. Comme tu le dis, c’est le dilemme CPU vs Mémoire. Et si on peut gagner 30% de perf en utilisant très peu de mémoire en plus…

  5. […] Publié dans Articles by Romain Verdier sur mai 6th, 2008 Cet article est un complément du précédent. Vous pouviez y lire dans la conclusion : Nous nous sommes contentés d’évoquer la solution […]

  6. simon mourier said, on juin 16, 2008 at 10:20

    Au delà de l’aspect pédagogique de l’article :-), on peut aller plus vite je pense:

    public bool IsDefaultValue(Type type, object value)
    {
    if (type == null)
    throw new ArgumentNullException(« type »);

    if (!type.IsValueType)
    return (value == null);

    if (value == null)
    return false;

    return ((ValueType)value).Equals(Activator.CreateInstance(type));
    }

  7. Romain Verdier said, on juin 16, 2008 at 10:50

    Il me fallait un exemple :) Celui choisi pour l’article sert juste de support ; il est évident que le vrai problème n’est pas de savoir comment tester la valeur par défaut d’un type.

    Si on ajoute le test sur les nullables, ta méthode fonctionne et a pour elle d’être concise. C’est probablement celle que j’aurais utilisée.

    Une seule (petite) remarque cependant : le mot clé default repose sur l’opération initobj en CIL, ce qui le rend plus performant qu’une instanciation dynamique via la méthode CreateInstance. Lorsqu’on est dans un contexte générique, ou qu’on connait le type de l’objet, autant l’utiliser…

  8. Tony THONG said, on avril 30, 2013 at 3:43

    Bonjour,

    Il y a une autre solution assez simple et plus performante pour creer un cache dont la cle est du type System.Type.

    En effet, il faut pour conserver un maximum de performance preserver du code generique qui evite les problematiques de performance liees au boxing.

    L’astuce consiste a ne jamais implementer directement une methode generique, ici le fameux IsDefaultValue.

    Il faut plutot creer une classe generique avec une methode, par exemple static public class Default {}.

    Le tout avec un delegue (qui pointe vers une methode emittee qui appelle la methode cible) dans le constructeur statique et une methode appelant celui ci : bool Default.Is(T value) { return Default.MyDelegate(value); }
    Cette technique peut nettement ameliorer les performances.

    Pour des raisons de syntaxe simplifee (generique auto parametre) il suffit ensuite de produire la methode generique IsDefaultValue(T value) { return Default.Is(value); }

    note : biensur, ceci est possible uniquement dans un contexte purement generique.

    Cordialement.


Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

Suivre

Recevez les nouvelles publications par mail.

%d blogueurs aiment cette page :