覆盖现有的扩展方法

72
我想用自己的方法替换.NET或ASP MVC框架中包含的扩展方法。
例如:我想替换ASP.NET MVC库中的TextBox方法。
public static string TextBox(this HtmlHelper htmlHelper, string name)
{
   ...
}

可以吗?我不能使用override或new关键字。
5个回答

120
更新:这个问题是我在2013年12月的博客主题。感谢这个好问题!
你可以在某种程度上做到这一点。但是我应该先简要谈谈C#中重载解析的基本设计原则。重载解析当然是关于从具有相同名称的方法集合中选择唯一最佳成员进行调用。
确定哪个方法是“最佳”的有许多因素参与;不同的语言使用不同的“混合”因素来解决这个问题。C#特别重视给定方法与调用点的“接近程度”。如果在基类中有一个适用的方法和一个派生类中的新适用方法可供选择,C#会选择派生类中的方法,因为它更接近,即使基类中的方法在其他方面更匹配。
所以我们按顺序来看。派生类比基类更接近。内部类比外部类更接近。类层次结构中的方法比扩展方法更接近。
现在我们来回答你的问题。扩展方法的紧密程度取决于两个因素:(1)我们需要经过多少个命名空间才能找到它?(2)我们是通过`using`找到扩展方法,还是它就在命名空间中?因此,你可以通过改变静态扩展类所在的命名空间,将其放在更接近调用位置的命名空间中,从而影响重载决策。或者,你可以改变`using`声明的顺序,将包含所需静态类的命名空间的`using`放在其他命名空间之前。
例如,如果你有以下代码:
namespace FrobCo.Blorble
{
  using BazCo.TheirExtensionNamespace;
  using FrobCo.MyExtensionNamespace;
  ... some extension method call
}

如果你想把你的优先于他们的,那么哪一个更接近就不明确了。你可以选择这样做:
namespace FrobCo
{
  using BazCo.TheirExtensionNamespace;
  namespace Blorble
  {
    using FrobCo.MyExtensionNamespace;
    ... some extension method call
  }

现在当重载解析去解决扩展方法调用时,首先是Blorble中的类,然后是FrobCo.MyExtensionNamespace中的类,然后是FrobCo中的类,最后是BazCo.TheirExtensionNamespace中的类。
清楚吗?

这里有一个问题,我该如何隐藏我的类方法,以便覆盖为该类创建的扩展方法?例如,类Deer有一个Run方法,我想确保当我在“Mars”命名空间(类MarsLand)中使用Deer时,将调用在Mars命名空间的扩展方法中声明的Run方法。Deer是Earth命名空间的一部分。 - Dhananjay
3
在类层次结构内部,适用的方法总是优先于扩展方法。如果需要调用扩展方法,请将其作为静态方法调用。 - Eric Lippert
1
为什么FrobCo.MyExtensionNamespaceBazCo.TheirExtensionNamespace更接近于FrobCo.Blorble中的任何一个类?在我看来,对于BazCo扩展,您需要上升两个命名空间级别,然后下降两个级别,而对于FrobCo扩展,您只需要上升一个命名空间级别,然后下降一个级别。因此,在我看来,FrobCo扩展更接近。 - Daniel A.A. Pelsmaeker
太不可思议了。在阅读这个答案的几分钟内,我学到了3或4件事情。 - Michael

34

扩展方法不能被覆盖,因为它们不是实例方法也不是虚方法。

如果您通过命名空间导入两个扩展方法类,则编译器将抱怨无法确定调用哪个方法:

调用以下方法或属性模糊不清...

唯一的解决方法是使用普通的静态方法语法来调用扩展方法。所以,而不是这样:

a.Foo();

您需要这样做:

YourExtensionMethodClass.Foo(a);

1
正如Eric所指出的那样,它不是导入两个不同的匹配扩展方法,而是在同一层次的嵌套中导入它们。 我相信他说的是用一个方法的行为覆盖另一个方法的行为,而不是关于多态行为。 - Rune FS

6

基于Eric的前提(和视图代码被渲染成ASP命名空间的事实),你应该能够像这样覆盖它(至少在ASP.NET MVC4.0 Razor中可以这样做)。

using System.Web.Mvc;

namespace ASP {
  public static class InputExtensionsOverride {
    public static MvcHtmlString TextBox(this HtmlHelper htmlHelper, string name) {
      TagBuilder tagBuilder = new TagBuilder("input");
      tagBuilder.Attributes.Add("type", "text");
      tagBuilder.Attributes.Add("name", name);
      tagBuilder.Attributes.Add("crazy-override", "true");
      return new MvcHtmlString(tagBuilder.ToString(TagRenderMode.Normal));
    }
  }
}

注意命名空间必须为“ASP”。

1
有人想到如何在ASP.NET Core中实现相同的功能吗?看起来在.NET 6中,视图会生成到AspNetCoreGeneratedDocument命名空间中,但是将扩展类添加到该命名空间中并不起作用(不可见)...更新:它是可见的,并且一切正常,只是Visual Studio在智能感知和“查找引用”中没有捕捉到它,我可以接受这个。 - Alex from Jitbit

4

扩展方法基本上只是静态方法,所以我不知道如何覆盖它们,但如果您将它们放在不同的命名空间中,则可以调用您自己的方法而不是要替换的方法。

但是Matt Manela谈到了实例方法优先于扩展方法: http://social.msdn.microsoft.com/forums/en-US/csharplanguage/thread/e42f1511-39e7-4fed-9e56-0cc19c00d33d

有关扩展方法的更多想法,您可以查看http://gen5.info/q/2008/07/03/extension-methods-nulls-namespaces-and-precedence-in-c/

编辑:我忘记了模糊性问题,所以您最好尝试不包括要替换的扩展方法。因此,您可能需要不使用“using”指令,而只需输入某些类的完整包名称,这可以解决问题。


1

按设计,无法覆盖扩展方法。以下是两种可能的解决方法:

1.) 使用更具体的类型

当目标类型比现有扩展方法的类型更具体时,可以使用此方法。此外,可能需要针对所有适用的类型执行此操作。

// "override" the default Linq method by using a more specific type
public static bool Any<TSource>(this List<TSource> source)
{
    return Enumerable.Any(source);
}

// this will call YOUR extension method
new List<T>().Any();

2.) 添加一个虚拟参数

这并不太好看。此外,这将要求您修改现有的调用以包含虚拟参数。

// this will be called instead of the default Linq method when "override" is specified
public static bool Any<TSource>(this IEnumerable<TSource> source, bool @override = true)
{
    return source.Any();
}

// this will call YOUR extension method
new List<T>().Any(true);

这在技术上不是一种重载吗? - Ivan García Topete
@marsze 是的,但问题是关于覆盖而不是重载。 - C.M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接