我可以向现有的静态类添加扩展方法吗?


532

我很喜欢C#中的扩展方法,但是将扩展方法添加到静态类(例如Console)并没有取得成功。

例如,如果我想向控制台添加一个名为“ WriteBlueLine”的扩展,以便可以进行以下操作:

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

我通过添加一个本地的,公共的静态方法(使用Console作为“ 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
那好吧。不幸的是,我想我会过去的。我还是处女的扩展方法(无论如何在生产代码中)。也许有一天,如果我很幸运的话。
Andy McCluggage,

我已经为ASP.NET MVC编写了许多HtmlHelper扩展。为DateTime写了一个给我给定日期的结尾(23:59.59)。当您要求用户指定结束日期,但实际上希望它是该天的结束时,此方法很有用。
tvanfosson

12
当前无法添加它们,因为该功能在C#中不存在。并不是因为它本身不可能实现,而是因为C#忙碌起来,他们对使LINQ正常工作的扩展方法很感兴趣,并且在静态扩展方法中没有看到足够的好处来证明实现它们所需的时间。埃里克·利珀特(Eric Lippert)在这里解释
乔丹·格雷

1
只需拨打Helpers.WriteBlueLine(null, "Hi");:)
侯赛因Yağlı

Answers:


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的实际调用的包装器类(或Facade)。您可以将任何想要的方法添加到包装类,因此它不必是扩展。
tvanfosson

我发现将静态方法添加到实现ConfigurationSection的类中会更有帮助。因此,在给定名为MyConfigurationSection的实现的情况下,我将调用MyConfigurationSection.GetSection(),该方法返回已键入的节;如果不存在,则返回null。最终结果相同,但是避免添加类。
点按

1
@tap-这只是一个例子,是我想到的第一个例子。不过,单一责任原则也发挥了作用。“容器”实际上应该负责从配置文件解释自身吗?通常,我只是拥有ConfigurationSectionHandler,并将ConfigurationManager的输出强制转换为适当的类,而不必理会包装器。
tvanfosson

5
为了内部使用,我开始创建静态类和结构的“ X”变体以添加自定义扩展:“ ConsoleX”包含用于“ Console”的新静态方法,“ MathX”包含用于“ Math”,“ ColorX”的新静态方法扩展了'Color'方法,等等。虽然不太一样,但是很容易在IntelliSense中记住和发现。
user1689175 2014年

1
@Xtro我同意这很糟糕,但是并不比不能在其中使用双重测试更糟糕,或者更糟糕的是,放弃测试您的代码是因为静态类使它变得如此困难。微软似乎同意我的看法,因为这是他们引入HttpContextWrapper / HttpContextBase类来解决MVC静态HttpContext.Current的原因。
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; }
}

运作方式如下。虽然您不能从技术上编写静态扩展方法,但是此代码利用了扩展方法中的漏洞。漏洞在于您可以在没有null异常的情况下对null对象调用扩展方法(除非您通过@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(...)

现在为什么选择调用默认构造函数作为示例,并且为什么不在不执行所有Expression垃圾代码的情况下不直接在第一个代码段中返回新的T()?那么今天是您的幸运日,因为您得到了2fer。正如任何高级.NET开发人员所知道的那样,新的T()很慢,因为它会生成对System.Activator的调用,该调用使用反射在调用它之前获取默认构造函数。该死的微软!但是,我的代码直接调用了对象的默认构造函数。

静态扩展会比这更好,但是绝望的时代需要绝望的措施。


2
我认为对于Dataset来说,这种技巧会起作用,但是我怀疑它对Console类有用,因为Console是静态类,不能将静态类型用作参数:)
ThomasBecker 2015年

是的,我要说同样的话。这是非静态类上的伪静态扩展方法。OP是静态类的扩展方法。
Mark A. Donohoe

2
这是更好,更容易只是有这样的方法,如一些命名约定XConsoleConsoleHelper等等。
亚历克斯·朱可夫斯基

9
这是一个引人入胜的技巧,但结果却很臭。创建空对象,然后似乎在其上调用方法-尽管多年以来人们一直被告知“在空对象上调用方法会导致异常”。它可以工作,但是..ugh ...让以后维护的人感到困惑。我不会拒绝投票,因为您已经添加了关于可能的信息。但我衷心希望没人能使用这种技术!!额外的抱怨:不要将其中之一传递给方法,并且期望得到OO子类化:调用的方法将是参数声明的类型,而不是传入参数的类型。
制造商史蒂夫

5
这很棘手,但我喜欢。一种替代方法(null as DataSet).Create();可能是default(DataSet).Create();
Bagerfahrer

54

这是不可能的。

是的,我认为MS在这里犯了一个错误。

他们的决定没有道理,并迫使程序员编写(如上所述)无意义的包装器类。

这是一个很好的例子:尝试扩展静态MS单元测试类Assert:我还需要1个Assert方法AreEqual(x1,x2)

做到这一点的唯一方法是指向不同的类,或围绕100多种不同的Assert方法编写包装。为什么!?

如果决定允许实例的扩展,则我认为没有逻辑原因不允许静态扩展。一旦可以扩展实例,关于节库的争论就不会成立。


20
我还试图扩展MS单元测试类Assert以添加Assert.Throws和Assert.DoesNotThrow,并且遇到了同样的问题。
Stefano Ricciardi

3
是啊我也是:(我想我可以做Assert.Throws的答案stackoverflow.com/questions/113395/...
CallMeLaNN

27

在尝试找到OP所遇到的相同问题的答案时,我偶然发现了这个线程。我没有找到想要的答案,但最终还是这样做了。

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 2014年


11

不。扩展方法定义需要您要扩展的类型的实例。它的不幸;我不确定为什么需要它...


4
这是因为使用扩展方法来扩展对象的实例。如果他们不这样做,那将只是常规的静态方法。
德里克·埃金斯

31
两者都很好,不是吗?

7

至于扩展方法,扩展方法本身是静态的。但是它们就像实例方法一样被调用。由于静态类不可实例化,因此您永远不会有该类的实例来从中调用扩展方法。因此,编译器不允许为静态类定义扩展方法。

Obnoxious先生写道:“任何高级.NET开发人员都知道,新的T()很慢,因为它会生成对System.Activator的调用,该调用使用反射在调用它之前获取默认构造函数”。

如果在编译时知道类型,则将new()编译为IL“ newobj”指令。Newobj采用直接调用的构造函数。调用System.Activator.CreateInstance()会编译为IL“调用”指令,以调用System.Activator.CreateInstance()。将New()用于泛型类型时,将导致对System.Activator.CreateInstance()的调用。Obnoxious先生的职位在这一点上尚不清楚……而且很令人讨厌。

这段代码:

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修饰符的要点是告诉C#编译器在该实例的左侧传递实例。.作为static / extension方法的第一个参数。

在将静态方法添加到类型的情况下,第一个参数没有实例要传递。


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)。如果其他类想要访问Console的其他静态功能,则必须通过其名称空间明确引用它们。

如果要将所有方法放在一个位置,则始终可以将所有方法添加到替换类中。

所以你会有类似

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

这可以工作,但Console不能,因为它是静态的。

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中。它可能是行数或错误的单词过滤器等等。只要在名称空间中指定Console时说出WebProject1并导入名称空间System,就会在System.Console上选择WebProject1.Console作为名称空间WebProject1中这些类的默认值。因此,只要您从未指定System.Console.WriteLine,那么这段代码会将所有Console.WriteLine调用变成蓝色。


不幸的是,当基类被密封时(如.NET类库中的许多基类),使用后代的方法不起作用
George Birbilis

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

YourType:

public class YourType { }

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

这正是我所做的。我的班级称为MyTrace :)
Gishu 2010年

有用的提示。有点代码味道,但是我想我们可以将空对象隐藏在基类中。谢谢。
汤姆·德洛福德

1
我无法编译此代码。错误“ System.Console”:静态类型不能用作参数
kuncevic.dev

是的,这无法完成。该死的我以为你在那儿找东西!我想,静态类型不能作为参数传递给方法。我们只希望MS能够从这棵树上看到树木并进行更改。
汤姆·德洛福德

3
我应该尝试编译自己的代码!如汤姆所说,这不适用于静态类。
Tenaka
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.