Posso adicionar métodos de extensão a uma classe estática existente?

535

Sou fã de métodos de extensão em C #, mas não tive sucesso ao adicionar um método de extensão a uma classe estática, como o Console.

Por exemplo, se eu quiser adicionar uma extensão ao console, chamada 'WriteBlueLine', para que eu possa:

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

Eu tentei isso adicionando um método estático público local, com o Console como um parâmetro 'this' ... mas não há dados!

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

Isso não adicionou um método 'WriteBlueLine' ao Console ... estou fazendo errado? Ou pedindo o impossível?

Leon Bambrick
fonte
3
Ah bem. infeliz, mas acho que vou sobreviver. Eu ainda sou um método de extensão virgem (no código de produção de qualquer maneira). Talvez um dia, se eu tiver sorte.
Andy McCluggage
Eu escrevi várias extensões HtmlHelper para o ASP.NET MVC. Escrevi um para o DateTime para me dar o fim da data especificada (23: 59.59). Útil quando você solicita ao usuário que especifique uma data de término, mas realmente deseja que seja o fim desse dia.
Tbf #
12
Atualmente, não há como adicioná-los porque o recurso não existe em C #. Não porque é impossível por si só , mas porque as espreitadelas do C # estão muito ocupadas, estavam interessadas principalmente em métodos de extensão para fazer o LINQ funcionar e não viam benefícios suficientes nos métodos de extensão estática para justificar o tempo que levariam para implementar. Eric Lippert explica aqui .
Jordan Gray
1
Just call Helpers.WriteBlueLine(null, "Hi");:)
Hüseyin Yağlı

Respostas:

286

Não. Os métodos de extensão requerem uma variável de instância (valor) para um objeto. No entanto, você pode escrever um invólucro estático em torno da ConfigurationManagerinterface. Se você implementar o wrapper, não precisará de um método de extensão, pois você pode simplesmente adicionar o método diretamente.

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

      .....

      public static ConfigurationSection GetWidgetSection()
      {
          return GetSection( "widgets" );
      }
 }
tvanfosson
fonte
8
@Luis - no contexto, a idéia seria "eu poderia adicionar um método de extensão à classe ConfigurationManager para obter uma seção específica?" Você não pode adicionar um método de extensão a uma classe estática, pois requer uma instância do objeto, mas pode escrever uma classe (ou fachada) de wrapper que implemente a mesma assinatura e adie a chamada real para o ConfigurationManager real. Você pode adicionar qualquer método que desejar à classe wrapper, para que não precise ser uma extensão.
tvanfosson
Acho mais útil adicionar apenas um método estático à classe que implementa o ConfigurationSection. Portanto, dada uma implementação chamada MyConfigurationSection, eu chamaria MyConfigurationSection.GetSection (), que retorna a seção já digitada ou null, caso não exista. O resultado final é o mesmo, mas evita adicionar uma classe.
toque
1
@tap - é apenas um exemplo, e o primeiro que veio à mente. O princípio da responsabilidade única entra em jogo, no entanto. O "contêiner" deve ser realmente responsável pela interpretação do arquivo de configuração? Normalmente, eu simplesmente tenho o ConfigurationSectionHandler e transmito a saída do ConfigurationManager para a classe apropriada e não me preocupo com o wrapper.
tvanfosson
5
Para uso interno, comecei a criar variantes 'X' de classes e estruturas estáticas para adicionar extensões personalizadas: 'ConsoleX' contém novos métodos estáticos para 'Console', 'MathX' contém novos métodos estáticos para 'Math', 'ColorX' estende os métodos 'Cor', etc. Não é o mesmo, mas é fácil de lembrar e descobrir no IntelliSense.
user1689175
1
@ Xtro Concordo que é horrível, mas não é pior do que não poder usar um teste duplo em seu lugar ou, pior, desistir de testar seu código porque as classes estáticas o tornam tão difícil. A Microsoft parece concordar comigo porque é a razão pela qual eles introduziram as classes HttpContextWrapper / HttpContextBase para contornar o estático HttpContext.Current for MVC.
tvanfosson
92

Você pode adicionar extensões estáticas às classes em c #? Não, mas você pode fazer isso:

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

Aqui está como isso funciona. Embora você não possa tecnicamente escrever métodos de extensão estáticos, esse código explora uma brecha nos métodos de extensão. Essa brecha é que você pode chamar métodos de extensão em objetos nulos sem obter a exceção nula (a menos que você acesse algo via @this).

Então, aqui está como você usaria isso:

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

Agora, por que escolhi chamar o construtor padrão como exemplo e AND por que não retornei o novo T () no primeiro trecho de código sem fazer todo esse lixo de expressão? Bem, hoje é seu dia de sorte, porque você recebe um 2fer. Como qualquer desenvolvedor .NET avançado sabe, o novo T () é lento porque gera uma chamada para System.Activator que usa reflexão para obter o construtor padrão antes de chamá-lo. Maldito seja, Microsoft! No entanto, meu código chama o construtor padrão do objeto diretamente.

Extensões estáticas seriam melhores do que isso, mas tempos desesperados exigem medidas desesperadas.

Mr. Obnoxious
fonte
2
Eu acho que para Dataset este truque irá funcionar, mas eu duvido que ele funciona para a classe Console como Console é classe estática, tipos estáticos não podem ser usados como argumentos :)
ThomasBecker
Sim, eu ia dizer a mesma coisa. Este é um método de extensão pseudo-estática em uma classe não estática. O OP era um método de extensão em uma classe estática.
Mark A. Donohoe
2
É muito melhor e mais fácil simplesmente ter alguma convenção de nomenclatura para esses métodos como XConsole, ConsoleHelpere assim por diante.
Alex Zhukovskiy
9
Este é um truque fascinante, mas o resultado é fedorento. Você cria um objeto nulo e, em seguida, parece chamar um método - apesar de anos depois de saber que "chamar um método em um objeto nulo causa uma exceção". Funciona, mas ... uau ... Confuso para quem mantém mais tarde. Não vou votar, porque você adicionou ao pool de informações o que é possível. Mas sinceramente espero que ninguém nunca use essa técnica !! Reclamação adicional: não transmita uma delas a um método e espere obter subclasse de OO: o método chamado será o tipo de declaração de parâmetro e não o tipo de parâmetro passado .
Página
5
Isso é complicado, mas eu gosto. Uma alternativa para (null as DataSet).Create();poderia ser default(DataSet).Create();.
Bagerfahrer 27/04/19
54

Não é possível.

E sim, acho que a MS cometeu um erro aqui.

Sua decisão não faz sentido e força os programadores a escrever (como descrito acima) uma classe de invólucro sem sentido.

Aqui está um bom exemplo: Tentando estender a classe estática de teste de Unidade MS Assert: Quero mais 1 método Assert AreEqual(x1,x2).

A única maneira de fazer isso é apontar para diferentes classes ou escrever um wrapper em torno de centenas de diferentes métodos Assert. Por quê!?

Se a decisão foi tomada para permitir extensões de instâncias, não vejo razão lógica para não permitir extensões estáticas. Os argumentos sobre o seccionamento de bibliotecas não se sustentam quando as instâncias podem ser estendidas.

Tom Deloford
fonte
20
Eu também estava tentando estender a classe MS Unit Test Assert para adicionar Assert.Throws e Assert.DoesNotThrow e enfrentou o mesmo problema.
Stefano Ricciardi
3
Sim, eu também :( Eu pensei que posso fazer Assert.Throwscom a resposta stackoverflow.com/questions/113395/…
CallMeLaNN
27

Eu me deparei com esse tópico enquanto tentava encontrar uma resposta para a mesma pergunta que o OP tinha. Não encontrei a resposta que queria, mas acabei fazendo isso.

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

E eu uso assim:

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

Talvez você possa adicionar uma classe estática com seu namespace personalizado e o mesmo nome 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
fonte
1
Mas isso não resolve o problema de precisar reimplementar todos os métodos da classe estática original que você deseja manter em seu wrapper. É ainda um invólucro, embora não têm o mérito de precisar de menos mudanças no código que usa-lo ...
binki
11

Não. As definições de método de extensão exigem uma instância do tipo que você está estendendo. É lamentável; Não sei por que é necessário ...


fonte
4
Isso ocorre porque um método de extensão é usado para estender uma instância de um objeto. Se não fizessem isso, seriam apenas métodos estáticos regulares.
Derek Ekins
31
Seria bom fazer as duas coisas, não seria?
7

Quanto aos métodos de extensão, os próprios métodos de extensão são estáticos; mas eles são chamados como se fossem métodos de instância. Como uma classe estática não é instanciada, você nunca teria uma instância da classe para invocar um método de extensão. Por esse motivo, o compilador não permite que métodos de extensão sejam definidos para classes estáticas.

Obnoxious escreveu: "Como qualquer desenvolvedor .NET avançado sabe, o novo T () é lento porque gera uma chamada para System.Activator que usa reflexão para obter o construtor padrão antes de chamá-lo".

New () é compilado na instrução IL "newobj" se o tipo for conhecido no momento da compilação. Newobj usa um construtor para invocação direta. Chamadas para System.Activator.CreateInstance () compilam a instrução "call" de IL para chamar System.Activator.CreateInstance (). New () quando usado em tipos genéricos resultará em uma chamada para System.Activator.CreateInstance (). O post do Sr. Obnóxio não era claro sobre este ponto ... e bem, detestável.

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

produz este 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
fonte
5

Você não pode adicionar estática métodos a um tipo. Você só pode adicionar métodos (pseudo-) de instância a uma instância de um tipo.

O objetivo do thismodificador é dizer ao compilador C # para passar a instância no lado esquerdo do .como o primeiro parâmetro do método static / extension.

No caso de adicionar métodos estáticos a um tipo, não há instância a ser transmitida para o primeiro parâmetro.

Brannon
fonte
4

Tentei fazer isso com o System.Environment quando estava aprendendo métodos de extensão e não obtive sucesso. O motivo é, como outros mencionam, porque os métodos de extensão requerem uma instância da classe.

Robert S.
fonte
3

Não é possível escrever um método de extensão, no entanto, é possível imitar o comportamento que você 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();
    }
}

Isso permitirá que você chame Console.WriteBlueLine (fooText) em outras classes. Se as outras classes quiserem acessar as outras funções estáticas do Console, elas deverão ser explicitamente referenciadas através de seu espaço para nome.

Você sempre pode adicionar todos os métodos à classe de substituição se desejar tê-los em um só lugar.

Então você teria 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.
}

Isso forneceria o tipo de comportamento que você está procurando.

* Nota O console precisará ser adicionado através do espaço para nome em que você o colocou.

Douglas Potesta
fonte
1

sim, em um sentido limitado.

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

Isso funciona, mas o console não, porque é estático.

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

Isso funciona porque contanto que não esteja no mesmo espaço para nome. O problema é que você precisa escrever um método estático de proxy para cada método que o System.Console possui. Não é necessariamente uma coisa ruim, pois você pode adicionar algo como isto:

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

O modo como funciona é que você conecta algo ao WriteLine padrão. Pode ser uma contagem de linhas ou um filtro de palavras incorretas ou qualquer outra coisa. Sempre que você especificar o Console no seu espaço para nome, diga WebProject1 e importe o espaço para nome System, WebProject1.Console será escolhido sobre System.Console como padrão para as classes no espaço para nome WebProject1. Portanto, esse código transformará todas as chamadas Console.WriteLine em azul, na medida em que você nunca especificou System.Console.WriteLine.

Cachorro preto
fonte
infelizmente, a abordagem de usar uma obra descendente doen't quando a classe base é selada (como muitos na biblioteca de classes .NET)
George Birbilis
1

O seguinte foi rejeitado como uma edição da resposta de tvanfosson. Fui convidado a contribuir como minha própria resposta. Usei sua sugestão e finalizei a implementação de um ConfigurationManagerwrapper. Em princípio, simplesmente preenchi a ...resposta de tvanfosson.

Não. Os métodos de extensão requerem uma instância de um objeto. No entanto, você pode escrever um invólucro estático na interface ConfigurationManager. Se você implementar o wrapper, não precisará de um método de extensão, pois você pode adicionar o método diretamente.

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

Você pode usar uma conversão em null para fazê-lo funcionar.

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

A extensão:

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

Seu tipo:

public class YourType { }
Wouter
fonte
-4

Você pode fazer isso se estiver disposto a "frig" um pouco, criando uma variável da classe estática e atribuindo-a a null. No entanto, esse método não estaria disponível para chamadas estáticas na classe, portanto, não tenho certeza de quanto uso seria:

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
fonte
isto é exatamente o que eu fiz. Minha classe é chamado MYTrace :)
Gishu
Dica útil. um pouco de cheiro de código, mas acho que poderíamos esconder o objeto nulo em uma classe base ou algo assim. Obrigado.
Tom Deloford
1
Não consigo compilar esse código. Erro 'System.Console': tipos estáticos não podem ser usados ​​como parâmetros
kuncevic.dev
Sim, isso não pode ser feito. Porra, eu pensei que você estava em algo lá! Tipos estáticos não podem ser passados ​​como parâmetros em métodos que fazem sentido, suponho. Vamos apenas torcer para que o MS veja a madeira das árvores e a troque.
Tom Deloford
3
Eu deveria ter tentado compilar meu próprio código! Como Tom diz, isso não funcionará com classes estáticas.
Tenaka