Puis-je ajouter des méthodes d'extension à une classe statique existante?

535

Je suis fan des méthodes d'extension en C #, mais je n'ai pas réussi à ajouter une méthode d'extension à une classe statique, telle que Console.

Par exemple, si je veux ajouter une extension à la console, appelée 'WriteBlueLine', pour pouvoir aller:

Console.WriteBlueLine("This text is blue");

J'ai essayé cela en ajoutant une méthode statique publique locale, avec la console comme paramètre 'this' ... mais pas de dés!

public static class Helpers {
    public static void WriteBlueLine(this Console c, string text)
    {
        Console.ForegroundColor = ConsoleColor.Blue;
        Console.WriteLine(text);
        Console.ResetColor();
    }
}

Cela n'a pas ajouté de méthode 'WriteBlueLine' à la console ... est-ce que je me trompe? Ou demander l'impossible?

Leon Bambrick
la source
3
Tant pis. malheureux mais je pense que je m'en sortirai. Je suis TOUJOURS une méthode d'extension vierge (dans le code de production de toute façon). Peut-être un jour, si j'ai de la chance.
Andy McCluggage
J'ai écrit un certain nombre d'extensions HtmlHelper pour ASP.NET MVC. A écrit un pour DateTime pour me donner la fin de la date donnée (23: 59.59). Utile lorsque vous demandez à l'utilisateur de spécifier une date de fin, mais que vous voulez vraiment que ce soit la fin de cette journée.
tvanfosson
12
Il n'y a aucun moyen de les ajouter actuellement car la fonctionnalité n'existe pas en C #. Non pas parce que c'est impossible en soi , mais parce que les voyants C # sont très occupés, étaient principalement intéressés par les méthodes d'extension pour faire fonctionner LINQ et ne voyaient pas suffisamment d'avantages dans les méthodes d'extension statiques pour justifier le temps qu'ils mettraient à mettre en œuvre. Eric Lippert explique ici .
Jordan Grey
1
Appelez juste Helpers.WriteBlueLine(null, "Hi");:)
Hüseyin Yağlı

Réponses:

286

Les méthodes d'extension nécessitent une variable d'instance (valeur) pour un objet. Vous pouvez cependant écrire un wrapper statique autour de l' ConfigurationManagerinterface. Si vous implémentez l'encapsuleur, vous n'avez pas besoin d'une méthode d'extension car vous pouvez simplement ajouter la méthode directement.

 public static class ConfigurationManagerWrapper
 {
      public static ConfigurationSection GetSection( string name )
      {
         return ConfigurationManager.GetSection( name );
      }

      .....

      public static ConfigurationSection GetWidgetSection()
      {
          return GetSection( "widgets" );
      }
 }
tvanfosson
la source
8
@Luis - dans le contexte, l'idée serait "pourrais-je ajouter une méthode d'extension à la classe ConfigurationManager pour obtenir une section spécifique?" Vous ne pouvez pas ajouter une méthode d'extension à une classe statique car elle nécessite une instance de l'objet, mais vous pouvez écrire une classe wrapper (ou façade) qui implémente la même signature et reporte l'appel réel au vrai ConfigurationManager. Vous pouvez ajouter la méthode que vous souhaitez à la classe wrapper afin qu'elle n'ait pas besoin d'être une extension.
tvanfosson
Je trouve plus utile d'ajouter simplement une méthode statique à la classe implémentant ConfigurationSection. Donc, étant donné une implémentation appelée MyConfigurationSection, j'appellerais MyConfigurationSection.GetSection (), qui renvoie la section déjà tapée, ou null si elle n'existe pas. Le résultat final est le même, mais il évite d'ajouter une classe.
appuyez sur
1
@tap - ce n'est qu'un exemple, et le premier qui m'est venu à l'esprit. Le principe de la responsabilité unique entre cependant en jeu. Le "conteneur" doit-il être réellement responsable de son interprétation à partir du fichier de configuration? Normalement, j'ai simplement ConfigurationSectionHandler et convertis la sortie de ConfigurationManager dans la classe appropriée et ne me soucie pas du wrapper.
tvanfosson
5
Pour une utilisation en interne, j'ai commencé à créer des variantes 'X' de classes statiques et de structures pour ajouter des extensions personnalisées: 'ConsoleX' contient de nouvelles méthodes statiques pour 'Console', 'MathX' contient de nouvelles méthodes statiques pour 'Math', 'ColorX' étend les méthodes «Couleur», etc. Pas tout à fait la même chose, mais facile à mémoriser et à découvrir dans IntelliSense.
user1689175
1
@Xtro Je suis d'accord, c'est horrible, mais pas pire que de ne pas pouvoir utiliser un double test à sa place ou, pire, abandonner le test de votre code parce que les classes statiques le rendent si difficile. Microsoft semble être d'accord avec moi parce que c'est la raison pour laquelle ils ont introduit les classes HttpContextWrapper / HttpContextBase pour contourner le HttpContext.Current statique pour MVC.
tvanfosson
92

Pouvez-vous ajouter des extensions statiques aux classes en C #? Non mais vous pouvez le faire:

public static class Extensions
{
    public static T Create<T>(this T @this)
        where T : class, new()
    {
        return Utility<T>.Create();
    }
}

public static class Utility<T>
    where T : class, new()
{
    static Utility()
    {
        Create = Expression.Lambda<Func<T>>(Expression.New(typeof(T).GetConstructor(Type.EmptyTypes))).Compile();
    }
    public static Func<T> Create { get; private set; }
}

Voici comment ça fonctionne. Bien que vous ne puissiez techniquement pas écrire de méthodes d'extension statiques, ce code exploite plutôt une faille dans les méthodes d'extension. Cette lacune étant que vous pouvez appeler des méthodes d'extension sur des objets null sans obtenir l'exception null (sauf si vous accédez à quoi que ce soit via @this).

Alors, voici comment vous utiliseriez ceci:

    var ds1 = (null as DataSet).Create(); // as oppose to DataSet.Create()
    // or
    DataSet ds2 = null;
    ds2 = ds2.Create();

    // using some of the techniques above you could have this:
    (null as Console).WriteBlueLine(...); // as oppose to Console.WriteBlueLine(...)

Maintenant, POURQUOI ai-je choisi d'appeler le constructeur par défaut à titre d'exemple, et pourquoi ne retourne-je pas simplement T () dans le premier extrait de code sans faire toutes ces ordures d'expression? Eh bien aujourd'hui, votre jour de chance car vous obtenez un 2fer. Comme tout développeur .NET avancé le sait, le nouveau T () est lent car il génère un appel à System.Activator qui utilise la réflexion pour obtenir le constructeur par défaut avant de l'appeler. Merde, Microsoft! Cependant, mon code appelle directement le constructeur par défaut de l'objet.

Les extensions statiques seraient mieux que cela, mais les temps désespérés appellent des mesures désespérées.

M. Obnoxious
la source
2
Je pense que pour Dataset, cette astuce fonctionnera, mais je doute que cela fonctionne pour la classe Console car la console est une classe statique, les types statiques ne peuvent pas être utilisés comme arguments :)
ThomasBecker
Ouais, j'allais dire la même chose. Il s'agit de méthodes d'extension pseudo-statiques sur une classe non statique. L'OP était une méthode d'extension sur une classe statique.
Mark A. Donohoe
2
Il est beaucoup mieux et plus facile de simplement avoir une convention de dénomination pour de telles méthodes XConsole, ConsoleHelperetc.
Alex Zhukovskiy
9
C'est une astuce fascinante, mais le résultat est malodorant. Vous créez un objet nul, puis semblez y appeler une méthode - malgré des années où l'on vous a dit que "l'appel d'une méthode sur un objet nul provoque une exception". Cela fonctionne, mais ... rire ... Confus à quiconque entretient plus tard. Je ne vais pas voter contre, car vous avez ajouté au pool d'informations sur ce qui est possible. Mais j'espère sincèrement que personne n'utilisera jamais cette technique !! Plainte supplémentaire: ne transmettez pas l'un de ces éléments à une méthode et attendez-vous à obtenir un sous-classement OO: la méthode appelée sera le type de déclaration de paramètre et non le type de paramètre transmis .
ToolmakerSteve
5
C'est délicat, mais j'aime ça. Une alternative (null as DataSet).Create();pourrait être default(DataSet).Create();.
Bagerfahrer
54

Ce n'est pas possible.

Et oui, je pense que MS a fait une erreur ici.

Leur décision n'a pas de sens et oblige les programmeurs à écrire (comme décrit ci-dessus) une classe wrapper inutile.

Voici un bon exemple: Essayer d'étendre la classe de test MS Unit statique Assert: Je veux 1 méthode Assert de plus AreEqual(x1,x2).

La seule façon de le faire est de pointer vers différentes classes ou d'écrire un wrapper autour de 100s de différentes méthodes Assert. Pourquoi!?

Si la décision a été prise d'autoriser les extensions d'instances, je ne vois aucune raison logique de ne pas autoriser les extensions statiques. Les arguments concernant la sectionnement des bibliothèques ne tiennent pas une fois que les instances peuvent être étendues.

Tom Deloford
la source
20
J'essayais également d'étendre la classe MS Unit Test Assert pour ajouter Assert.Throws et Assert.DoesNotThrow et j'ai fait face au même problème.
Stefano Ricciardi
3
Ouais moi aussi :( Je pensais que je pouvais faire Assert.Throwsà la réponse stackoverflow.com/questions/113395/…
CallMeLaNN
27

Je suis tombé sur ce fil tout en essayant de trouver une réponse à la même question que l'OP avait. Je n'ai pas trouvé la réponse que je voulais mais j'ai fini par le faire.

public static class MyConsole
{
    public static void WriteLine(this ConsoleColor Color, string Text)
    {
        Console.ForegroundColor = Color;
        Console.WriteLine(Text);   
    }
}

Et je l'utilise comme ceci:

ConsoleColor.Cyan.WriteLine("voilà");
Adel G.Eibesh
la source
19

Vous pourriez peut-être ajouter une classe statique avec votre espace de noms personnalisé et le même nom de classe:

using CLRConsole = System.Console;

namespace ExtensionMethodsDemo
{
    public static class Console
    {
        public static void WriteLine(string value)
        {
            CLRConsole.WriteLine(value);
        }

        public static void WriteBlueLine(string value)
        {
            System.ConsoleColor currentColor = CLRConsole.ForegroundColor;

            CLRConsole.ForegroundColor = System.ConsoleColor.Blue;
            CLRConsole.WriteLine(value);

            CLRConsole.ForegroundColor = currentColor;
        }

        public static System.ConsoleKeyInfo ReadKey(bool intercept)
        {
            return CLRConsole.ReadKey(intercept);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.WriteBlueLine("This text is blue");   
            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.StackTrace);
            }

            Console.WriteLine("Press any key to continue...");
            Console.ReadKey(true);
        }
    }
}
Pag Sun
la source
1
Mais cela ne résout pas le problème de la nécessité de réimplémenter chaque méthode unique de la classe statique d'origine que vous souhaitez conserver dans votre wrapper. C'est toujours un wrapper, bien qu'il ait le mérite d'avoir besoin de moins de changements dans le code qui l'utilise…
binki
11

Nan. Les définitions de méthode d'extension nécessitent une instance du type que vous étendez. C'est regrettable; Je ne sais pas pourquoi c'est nécessaire ...


la source
4
C'est parce qu'une méthode d'extension est utilisée pour étendre une instance d'un objet. S'ils ne le faisaient pas, ce ne seraient que des méthodes statiques régulières.
Derek Ekins
31
Ce serait bien de faire les deux, non?
7

Quant aux méthodes d'extension, les méthodes d'extension elles-mêmes sont statiques; mais elles sont invoquées comme s'il s'agissait de méthodes d'instance. Puisqu'une classe statique n'est pas instanciable, vous n'auriez jamais d'instance de la classe à partir de laquelle appeler une méthode d'extension. Pour cette raison, le compilateur ne permet pas de définir des méthodes d'extension pour les classes statiques.

M. Obnoxious a écrit: "Comme tout développeur .NET avancé le sait, le nouveau T () est lent car il génère un appel à System.Activator qui utilise la réflexion pour obtenir le constructeur par défaut avant de l'appeler".

New () est compilé dans l'instruction IL "newobj" si le type est connu au moment de la compilation. Newobj prend un constructeur pour une invocation directe. Les appels à System.Activator.CreateInstance () sont compilés dans l'instruction IL "call" pour appeler System.Activator.CreateInstance (). New () lorsqu'il est utilisé contre des types génériques entraînera un appel à System.Activator.CreateInstance (). Le message de M. Obnoxious n'était pas clair sur ce point ... et bien, désagréable.

Ce code:

System.Collections.ArrayList _al = new System.Collections.ArrayList();
System.Collections.ArrayList _al2 = (System.Collections.ArrayList)System.Activator.CreateInstance(typeof(System.Collections.ArrayList));

produit cet IL:

  .locals init ([0] class [mscorlib]System.Collections.ArrayList _al,
           [1] class [mscorlib]System.Collections.ArrayList _al2)
  IL_0001:  newobj     instance void [mscorlib]System.Collections.ArrayList::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldtoken    [mscorlib]System.Collections.ArrayList
  IL_000c:  call       class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
  IL_0011:  call       object [mscorlib]System.Activator::CreateInstance(class [mscorlib]System.Type)
  IL_0016:  castclass  [mscorlib]System.Collections.ArrayList
  IL_001b:  stloc.1
Brian Griffin
la source
5

Vous ne pouvez pas ajouter d' électricité statique méthodes à un type. Vous pouvez uniquement ajouter des méthodes (pseudo-) d'instance à une instance d'un type.

Le but du thismodificateur est de dire au compilateur C # de passer l'instance sur le côté gauche de la .comme premier paramètre de la méthode statique / d'extension.

Dans le cas de l'ajout de méthodes statiques à un type, il n'y a aucune instance à passer pour le premier paramètre.

Brannon
la source
4

J'ai essayé de le faire avec System.Environment lorsque j'apprenais les méthodes d'extension et que je n'ai pas réussi. La raison est, comme d'autres le mentionnent, car les méthodes d'extension nécessitent une instance de la classe.

Robert S.
la source
3

Il n'est pas possible d'écrire une méthode d'extension, mais il est possible d'imiter le comportement que vous demandez.

using FooConsole = System.Console;

public static class Console
{
    public static void WriteBlueLine(string text)
    {
        FooConsole.ForegroundColor = ConsoleColor.Blue;
        FooConsole.WriteLine(text);
        FooConsole.ResetColor();
    }
}

Cela vous permettra d'appeler Console.WriteBlueLine (fooText) dans d'autres classes. Si les autres classes veulent accéder aux autres fonctions statiques de la console, elles devront être explicitement référencées via leur espace de noms.

Vous pouvez toujours ajouter toutes les méthodes dans la classe de remplacement si vous souhaitez les avoir toutes au même endroit.

Vous auriez donc quelque chose comme

using FooConsole = System.Console;

public static class Console
{
    public static void WriteBlueLine(string text)
    {
        FooConsole.ForegroundColor = ConsoleColor.Blue;
        FooConsole.WriteLine(text);
        FooConsole.ResetColor();
    }
    public static void WriteLine(string text)
    {
        FooConsole.WriteLine(text);
    }
...etc.
}

Cela fournirait le type de comportement que vous recherchez.

* Remarque La console devra être ajoutée via l'espace de noms dans lequel vous l'avez placée.

Douglas Potesta
la source
1

oui, dans un sens limité.

public class DataSet : System.Data.DataSet
{
    public static void SpecialMethod() { }
}

Cela fonctionne mais pas la console car elle est statique.

public static class Console
{       
    public static void WriteLine(String x)
    { System.Console.WriteLine(x); }

    public static void WriteBlueLine(String x)
    {
        System.Console.ForegroundColor = ConsoleColor.Blue;
        System.Console.Write(.x);           
    }
}

Cela fonctionne car tant qu'il n'est pas sur le même espace de noms. Le problème est que vous devez écrire une méthode statique proxy pour chaque méthode que System.Console possède. Ce n'est pas nécessairement une mauvaise chose car vous pouvez ajouter quelque chose comme ceci:

    public static void WriteLine(String x)
    { System.Console.WriteLine(x.Replace("Fck","****")); }

ou

 public static void WriteLine(String x)
    {
        System.Console.ForegroundColor = ConsoleColor.Blue;
        System.Console.WriteLine(x); 
    }

La façon dont cela fonctionne est que vous connectez quelque chose à la ligne d'écriture standard. Il peut s'agir d'un nombre de lignes ou d'un mauvais filtre de mots ou autre. Chaque fois que vous spécifiez simplement Console dans votre espace de noms, dites WebProject1 et importez le système d'espace de noms, WebProject1.Console sera choisi par défaut sur System.Console pour ces classes dans l'espace de noms WebProject1. Ainsi, ce code transformera tous les appels Console.WriteLine en bleu dans la mesure où vous n'avez jamais spécifié System.Console.WriteLine.

Chien noir
la source
malheureusement, l'approche consistant à utiliser un descendant ne fonctionne pas lorsque la classe de base est scellée (comme beaucoup dans la bibliothèque de classes .NET)
George Birbilis
1

Ce qui suit a été rejeté comme une modification de la réponse de tvanfosson. On m'a demandé de contribuer comme ma propre réponse. J'ai utilisé sa suggestion et terminé la mise en œuvre d'un ConfigurationManagerwrapper. En principe, j'ai simplement rempli la ...réponse de tvanfosson.

Les méthodes d'extension nécessitent une instance d'un objet. Vous pouvez cependant écrire un wrapper statique autour de l'interface ConfigurationManager. Si vous implémentez l'encapsuleur, vous n'avez pas besoin d'une méthode d'extension car vous pouvez simplement ajouter la méthode directement.

public static class ConfigurationManagerWrapper
{
    public static NameValueCollection AppSettings
    {
        get { return ConfigurationManager.AppSettings; }
    }

    public static ConnectionStringSettingsCollection ConnectionStrings
    {
        get { return ConfigurationManager.ConnectionStrings; }
    }

    public static object GetSection(string sectionName)
    {
        return ConfigurationManager.GetSection(sectionName);
    }

    public static Configuration OpenExeConfiguration(string exePath)
    {
        return ConfigurationManager.OpenExeConfiguration(exePath);
    }

    public static Configuration OpenMachineConfiguration()
    {
        return ConfigurationManager.OpenMachineConfiguration();
    }

    public static Configuration OpenMappedExeConfiguration(ExeConfigurationFileMap fileMap, ConfigurationUserLevel userLevel)
    {
        return ConfigurationManager.OpenMappedExeConfiguration(fileMap, userLevel);
    }

    public static Configuration OpenMappedMachineConfiguration(ConfigurationFileMap fileMap)
    {
        return ConfigurationManager.OpenMappedMachineConfiguration(fileMap);
    }

    public static void RefreshSection(string sectionName)
    {
        ConfigurationManager.RefreshSection(sectionName);
    }
}
André C. Andersen
la source
0

Vous pouvez utiliser un cast sur null pour le faire fonctionner.

public static class YoutTypeExtensionExample
{
    public static void Example()
    {
        ((YourType)null).ExtensionMethod();
    }
}

L'extension:

public static class YourTypeExtension
{
    public static void ExtensionMethod(this YourType x) { }
}

Ton type:

public class YourType { }
Wouter
la source
-4

Vous POUVEZ le faire si vous êtes prêt à "frigorifier" un peu en créant une variable de la classe statique et en l'assignant à null. Cependant, cette méthode ne serait pas disponible pour les appels statiques sur la classe, donc vous ne savez pas combien elle serait utile:

Console myConsole = null;
myConsole.WriteBlueLine("my blue line");

public static class Helpers {
    public static void WriteBlueLine(this Console c, string text)
    {
        Console.ForegroundColor = ConsoleColor.Blue;
        Console.WriteLine(text);
        Console.ResetColor();
    }
}
Tenaka
la source
c'est exactement ce que j'ai fait. Ma classe s'appelle MyTrace :)
Gishu
Conseil utile. un peu d'odeur de code, mais je suppose que nous pourrions cacher l'objet nul dans une classe de base ou quelque chose. Merci.
Tom Deloford
1
Je ne peux pas compiler ce code. Erreur «System.Console»: les types statiques ne peuvent pas être utilisés comme paramètres
kuncevic.dev
Oui, cela ne peut pas être fait. Merde, je pensais que vous étiez sur quelque chose là-bas! Les types statiques ne peuvent pas être passés en tant que paramètres dans des méthodes, ce qui est logique, je suppose. Espérons simplement que MS voit le bois des arbres sur celui-ci et le change.
Tom Deloford
3
J'aurais dû essayer de compiler mon propre code! Comme le dit Tom, cela ne fonctionnera pas avec les classes statiques.
Tenaka