扩展方法的缺点是什么?

32

扩展方法是一项非常有用的功能,您可以在任何类中添加许多所需的函数。但我想知道它是否存在可能会给我带来麻烦的缺点。有什么评论或建议吗?

7个回答

39
  • 扩展方法的导入方式(即整个命名空间)不够细粒度。如果想要导入某个命名空间中的一个扩展方法,就必须把该命名空间中的所有扩展方法都导入。
  • 从源代码上并不容易看出该方法是在哪里定义的。这也是一种 优点——它意味着你可以让你的代码在类型的其余方法上保持一致,即使由于某些原因你不能把它放在同一个位置上。换句话说,在高层次上理解代码更简单,但在确切地执行什么方面则更复杂。我认为这也适用于LINQ。
  • 你只能拥有扩展方法,而不能拥有属性、索引器、操作符、构造函数等。
  • 如果你正在扩展一个第三方类,并且在以后的版本中他们介绍了一个具有相同签名的新方法,你将无法轻松知道你所调用的代码的含义已经改变了。如果新方法与你的扩展非常 相似,但边界条件(或其他条件)略有不同,则可能会导致一些非常棘手的问题。尽管发生这种情况的可能性相对较小。

很好的解释。在多个用户同时调用时,扩展方法是否会有害?比如我有一个扩展方法 `public static A ToDTO(this AD ad) { return AutoMapper.Mapper.Map<A>(ad); }`,我这样调用 `AD.ToDTO()` - 当单个用户使用时它可以正常工作,但是当多个用户调用时,应用程序会卡在这个方法上,并在一段时间后抛出超时异常,这是扩展方法的一种缺点吗?因为我们在这里有一个静态类。 - Gaurav Arora
1
@GauravKumarArora:事实上,某个方法是扩展方法与此无关——如果它作为普通静态方法在“旧风格”中调用是安全的,那么作为扩展方法也是安全的。假设你所说的“多个用户”实际上是指线程安全性,那么扩展方法既不提供也不删除任何类型的线程安全性。 - Jon Skeet
1
我们无法在单元测试中模拟扩展方法。 - Sandeep Shekhawat
如果粒度成为问题,可以通过不导入命名空间并完全限定调用特定的扩展方法来解决。MyNamespace.ExtMethod(myObject, params) 这样做放弃了漂亮的语法,但它只是一个函数。(实际上,导入整个命名空间的烦恼可能对命名空间中的任何实体都是一个问题,因此这并不是扩展方法所特有的。在某些情况下,使用#include别名可以减轻痛苦。) - C Perkins

6

注意两点:

  • 除非您在VS.NET中,否则很难确定扩展方法的来源
  • 无法通过反射或C# 4.0的动态查找解析扩展方法

1
您可以通过反射找到扩展方法。查找属性“ExtensionAttribute”。请参阅https://dev59.com/ZHVC5IYBdhLWcg3wbQfA - Patrik Svensson
@ Patrik - 但是你无法明确地做到这一点,因为你不知道哪些“usings”应该被考虑。有时候这很重要。 - Marc Gravell
1
@Patrik: 我所说的“分辨率”是指:虽然您可以调用myType.extendedMethod(),但您无法通过通常的反射代码,即typeof(MyType).GetMethod("extendedMethod").Invoke(...)或动态查找,即dynamic myType = new MyType(); myType.extendedMethod();来执行它。 - Buu

4
  • 使用静态方法进行空值检查是不同的。并非一定更好或更差 - 而是不同的,开发人员需要理解这一点。在空值上调用方法可能会出乎意料,并且(有时)非常有用。

  • 没有多态性(尽管支持重载)

  • 如果两个扩展方法对源类型产生冲突(而且都不符合“更好”的条件),则可能会导致歧义混乱。编译器将拒绝使用任何一个...这意味着在程序集A中添加扩展方法可能会破坏*程序集B中的不相关代码。我见过这种情况几次...

  • 您无法在C# 2.0中使用“in”,因此,如果您正在为C# 2.0编写库,则无法使用它

  • 它需要[ExtensionAttribute] - 因此,如果您正在为.NET 2.0编写库,则会遇到麻烦:如果声明自己的[ExtensionAttribute],则可能会与.NET 3.5调用方发生冲突

但请不要误解 - 我是一个大粉丝!

您可能已经猜到,我目前正在编写一个需要为C# 2.0和.NET 2.0调用者工作的库 - 并且(令人恼火的是)扩展方法将非常有用!

*=仅针对编译器;已经编译的代码将没有问题


尽管扩展方法调用无法进行多态分派,但是扩展方法可以使用各种方式根据它们被调用的类型来变化其操作。例如,可以定义一个 SafeEquals<T,U>(this T it, U other) 扩展方法,该方法将调用存储在 SafeComparer<T,U>.CheckEquality(it, other) 中的 Func<T,U,bool> [默认委托将检查 TU 并确定如何执行比较]。 - supercat

3
扩展方法很有趣,但也存在潜在问题。例如,如果您编写了一个扩展方法,而另一个库创建了具有相同签名的扩展方法,那么您将在使用两个命名空间时遇到困难。
此外,可以说它们不够易发现。我认为这取决于情况。在某些情况下,您的代码应该封装在类中,在其他情况下,将该功能添加为扩展方法是可以的。
通常,我会将扩展方法作为我的自定义类或BCL类的包装器,并将它们放在不同的命名空间中。例如 Utils 和 Utils.Extensions。这样就不必使用扩展方法了。

1
@ 重复签名的评论:这也是许多其他编程语言中固有的风险,人们还是能够应对得不错的。虽然它可能仍然是一个劣势,在大多数情况下,我认为这是不太可能发生的。 - Chris

0
对我来说,如果不深入了解现有代码就添加方法是很困难的。例如,你一直在使用第三方类来做什么,如果你不是原开发人员并且不知道类的结构是如何组织的,那么你有什么必要去扩展这个类的功能呢?这几乎是没有意义的,就像在看不见东西的情况下开车一样。在LINQ中,由于它是由微软的设计师/编码实现的,所以这样做很有意义,因为他们拥有了如何实现序列的内部知识,现在他们想通过添加Count方法来计算序列中所有元素的数量来扩展其功能。话虽如此,我希望有人能够通过给出一个实际的例子来反驳我的观点,证明扩展方法的必要性。

一个简单的例子是,比如为字符串添加一个“IsNullOrWhiteSpace()”扩展方法。 - jkerak

0
就缺點而言,我會認為它們有點像宏 - 由於其他人可能不熟悉你添加的擴展功能,因此最終可能會得到更難以維護的代碼。

0

扩展方法有两种不同的应用场景:

  1. 如果你想扩展来自外部程序集的任何类、结构或接口,除了那些没有标记为密封的类,你可能有一个选项可以从这些类派生并使用附加方法进行扩展,但是对于密封的类和结构以及接口,它们不能被继承。
  2. 然而,在你自己的程序集中,为特定的类、结构或接口提供扩展方法是一个非常主观的设计问题。如果你想对使用你的程序集的人很慷慨,你可以创建非常小的接口,理想情况下只有几个属性,然后将整个批量的扩展方法附加到它们上面。以下是一个例子:

    public interface IPointF{
        float X { get; }
        float Y { get; }
    }
    
    public struct PointF: IPointF{
        public PointF(float x, float y){
            X=x;Y=y;
        }
        public float X { get; private set; }
        public float Y { get; private set; }
    }
    
    public static class PointHelper{
        public static PointF ADD(this IPointF p, float val){
            return new PointF(p.x+val, p.y+val);
        }
    }
    

因此,任何未来的用户都可以通过继承IPoint来节省大量编码时间。


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