Могу ли я добавить методы расширения в существующий статический класс?

535

Я фанат методов расширения в C #, но не смог успешно добавить метод расширения в статический класс, такой как Console.

Например, если я хочу добавить в консоль расширение, называемое «WriteBlueLine», чтобы я мог перейти:

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

Я попробовал это, добавив локальный, публичный статический метод, с консолью в качестве параметра 'this' ... но без кубиков!

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ı

Ответы:

286

Методы расширения требуют переменную экземпляра (значение) для объекта. Вы можете, однако, написать статическую оболочку вокруг ConfigurationManagerинтерфейса. Если вы реализуете оболочку, вам не нужен метод расширения, поскольку вы можете просто добавить метод напрямую.

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

      .....

      public static ConfigurationSection GetWidgetSection()
      {
          return GetSection( "widgets" );
      }
 }
tvanfosson
источник
8
@Luis - в контексте идея заключалась бы в том, «можно ли добавить метод расширения в класс ConfigurationManager, чтобы получить определенный раздел?» Вы не можете добавить метод расширения к статическому классу, поскольку для него требуется экземпляр объекта, но вы можете написать класс-оболочку (или фасад), который реализует ту же сигнатуру и откладывает фактический вызов реального ConfigurationManager. Вы можете добавить любой метод, который вы хотите, в класс-оболочку, чтобы он не был расширением.
tvanfosson
Я считаю более полезным просто добавить статический метод в класс, реализующий ConfigurationSection. Поэтому, учитывая реализацию MyConfigurationSection, я бы вызвал MyConfigurationSection.GetSection (), которая возвращает уже введенный раздел, или ноль, если он не существует. Конечный результат тот же, но он избегает добавления класса.
нажмите
1
@tap - это всего лишь пример и первый, который пришёл в голову. Однако в игру вступает принцип единой ответственности. Должен ли «контейнер» действительно отвечать за интерпретацию себя из файла конфигурации? Обычно у меня просто есть ConfigurationSectionHandler и приводятся выходные данные из ConfigurationManager в соответствующий класс, и я не беспокоюсь об этом.
tvanfosson
5
Для внутреннего использования я начал создавать «X» варианты статических классов и структур для добавления пользовательских расширений: «ConsoleX» содержит новые статические методы для «Console», «MathX» содержит новые статические методы для «Math», «ColorX» расширяет методы Color и т. д. Не совсем то же самое, но легко запомнить и обнаружить в IntelliSense.
user1689175
1
@ Xtro Я согласен, что это ужасно, но не хуже, чем неспособность использовать тестовый двойник вместо него, или, что еще хуже, отказаться от тестирования вашего кода, потому что статические классы делают его таким сложным. Кажется, Microsoft согласна со мной, потому что именно по этой причине они представили классы HttpContextWrapper / HttpContextBase, чтобы обойти статический HttpContext.Current для MVC.
tvanfosson
92

Можете ли вы добавить статические расширения для классов в 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(...)

Теперь, ПОЧЕМУ я выбрал вызов конструктора по умолчанию в качестве примера, и почему я просто не могу вернуть new T () в первом фрагменте кода, не выполняя весь этот мусор Expression? Ну, сегодня ваш счастливый день, потому что вы получаете 2fer. Как знает любой продвинутый разработчик .NET, new 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 Assert: я хочу еще 1 метод Assert AreEqual(x1,x2).

Единственный способ сделать это - указать на разные классы или написать обертку вокруг сотен различных методов Assert. Почему!?

Если было принято решение разрешить расширения экземпляров, я не вижу логической причины не разрешать статические расширения. Аргументы о секционировании библиотек не вступают в силу после того, как экземпляры могут быть расширены.

Том Делофорд
источник
20
Я также пытался расширить класс MS Unit Test для добавления 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 (). New () при использовании против универсальных типов приведет к вызову 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));

производит этот 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
Брайан Гриффин
источник
5

Вы не можете добавить статические методы к типу. Вы можете добавлять (псевдо) методы экземпляра только к экземпляру типа.

Точка thisмодификатора, чтобы сказать С # компилятора передать экземпляр на левой стороне в .качестве первого параметра метода статического / расширения.

В случае добавления статических методов к типу нет экземпляра для передачи первого параметра.

Brannon
источник
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 и импортируете пространство имен System, WebProject1.Console будет выбираться вместо System.Console по умолчанию для этих классов в пространстве имен WebProject1. Таким образом, этот код превратит все вызовы Console.WriteLine в синий, поскольку вы никогда не указывали System.Console.WriteLine.

Черный собака
источник
к сожалению, подход с использованием потомка не работает, когда базовый класс запечатан (как многие в библиотеке классов .NET)
Джордж Бирбилис
1

Следующее было отклонено в качестве редактирования ответа tvanfosson. Меня попросили внести это как мой собственный ответ. Я воспользовался его предложением и закончил реализацию ConfigurationManagerобертки. В принципе, я просто заполнил ...ответ в Тванфоссоне.

Нет. Методы расширения требуют экземпляра объекта. Однако вы можете написать статическую оболочку для интерфейса 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 { }
Wouter
источник
-4

Вы МОЖЕТЕ сделать это, если вы хотите немного «заморозить» его, создав переменную статического класса и присвоив ей значение null. Однако этот метод не будет доступен для статических вызовов класса, поэтому не уверен, насколько он будет полезен:

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
источник
это именно то, что я сделал. Мой класс называется MyTrace :)
Gishu
Полезный совет. немного запах кода, но я думаю, мы могли бы скрыть нулевой объект в базовом классе или что-то в этом роде. Спасибо.
Том Делофорд
1
Я не могу скомпилировать этот код. Ошибка «System.Console»: статические типы нельзя использовать в качестве параметров
kuncevic.dev
Да, это не может быть сделано. Черт, я думал, что вы там на что-то! Я полагаю, что статические типы нельзя передавать как параметры в методы, что имеет смысл. Будем только надеяться, что MS увидит дерево с деревьев на этом и изменит его.
Том Делофорд
3
Я должен был попытаться скомпилировать свой собственный код! Как говорит Том, это не будет работать со статическими классами.
Тенака