¿Puedo agregar métodos de extensión a una clase estática existente?

535

Soy fanático de los métodos de extensión en C #, pero no he tenido éxito al agregar un método de extensión a una clase estática, como la consola.

Por ejemplo, si quiero agregar una extensión a la consola, llamada 'WriteBlueLine', para poder ir:

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

Intenté esto agregando un método estático público local, con la consola como un parámetro 'esto' ... ¡pero sin dados!

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

Esto no agregó un método 'WriteBlueLine' a la consola ... ¿Lo estoy haciendo mal? ¿O pedir lo imposible?

Leon Bambrick
fuente
3
Oh bien. desafortunado pero creo que me las arreglaré. Todavía soy un método de extensión virgen (en el código de producción de todos modos). Tal vez algún día, si tengo suerte.
Andy McCluggage
He escrito varias extensiones HtmlHelper para ASP.NET MVC. Escribió uno para DateTime para darme el final de la fecha dada (23: 59.59). Es útil cuando le pide al usuario que especifique una fecha de finalización, pero realmente quiere que sea el final de ese día.
tvanfosson el
12
No hay forma de agregarlos actualmente porque la función no existe en C #. No porque sea imposible per se , sino porque los píos de C # están muy ocupados, estaban interesados ​​principalmente en los métodos de extensión para hacer que LINQ funcione y no vieron suficientes beneficios en los métodos de extensión estática para justificar el tiempo que tomarían implementar. Eric Lippert explica aquí .
Jordan Grey
1
Simplemente llame Helpers.WriteBlueLine(null, "Hi");:)
Hüseyin Yağlı

Respuestas:

286

No. Los métodos de extensión requieren una variable de instancia (valor) para un objeto. Sin embargo, puede escribir un contenedor estático alrededor de la ConfigurationManagerinterfaz. Si implementa el reiniciador, no necesita un método de extensión ya que simplemente puede agregar el método directamente.

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

      .....

      public static ConfigurationSection GetWidgetSection()
      {
          return GetSection( "widgets" );
      }
 }
tvanfosson
fuente
8
@Luis: en contexto, la idea sería "¿podría agregar un método de extensión a la clase ConfigurationManager para obtener una sección específica?" No puede agregar un método de extensión a una clase estática, ya que requiere una instancia del objeto, pero puede escribir una clase de envoltura (o fachada) que implemente la misma firma y difiera la llamada real al ConfigurationManager real. Puede agregar el método que desee a la clase contenedora para que no sea una extensión.
tvanfosson
Me resulta más útil simplemente agregar un método estático a la clase que implementa ConfigurationSection. Entonces, dada una implementación llamada MyConfigurationSection, llamaría a MyConfigurationSection.GetSection (), que devuelve la sección ya escrita o nula si no existe. El resultado final es el mismo, pero evita agregar una clase.
toca el
1
@tap: es solo un ejemplo, y el primero que me vino a la mente. Sin embargo, el principio de responsabilidad única entra en juego. ¿Debería el "contenedor" ser realmente responsable de la interpretación del archivo de configuración? Normalmente, simplemente tengo ConfigurationSectionHandler y envío la salida de ConfigurationManager a la clase adecuada y no me molesto con el contenedor.
tvanfosson
55
Para uso interno, comencé a crear variantes 'X' de clases y estructuras estáticas para agregar extensiones personalizadas: 'ConsoleX' contiene nuevos métodos estáticos para 'Console', 'MathX' contiene nuevos métodos estáticos para 'Math', 'ColorX' extiende los métodos de 'Color', etc. No es lo mismo, pero es fácil de recordar y descubrir en IntelliSense.
user1689175
1
@Xtro Estoy de acuerdo en que es horrible, pero no es peor que no poder usar una prueba doble en su lugar o, peor aún, renunciar a probar su código porque las clases estáticas lo hacen muy difícil. Microsoft parece estar de acuerdo conmigo porque es la razón por la que introdujeron las clases HttpContextWrapper / HttpContextBase para sortear el HttpContext.Current estático para MVC.
tvanfosson
92

¿Se pueden agregar extensiones estáticas a las clases en C #? No, pero puedes hacer esto:

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

Así es como funciona. Si bien técnicamente no puede escribir métodos de extensión estáticos, en su lugar, este código explota una laguna en los métodos de extensión. Esa laguna es que puede llamar a métodos de extensión en objetos nulos sin obtener la excepción nula (a menos que acceda a algo a través de @this).

Así que así es como usarías esto:

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

Ahora, ¿POR QUÉ elegí llamar al constructor predeterminado como ejemplo y Y por qué no solo devuelvo el nuevo T () en el primer fragmento de código sin hacer toda esa basura Expression? Bueno, hoy es tu día de suerte porque obtienes un 2fer. Como cualquier desarrollador avanzado de .NET sabe, el nuevo T () es lento porque genera una llamada a System.Activator que utiliza la reflexión para obtener el constructor predeterminado antes de llamarlo. ¡Maldita sea Microsoft! Sin embargo, mi código llama al constructor predeterminado del objeto directamente.

Las extensiones estáticas serían mejores que esto, pero los tiempos desesperados requieren medidas desesperadas.

Señor desagradable
fuente
2
Creo que para Dataset este truco funcionará, pero dudo que funcione para la clase Console ya que Console es una clase estática, los tipos estáticos no pueden usarse como argumentos :)
ThomasBecker
Sí, iba a decir lo mismo. Se trata de métodos de extensión pseudoestática en una clase no estática. El OP era un método de extensión en una clase estática.
Mark A. Donohoe
2
Es mucho mejor y más fácil tener alguna convención de nomenclatura para métodos como XConsole, ConsoleHelperetc.
Alex Zhukovskiy
99
Este es un truco fascinante, pero el resultado es maloliente. Usted crea un objeto nulo, luego parece llamar a un método en él, a pesar de años de que le dijeron que "llamar a un método en un objeto nulo causa una excepción". Funciona, pero ..ugh ... Confuso para cualquiera que lo mantenga más tarde. No votaré negativamente, porque has agregado al conjunto de información lo que es posible. ¡Pero sinceramente espero que nadie use esta técnica! Queja adicional: no pase uno de estos a un método, y espere obtener subclases OO: el método llamado será tipo de declaración de parámetro, no tipo de parámetro pasado .
ToolmakerSteve
55
Esto es complicado, pero me gusta. Una alternativa a (null as DataSet).Create();podría ser default(DataSet).Create();.
Bagerfahrer
54

No es posible.

Y sí, creo que MS cometió un error aquí.

Su decisión no tiene sentido y obliga a los programadores a escribir (como se describió anteriormente) una clase de envoltura sin sentido.

Aquí hay un buen ejemplo: Intentar extender la clase de prueba estática de MS Unit Assert: Quiero 1 método Assert más AreEqual(x1,x2).

La única forma de hacerlo es apuntar a diferentes clases o escribir un contenedor alrededor de cientos de métodos Assert diferentes. ¿¡Por qué!?

Si se tomó la decisión de permitir extensiones de instancias, no veo ninguna razón lógica para no permitir extensiones estáticas. Los argumentos sobre seccionar bibliotecas no resisten una vez que se pueden extender las instancias.

Tom Deloford
fuente
20
También estaba tratando de extender la clase Assert de MS Unit Test para agregar Assert.Throws y Assert.DoesNotThrow y enfrenté el mismo problema.
Stefano Ricciardi
3
Sí, yo también :( Pensé que podía Assert.Throwsresponder a la respuesta stackoverflow.com/questions/113395/…
CallMeLaNN
27

Me topé con este hilo mientras intentaba encontrar una respuesta a la misma pregunta que tenía el OP. No encontré la respuesta que quería pero terminé haciendo esto.

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

Y lo uso así:

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

Tal vez podría agregar una clase estática con su espacio de nombres personalizado y el mismo nombre de clase:

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
fuente
1
Pero esto no resuelve el problema de la necesidad de volver a implementar cada método de la clase estática original que desea mantener en su contenedor. Es todavía un envoltorio, aunque tiene el mérito de tener un menor número de cambios en el código que utiliza ...
binki
11

No Las definiciones de métodos de extensión requieren una instancia del tipo que está extendiendo. Es desafortunado; No estoy seguro de por qué se requiere ...


fuente
44
Esto se debe a que se usa un método de extensión para extender una instancia de un objeto. Si no lo hicieran, solo serían métodos estáticos regulares.
Derek Ekins
31
Sería bueno hacer ambas cosas, ¿no?
7

En cuanto a los métodos de extensión, los métodos de extensión en sí mismos son estáticos; pero se invocan como si fueran métodos de instancia. Como una clase estática no es instanciable, nunca tendría una instancia de la clase para invocar un método de extensión. Por esta razón, el compilador no permite que se definan métodos de extensión para clases estáticas.

Obnoxious escribió: "Como cualquier desarrollador avanzado de .NET sabe, el nuevo T () es lento porque genera una llamada a System.Activator que utiliza la reflexión para obtener el constructor predeterminado antes de llamarlo".

New () se compila con la instrucción IL "newobj" si el tipo se conoce en tiempo de compilación. Newobj toma un constructor para invocación directa. Las llamadas a System.Activator.CreateInstance () se compilan con la instrucción "call" de IL para invocar System.Activator.CreateInstance (). New () cuando se usa contra tipos genéricos dará como resultado una llamada a System.Activator.CreateInstance (). La publicación del Sr. Obnoxious no estaba clara en este punto ... y bueno, desagradable.

Este código:

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

produce esta 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
fuente
5

No puede agregar métodos estáticos a un tipo. Solo puede agregar (pseudo-) métodos de instancia a una instancia de un tipo.

El objetivo del thismodificador es decirle al compilador de C # que pase la instancia en el lado izquierdo .del primer parámetro del método estático / de extensión.

En el caso de agregar métodos estáticos a un tipo, no hay ninguna instancia para pasar el primer parámetro.

Brannon
fuente
4

Traté de hacer esto con System.Environment cuando estaba aprendiendo métodos de extensión y no tuve éxito. La razón es, como otros mencionan, porque los métodos de extensión requieren una instancia de la clase.

Robert S.
fuente
3

No es posible escribir un método de extensión, sin embargo, es posible imitar el comportamiento que está solicitando.

using FooConsole = System.Console;

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

Esto le permitirá llamar a Console.WriteBlueLine (fooText) en otras clases. Si las otras clases desean acceder a las otras funciones estáticas de la Consola, deberán ser referenciadas explícitamente a través de su espacio de nombres.

Siempre puede agregar todos los métodos a la clase de reemplazo si desea tenerlos todos en un solo lugar.

Entonces tendrías algo como

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

Esto proporcionaría el tipo de comportamiento que está buscando.

* Nota La consola tendrá que agregarse a través del espacio de nombres en el que la colocó.

Douglas Potesta
fuente
1

Sí, en un sentido limitado.

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

Esto funciona pero la consola no lo hace porque es estática.

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

Esto funciona porque siempre que no esté en el mismo espacio de nombres. El problema es que debe escribir un método estático proxy para cada método que tenga System.Console. No es necesariamente algo malo, ya que puede agregar algo como esto:

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

o

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

La forma en que funciona es que conectas algo a la línea de escritura estándar. Podría ser un recuento de líneas o un filtro de palabras malas o lo que sea. Siempre que especifique la consola en su espacio de nombres, diga WebProject1 e importe el espacio de nombres Sistema, WebProject1.Console se elegirá sobre System.Console como predeterminado para esas clases en el espacio de nombres WebProject1. Entonces, este código convertirá todas las llamadas de Console.WriteLine en azul en la medida en que nunca especificó System.Console.WriteLine.

Perro negro
fuente
desafortunadamente, el enfoque de usar un descendiente no funciona cuando la clase base está sellada (como muchos en la biblioteca de clases .NET)
George Birbilis
1

Lo siguiente fue rechazado como una edición de la respuesta de tvanfosson. Me pidieron que contribuyera como mi propia respuesta. Usé su sugerencia y terminé la implementación de un ConfigurationManagercontenedor. En principio, simplemente ...completé la respuesta de tvanfosson.

No. Los métodos de extensión requieren una instancia de un objeto. Sin embargo, puede escribir un contenedor estático alrededor de la interfaz de ConfigurationManager. Si implementa el reiniciador, no necesita un método de extensión ya que solo puede agregar el método directamente.

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
fuente
0

Puede usar un yeso en nulo para que funcione.

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

La extensión:

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

Tu tipo:

public class YourType { }
Wouter
fuente
-4

PUEDE hacer esto si está dispuesto a "frigirlo" un poco haciendo una variable de la clase estática y asignándola a nulo. Sin embargo, este método no estaría disponible para llamadas estáticas en la clase, por lo que no estoy seguro de cuánto uso tendría:

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
fuente
Esto es exactamente lo que hice. Mi clase se llama MyTrace :)
Gishu
Consejo útil un poco de olor a código, pero supongo que podríamos ocultar el objeto nulo en una clase base o algo así. Gracias.
Tom Deloford
1
No puedo compilar este código. Error 'System.Console': los tipos estáticos no se pueden usar como parámetros
kuncevic.dev
Sí, esto no se puede hacer. ¡Maldición, pensé que estabas en algo allí! Los tipos estáticos no se pueden pasar como parámetros a métodos, lo que tiene sentido, supongo. Esperemos que la EM vea la madera de los árboles en esta y la cambie.
Tom Deloford
3
¡Debería haber intentado compilar mi propio código! Como dice Tom, esto no funcionará con clases estáticas.
Tenaka