Czy mogę dodać metody rozszerzeń do istniejącej klasy statycznej?

532

Jestem fanem metod rozszerzenia w języku C #, ale nie udało mi się dodać metody rozszerzenia do klasy statycznej, takiej jak konsola.

Na przykład, jeśli chcę dodać rozszerzenie do konsoli o nazwie „WriteBlueLine”, aby móc przejść:

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

Próbowałem tego, dodając lokalną, publiczną metodę statyczną, z konsolą jako parametrem „to” ... ale żadnych kości!

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

Nie dodało to metody „WriteBlueLine” do konsoli ... czy robię to źle? Czy prosić o niemożliwe?

Leon Bambrick
źródło
3
No cóż. niefortunne, ale myślę, że sobie poradzę. Nadal jestem metodą dziewiczą metodą rozszerzenia (zresztą w kodzie produkcyjnym). Może któregoś dnia, jeśli będę miał szczęście.
Andy McCluggage
Napisałem wiele rozszerzeń HtmlHelper dla ASP.NET MVC. Napisałem jeden dla DateTime, aby podać mi koniec podanej daty (23: 59.59). Przydatne, gdy poprosisz użytkownika o określenie daty zakończenia, ale naprawdę chcesz, aby był to koniec tego dnia.
tvanfosson
12
Obecnie nie ma możliwości ich dodania, ponieważ funkcja nie istnieje w języku C #. Nie dlatego, że samo w sobie jest to niemożliwe , ale ponieważ peepy C # są bardzo zajęte, byli głównie zainteresowani metodami rozszerzenia, aby LINQ działał i nie dostrzegli wystarczających korzyści w statycznych metodach rozszerzenia, aby uzasadnić czas potrzebny na wdrożenie. Eric Lippert wyjaśnia tutaj .
Jordan Gray
1
Wystarczy zadzwonić Helpers.WriteBlueLine(null, "Hi");:)
Hüseyin Yağlı

Odpowiedzi:

285

Nie. Metody rozszerzenia wymagają zmiennej obiektu (wartości) dla obiektu. Możesz jednak napisać statyczne opakowanie wokół ConfigurationManagerinterfejsu. Jeśli zaimplementujesz opakowanie, nie potrzebujesz metody rozszerzenia, ponieważ możesz po prostu dodać metodę bezpośrednio.

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

      .....

      public static ConfigurationSection GetWidgetSection()
      {
          return GetSection( "widgets" );
      }
 }
tvanfosson
źródło
8
@Luis - w kontekście można by pomyśleć „czy mogę dodać metodę rozszerzenia do klasy ConfigurationManager, aby uzyskać konkretną sekcję?” Nie można dodać metody rozszerzenia do klasy statycznej, ponieważ wymaga ona wystąpienia obiektu, ale można napisać klasę opakowania (lub fasadę), która implementuje tę samą sygnaturę i odkłada rzeczywiste wywołanie do rzeczywistego menedżera konfiguracji ConfigurationManager. Możesz dodać dowolną metodę do klasy opakowania, więc nie musi to być rozszerzenie.
tvanfosson
Bardziej pomocne jest po prostu dodanie metody statycznej do klasy implementującej ConfigurationSection. Biorąc pod uwagę implementację o nazwie MyConfigurationSection, wywołałbym MyConfigurationSection.GetSection (), która zwraca wpisaną już sekcję, lub null, jeśli nie istnieje. Wynik końcowy jest taki sam, ale unika dodawania klasy.
dotknij
1
@tap - to tylko przykład i pierwszy, który przyszedł mi do głowy. W grę wchodzi jednak zasada pojedynczej odpowiedzialności. Czy „kontener” powinien być rzeczywiście odpowiedzialny za interpretację samego pliku konfiguracyjnego? Zwykle po prostu mam ConfigurationSectionHandler i przesyłam dane wyjściowe z ConfigurationManager do odpowiedniej klasy i nie zawracam sobie głowy opakowaniem.
tvanfosson
5
Do użytku wewnętrznego zacząłem tworzyć warianty „X” klas statycznych i struktur do dodawania własnych rozszerzeń: „ConsoleX” zawiera nowe metody statyczne dla „Console”, „MathX” zawiera nowe metody statyczne dla „Math”, „ColorX” rozszerza metody „Kolor” itp. Niezupełnie takie same, ale łatwe do zapamiętania i odkrycia w IntelliSense.
user1689175,
1
@Xtro Zgadzam się, że to okropne, ale nie gorsze niż to, że nie można użyć podwójnego testu na swoim miejscu lub, co gorsza, poddać się testowaniu kodu, ponieważ klasy statyczne sprawiają, że jest to trudne. Wydaje się, że Microsoft się ze mną zgadza, ponieważ to dlatego wprowadzili klasy HttpContextWrapper / HttpContextBase, aby obejść statyczne HttpContext.Current dla MVC.
tvanfosson
91

Czy możesz dodać statyczne rozszerzenia do klas w języku C #? Nie, ale możesz to zrobić:

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

Oto jak to działa. Chociaż technicznie nie można pisać statycznych metod rozszerzenia, ten kod wykorzystuje lukę w metodach rozszerzenia. Ta luka polega na tym, że możesz wywoływać metody rozszerzenia na obiektach zerowych bez uzyskania wyjątku zerowego (chyba że uzyskasz dostęp do czegokolwiek za pośrednictwem @ tego).

Oto, jak byś tego użył:

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

DLACZEGO dlaczego wybrałem jako przykład wywoływanie domyślnego konstruktora i ORAZ dlaczego po prostu nie zwracam nowej T () w pierwszym fragmencie kodu bez robienia wszystkich śmieci z wyrażeń? Cóż, dziś masz szczęśliwy dzień, bo dostajesz 2fer. Jak wie każdy zaawansowany programista .NET, nowa funkcja T () działa powoli, ponieważ generuje wywołanie do System.Activator, który używa odbicia, aby uzyskać domyślny konstruktor przed wywołaniem go. Cholera, Microsoft! Jednak mój kod wywołuje bezpośrednio domyślny konstruktor obiektu.

Rozszerzenia statyczne byłyby lepsze od tego, ale desperackie czasy wymagają desperackich środków.

Pan Wstrętny
źródło
2
Myślę, że dla zestawu danych ta sztuczka będzie działać, ale wątpię, aby działała w klasie Console, ponieważ konsola jest klasą statyczną, typów statycznych nie można używać jako argumentów :)
ThomasBecker
Tak, zamierzałem powiedzieć to samo. Jest to pseudostatyczne metody rozszerzania klasy niestatycznej. OP był metodą rozszerzenia klasy statycznej.
Mark A. Donohoe
2
Jest o wiele lepiej i łatwiej po prostu mieć trochę konwencję nazewnictwa dla takich metod, takich jak XConsole, ConsoleHelperi tak dalej.
Alex Zhukovskiy,
9
To fascynująca sztuczka, ale rezultat śmierdzi. Tworzysz obiekt zerowy, a następnie wydajesz się wywoływać na nim metodę - pomimo lat, gdy mówiono, że „wywołanie metody na obiekcie zerowym powoduje wyjątek”. Działa, ale ... śmiech ... Mylące dla każdego, kto utrzyma później. Nie będę głosować za odrzuceniem, ponieważ dodałeś do puli informacji, co jest możliwe. Ale szczerze mam nadzieję, że nikt nigdy nie użyje tej techniki !! Dodatkowa skarga: nie przekazuj żadnej z nich do metody i spodziewaj się otrzymania podklasy OO: wywoływana metoda będzie rodzajem deklaracji parametru, a nie typem przekazywanego parametru .
ToolmakerSteve
5
To trudne, ale mi się podoba. Jedną z alternatyw (null as DataSet).Create();może być default(DataSet).Create();.
Bagerfahrer
54

To nie jest możliwe.

I tak, myślę, że stwardnienie rozsiane popełniło tutaj błąd.

Ich decyzja nie ma sensu i zmusza programistów do napisania (jak opisano powyżej) bezsensownej klasy opakowania.

Oto dobry przykład: Próba rozszerzenia statycznej klasy testowania MS Unit Assert: Chcę jeszcze 1 metodę Assert AreEqual(x1,x2).

Jedynym sposobem na to jest wskazanie różnych klas lub napisanie opakowania wokół setek różnych metod Assert. Dlaczego!?

Jeśli podjęto decyzję o zezwoleniu na rozszerzenia instancji, nie widzę logicznego powodu, aby nie zezwalać na rozszerzenia statyczne. Argumenty o bibliotekach sekcyjnych nie stoją po rozszerzeniu instancji.

Tom Deloford
źródło
20
Próbowałem również rozszerzyć klasę MS Unit Test Assert, aby dodać Assert.Throws i Assert.DoesNotThrow i napotkałem ten sam problem.
Stefano Ricciardi
3
Tak, ja też :( Myślałem, że dam radę Assert.Throwsodpowiedzieć stackoverflow.com/questions/113395/…
CallMeLaNN
27

Natknąłem się na ten wątek, próbując znaleźć odpowiedź na to samo pytanie, jakie miał OP. Nie znalazłem odpowiedzi, której chciałem, ale ostatecznie to zrobiłem.

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

I używam tego w ten sposób:

ConsoleColor.Cyan.WriteLine("voilà");
Adel G.Eibesh
źródło
19

Być może możesz dodać klasę statyczną z niestandardową przestrzenią nazw i tą samą nazwą klasy:

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
źródło
1
Ale to nie rozwiązuje problemu konieczności ponownego wdrożenia każdej metody z oryginalnej klasy statycznej, którą chcesz zachować w opakowaniu. Nadal jest to opakowanie, ale ma tę zaletę, że potrzebuje mniej zmian w kodzie, który go używa…
binki
11

Nie. Definicje metod rozszerzenia wymagają wystąpienia rozszerzanego typu. To niefortunne; Nie jestem pewien, dlaczego jest to wymagane ...


źródło
4
Jest tak, ponieważ do rozszerzenia instancji obiektu używana jest metoda rozszerzenia. Gdyby tego nie zrobili, byliby to zwykłe metody statyczne.
Derek Ekins
31
Byłoby miło zrobić obie rzeczy, prawda?
7

Jeśli chodzi o metody rozszerzenia, same metody rozszerzenia są statyczne; ale są wywoływane tak, jakby były metodami instancji. Ponieważ klasa statyczna nie jest możliwa do utworzenia, nigdy nie będzie instancji klasy, z której można by wywołać metodę rozszerzenia. Z tego powodu kompilator nie zezwala na definiowanie metod rozszerzeń dla klas statycznych.

Pan Obnoxious napisał: „Jak wie każdy zaawansowany programista .NET, nowa T () działa wolno, ponieważ generuje wywołanie do System.Activator, który używa odbicia, aby uzyskać domyślny konstruktor przed wywołaniem go”.

New () jest kompilowany do instrukcji IL „newobj”, jeśli typ jest znany w czasie kompilacji. Newobj pobiera konstruktora do bezpośredniego wywoływania. Wywołania System.Activator.CreateInstance () kompilują się z instrukcją IL „call” w celu wywołania System.Activator.CreateInstance (). Funkcja New () zastosowana wobec typów ogólnych spowoduje wywołanie funkcji System.Activator.CreateInstance (). Wpis pana Obnoxiousa był niejasny w tej kwestii ... i cóż, wstrętny.

Ten kod:

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

produkuje tę 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
źródło
5

Nie możesz dodać statycznego metod do typu. Można dodawać tylko metody (pseudo) instancji do instancji typu.

Celem thismodyfikatora jest poinformowanie kompilatora C #, aby przekazał instancję po lewej stronie .jako pierwszy parametr metody static / extension.

W przypadku dodawania metod statycznych do typu nie ma instancji do przekazania dla pierwszego parametru.

Brannon
źródło
4

Próbowałem to zrobić za pomocą System.Environment, kiedy uczyłem się metod rozszerzania i nie powiodło się. Powodem jest, jak wspominają inni, ponieważ metody rozszerzenia wymagają wystąpienia klasy.

Robert S.
źródło
3

Nie można napisać metody rozszerzenia, jednak można naśladować zachowanie, o które prosisz.

using FooConsole = System.Console;

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

Umożliwi to wywołanie Console.WriteBlueLine (fooText) w innych klasach. Jeśli inne klasy chcą uzyskać dostęp do innych statycznych funkcji konsoli, będą musiały zostać jawnie przywołane poprzez ich przestrzeń nazw.

Zawsze możesz dodać wszystkie metody do klasy zastępczej, jeśli chcesz mieć je wszystkie w jednym miejscu.

Więc miałbyś coś takiego

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

Zapewniłoby to rodzaj zachowania, którego szukasz.

* Uwaga: Konsola będzie musiała zostać dodana poprzez przestrzeń nazw, w której została umieszczona.

Douglas Potesta
źródło
1

tak, w ograniczonym sensie.

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

To działa, ale konsola nie działa, ponieważ jest statyczna.

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

Działa to, o ile nie znajduje się w tej samej przestrzeni nazw. Problem polega na tym, że musisz napisać statyczną metodę proxy dla każdej metody, jaką posiada System.Console. To niekoniecznie zła rzecz, ponieważ możesz dodać coś takiego:

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

lub

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

Działa to tak, że podpinasz coś do standardowej linii WriteLine. Może to być liczba wierszy, filtr niepoprawnych słów lub cokolwiek innego. Ilekroć po prostu określisz konsolę w przestrzeni nazw, powiedz WebProject1 i zaimportuj przestrzeń nazw System, WebProject1.Console zostanie wybrana zamiast System.Console jako domyślna dla tych klas w przestrzeni nazw WebProject1. Tak więc ten kod zmieni wszystkie wywołania Console.WriteLine w niebieskie, o ile nigdy nie podano System.Console.WriteLine.

Czarny pies
źródło
niestety podejście polegające na użyciu potomka nie działa, gdy klasa podstawowa jest zapieczętowana (jak wiele w bibliotece klas .NET)
George Birbilis
1

Poniższe zostało odrzucone jako edycja odpowiedzi tvanfosson. Poproszono mnie o udzielenie odpowiedzi jako mojej własnej odpowiedzi. Skorzystałem z jego sugestii i zakończyłem implementację ConfigurationManageropakowania. Zasadniczo po prostu wypełniłem odpowiedź ...w tvanfosson.

Nie. Metody rozszerzania wymagają wystąpienia obiektu. Możesz jednak napisać statyczne opakowanie wokół interfejsu ConfigurationManager. Jeśli zaimplementujesz opakowanie, nie potrzebujesz metody rozszerzenia, ponieważ możesz po prostu dodać metodę bezpośrednio.

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
źródło
0

Możesz użyć rzutowania na wartość NULL, aby działało.

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

Rozszerzenie:

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

Twój typ:

public class YourType { }
Wouter
źródło
-4

Możesz to zrobić, jeśli zechcesz trochę „fregować”, tworząc zmienną klasy statycznej i przypisując jej wartość null. Jednak ta metoda nie byłaby dostępna dla wywołań statycznych w klasie, więc nie jestem pewien, ile by to wykorzystało:

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
źródło
właśnie to zrobiłem. Moja klasa nazywa się MyTrace :)
Gishu
Przydatna wskazówka. trochę zapach kodu, ale myślę, że moglibyśmy ukryć obiekt zerowy w klasie bazowej lub coś w tym rodzaju. Dzięki.
Tom Deloford,
1
Nie mogę skompilować tego kodu. Błąd „System.Console”: typy statyczne nie mogą być użyte jako parametry
kuncevic.dev
Tak, nie można tego zrobić. Cholera, myślałem, że coś tam masz! Typy statyczne nie mogą być przekazywane jako parametry do metod, co, jak sądzę, ma sens. Miejmy tylko nadzieję, że stwardnienie rozsiane zobaczy drewno z drzew na tym drzewie i je zmieni.
Tom Deloford
3
Powinienem był spróbować skompilować własny kod! Jak mówi Tom, nie będzie to działać z klasami statycznymi.
Tenaka,