Чи можна додати методи розширення до існуючого статичного класу?


532

Я прихильник методів розширення в C #, але не мав жодного успіху, додавши метод розширення до статичного класу, наприклад консолі.

Наприклад, якщо я хочу додати розширення до консолі під назвою "WriteBlueLine", щоб я міг перейти:

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

Я спробував це, додавши локальний, загальнодоступний статичний метод із консоллю як "цей" параметр ... але без кісток!

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

Це не додало метод "WriteBlueLine" до консолі ... чи я це роблю неправильно? Або просити неможливе?


3
Ну добре. прикро, але я думаю, я пройду. Я ВИНАХОДЖУЙТЕ метод розширення незайманий (у виробничому коді все одно). Можливо, одного дня, якщо мені пощастить.
Енді Макклугдж

Я написав ряд розширень HtmlHelper для ASP.NET MVC. Написав один для DateTime, щоб дати мені кінець зазначеної дати (23: 59.59). Корисно, коли ви попросите користувача вказати дату закінчення, але дуже хочете, щоб це було кінцем цього дня.
tvanfosson

12
Зараз немає можливості їх додати, оскільки функція не існує в C #. Чи не тому , що це неможливо самі по собі , а тому , що C # визирає дуже зайняті, в основному зацікавлені в методах розширення , щоб зробити роботу LINQ і не бачив досить переваг в статичних методах розширення , щоб виправдати час вони будуть приймати для реалізації. Ерік Ліпперт пояснює тут .
Джордан Грей

1
Просто зателефонуйте Helpers.WriteBlueLine(null, "Hi");:)
Hüseyin Yağlı

Відповіді:


285

Ні. Методи розширення вимагають змінної екземпляра (значення) для об'єкта. Однак ви можете написати статичну обгортку навколо ConfigurationManagerінтерфейсу. Якщо ви реалізуєте обгортку, вам не потрібен метод розширення, оскільки ви можете просто додати метод безпосередньо.

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

      .....

      public static ConfigurationSection GetWidgetSection()
      {
          return GetSection( "widgets" );
      }
 }

8
@Luis - в контексті ідеї було б "чи можна додати метод розширення до класу ConfigurationManager, щоб отримати певний розділ?" Ви не можете додати метод розширення до статичного класу, оскільки для нього потрібен екземпляр об'єкта, але ви можете написати клас обгортки (або фасад), який реалізує ту саму підпис і віддає фактичний виклик справжньому ConfigurationManager. Ви можете додати будь-який бажаний метод до класу обгортки, щоб він не був розширенням.
tvanfosson

Мені здається кориснішим просто додати статичний метод до класу, що реалізує ConfigurationSection. Отже, з огляду на реалізацію під назвою MyConfigurationSection, я би назвав MyConfigurationSection.GetSection (), який повертає розділ, який уже був набраний, або null, якщо його не існує. Кінцевий результат такий же, але він уникає додавання класу.
торкніться

1
@tap - це лише приклад, і перший, який прийшов у голову. Однак принцип єдиної відповідальності вступає в силу. Чи повинен "контейнер" насправді відповідати за інтерпретацію файлу конфігурації? Зазвичай я просто маю ConfigurationSectionHandler і передаю висновок з ConfigurationManager у відповідний клас і не морочуюся обгорткою.
tvanfosson

5
Для внутрішнього використання я почав створювати "X" варіанти статичних класів та структур для додавання спеціальних розширень: "ConsoleX" містить нові статичні методи для "Console", "MathX" містить нові статичні методи для "Math", "ColorX" розширює методи 'Color' і т. д. Не зовсім однакові, але легко запам’ятовувати та виявляти в IntelliSense.
користувач1689175

1
@Xtro Я погоджуюся, що це жахливо, але не гірше, ніж не мати можливості використовувати тестовий подвійний на своєму місці або, що ще гірше, відмовитися від тестування свого коду, оскільки статичні класи це ускладнюють. Microsoft, здається, погоджується зі мною, тому що це причина, що вони запровадили класи HttpContextWrapper / HttpContextBase, щоб обійти статичний HttpContext.Current для MVC.
tvanfosson

91

Чи можете ви додати статичні розширення до класів у C #? Ні, але ви можете це зробити:

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

Ось як це працює. Хоча технічно не можна писати статичні методи розширення, натомість цей код використовує лазівку в методах розширення. Ця лазівка ​​полягає в тому, що ви можете викликати методи розширення на нульових об'єктах, не отримуючи нульового винятку (якщо ви нічого не отримуєте через @this).

Отже, ось як би ви цим скористалися:

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

Тепер ЧОМУ я вибрав за приклад конструктор за замовчуванням, і чому я просто не повертаю новий T () у перший фрагмент коду, не виконуючи все це сміття Expression? Ну сьогодні ваш щасливий день, тому що ви отримуєте 2fer. Як знає будь-який просунутий розробник .NET, новий T () повільний, оскільки він генерує виклик до System.Activator, який використовує відображення, щоб отримати конструктор за замовчуванням, перш ніж викликати його. Чорт за тобою, Microsoft! Однак мій код безпосередньо викликає конструктор за замовчуванням об'єкта.

Статичне розширення було б краще, ніж це, але відчайдушні часи вимагають відчайдушних заходів.


2
Я думаю, що для Dataset цей трюк спрацює, але я сумніваюся, що він працює для класу Console, оскільки Console - це статичний клас, статичні типи не можна використовувати як аргументи :)
ThomasBecker,

Так, я збирався сказати те саме. Це методи псевдостатичного розширення для нестатичного класу. OP був методом розширення для статичного класу.
Марк А. Донохое

2
Це набагато краще і легше всього мати деякі угоди про іменах для таких методів , як XConsole, ConsoleHelperі так далі.
Олексій Жуковський

9
Це захоплююча хитрість, але результат смердючий. Ви створюєте нульовий об'єкт, потім, як видається, викликаєте метод на ньому - незважаючи на те, що багато років говорили, що "виклик методу на нульовому об'єкті викликає виняток". Це працює, але ..фу ... Конфуз для когось, хто підтримує пізніше. Я не підкажу, оскільки ви додали до пулу інформації про те, що можливо. Але я щиро сподіваюся, що ніхто ніколи не використовує цю техніку !! Додаткова скарга: не передайте один із них методу, і розраховуйте отримати підкласику OO: викликаний метод буде типом оголошення параметра, а не переданим типом параметра .
ToolmakerSteve

5
Це хитро, але мені це подобається. Однією з альтернатив (null as DataSet).Create();може бути default(DataSet).Create();.
Багерфарер

54

Це неможливо.

І так, я думаю, що MS тут помилилася.

Їх рішення не має сенсу і змушує програмістів написати (як описано вище) безглуздий клас обгортки.

Ось хороший приклад: Спроба розширити статичний клас тестування MS Unit Assert: Я хочу ще 1 метод Assert AreEqual(x1,x2) .

Єдиний спосіб зробити це - вказати на різні класи або написати обгортку приблизно 100-х різних методів Assert. Чому !?

Якщо було прийнято рішення дозволити розширення примірників, я не бачу логічної причини не дозволяти статичних розширень. Аргументи щодо розділення бібліотек не стоять, коли екземпляри можуть бути розширені.


20
Я також намагався розширити тестовий клас MS Unit Assert, щоб додати Assert.Throws та Assert.DoesNotThrow, і зіткнувся з тією ж проблемою.
Стефано Річчарді

3
Так я теж :( я думав , що я можу зробити Assert.Throwsу відповідь stackoverflow.com/questions/113395 / ...
CallMeLaNN

27

Я натрапив на цю нитку, намагаючись знайти відповідь на те саме питання, що було і в ОП. Я не знайшов відповіді, яку хотів, але закінчив це робити.

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

І я використовую це так:

ConsoleColor.Cyan.WriteLine("voilà");

19

Можливо, ви можете додати статичний клас зі своїм власним простором імен та тим самим іменем класу:

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

1
Але це не вирішує проблему необхідності повторного впровадження кожного методу з початкового статичного класу, який ви хочете зберегти у своїй обгортці. Це все ще обгортка, хоча вона має перевагу потребувати менших змін у коді, який її використовує…
binki


11

Ні. Визначення методу розширення вимагає екземпляра типу, який ви розширюєте. Це прикро; Я не впевнений, чому це потрібно ...


4
Це тому, що метод розширення використовується для розширення примірника об'єкта. Якщо вони цього не зробили, вони були б просто звичайними статичними методами.
Дерек Екінс

31
Було б добре зробити і те, і чи не так?

7

Що стосується методів розширення, то самі методи розширення є статичними; але вони викликаються так, ніби вони є методами екземпляра. Оскільки статичний клас не є миттєвим, у вас ніколи не буде екземпляра класу, з якого слід викликати метод розширення. З цієї причини компілятор не дозволяє визначати методи розширення для статичних класів.

Пан Obnoxious писав: "Як знає будь-який просунутий розробник .NET, новий T () повільний, оскільки він генерує виклик до System.Activator, який використовує відображення для отримання конструктора за замовчуванням, перш ніж викликати його".

New () компілюється в інструкцію IL "newobj", якщо тип відомий під час компіляції. Newobj приймає конструктор для прямого виклику. Виклики до System.Activator.CreateInstance () компілюють в інструкцію IL "call" для виклику System.Activator.CreateInstance (). Новий () при використанні проти загальних типів призведе до виклику System.Activator.CreateInstance (). Пост містера Обидних з цього приводу був незрозумілим ... і ну, неприємним.

Цей код:

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

виробляє цю ІЛ:

  .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

5

Ви не можете додати статичні методи до типу. Ви можете додавати (псевдо-) екземплярні методи до примірника типу.

Сенс thisмодифікатора полягає в тому, щоб сказати компілятору C # передати екземпляр ліворуч від. як перший параметр методу статичного / розширення.

У разі додавання статичних методів до типу, для першого параметра немає жодного примірника.


4

Я намагався це зробити з System.Environment ще тоді, коли вивчав методи розширення і не мав успіху. Причина, як зазначають інші, тому, що методи розширення вимагають екземпляра класу.


3

Неможливо написати метод розширення, проте можна імітувати поведінку, про яку ви просите.

using FooConsole = System.Console;

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

Це дозволить вам викликати Console.WriteBlueLine (fooText) в інших класах. Якщо інші класи хочуть отримати доступ до інших статичних функцій консолі, на них доведеться чітко посилатися через простір імен.

Ви завжди можете додати всі методи до класу заміни, якщо хочете мати їх у одному місці.

Тож у вас вийшло щось подібне

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

Це забезпечить поведінку, яку ви шукаєте.

* Примітка консолі потрібно буде додати через простір імен, в який ви вводите її.


1

так, у обмеженому розумінні.

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

Це працює, але консоль не робить, тому що вона статична.

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

Це працює тому, що поки він не знаходиться в одному просторі імен. Проблема полягає в тому, що ви повинні написати статичний метод проксі для кожного методу, який має System.Console. Це не обов'язково погано, оскільки ви можете додати щось подібне:

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

або

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

Як це працює, це те, що ви підключите щось до стандартного WriteLine. Це може бути кількість рядків або фільтр поганих слів чи будь-що інше. Кожен раз, коли ви просто вкажете консоль у вашому просторі імен, скажімо, WebProject1 та імпортуйте систему простору імен, WebProject1.Console буде обраний через System.Console як стандартний для цих класів у просторі імен WebProject1. Таким чином, цей код перетворить усі виклики Console.WriteLine в синій, наскільки ви ніколи не вказували System.Console.WriteLine.


на жаль, підхід до використання нащадка не спрацьовує, коли базовий клас запечатаний (як багато хто в бібліотеці класів .NET)
Джордж Бірбіліс,

1

Далі було відхилено як редагування відповіді tvanfosson. Мене попросили внести це як власну відповідь. Я використав його пропозицію і закінчив реалізацію ConfigurationManagerобгортки. У принципі я просто заповнив відповідь ...в tvanfosson.

Ні. Методи розширення вимагають примірника об'єкта. Однак ви можете написати статичну обгортку навколо інтерфейсу ConfigurationManager. Якщо ви реалізуєте обгортку, вам не потрібен метод розширення, оскільки ви можете просто додати метод безпосередньо.

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

0

Ви можете використовувати ролик на null, щоб він працював.

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

Розширення:

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

Ваш тип:

public class YourType { }

-4

Ви МОЖЕТЕ це зробити, якщо ви хочете трохи «фрігнути» його, зробивши змінну статичного класу та призначивши її нульовою. Однак цей метод не буде доступний для статичних дзвінків у класі, тому не впевнений, якою користю він користуватиметься:

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

саме це я і зробив. Мій клас називається MyTrace :)
Gishu

Корисна порада. трохи запаху коду, але я думаю, що ми могли б приховати нульовий об’єкт в базовий клас чи щось таке. Дякую.
Том Делофорд

1
Я не можу скласти цей код. Помилка 'System.Console': статичні типи не можна використовувати як параметри
kuncevic.dev

Так, цього неможливо зробити. Чорт, я думав, що ти на щось там! Статичні типи не можна передавати як параметри методам, що, мабуть, має сенс. Будемо просто сподіватися, що МС побачить деревину з дерев на цьому і змінить її.
Том Делофорд,

3
Я повинен був спробувати скласти власний код! Як каже Том, це не буде працювати зі статичними класами.
Тенака
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.