如果源代码可用,为什么要使用扩展方法而不是继承

本文关键字:方法 扩展 继承 源代码 为什么 如果 | 更新日期: 2023-09-27 17:54:25

我刚刚在msdn/books中读到,如果现有类的源代码不可用,扩展方法对于向现有类添加方法是有用的,但是我注意到在一些编写得非常好的开源代码中,扩展方法仍然与继承(抽象,接口)一起使用在有作者自己编写的源代码的类上。

这只是一般性问题,这里没有源代码。

如果源代码可用,为什么要使用扩展方法而不是继承

一个常见的原因是依赖管理:假设您有一个相当通用的类User和一个不那么通用的类GravatarImage。现在可以用SomeUser.GravatarImage()代替GravatarImage.ImageForUser(SomeUser)了。这不仅方便;在一个大型项目中,其他程序员可能很难找到"正确"的做事方式。智能感知在这里会有很大帮助。

然而,User类是一个"后端"类,不需要知道任何关于图像、视图、重力或url的信息,所以你需要保持依赖关系干净。

类似的参数也适用于LINQ,它基本上由扩展方法组成。这些扩展方法扩展了集合接口,因此您可以使用非常小的接口创建许多功能。实现一种新的IEnumerable是非常容易的,但是你获得了LINQ提供的所有功能。

按照本例,IEnumerable接口只允许获取一个枚举数。而不是要求每个实现者提供一个Count方法,您可以调用扩展方法来完成相同的工作。

然而,值得注意的是,像IEnumerable.Count()这样的方法可能非常慢(它必须触及每个元素),而底层类的直接实现可以像返回一个简单的int一样简单。

这里已经有很多很棒的答案:我喜欢在可选行为和接口上使用扩展。还有其他一些很好的理由。

避免抽象方法重载考虑以下接口:

public interface IUserService
{
    User GetUser(int userId);
    User GetUser(int userId, bool includeProfilePic);
}

我们可以看到,当从IUserService获取用户时,可选地包含个人资料图片是多么有用。但是,使用接口上的两个方法,它们可以以完全不同的方式实现(这种简单的可能不会,但我经常遇到这个问题)。使用扩展方法,重载不能具有发散行为:

public interface IUserService
{
    User GetUser(int userId, bool includeProfilePic);
}
public static class UserServiceExtensions
{
    public static User GetUser(this IUserService userService, int userId)
    {
        return userService.GetUser(userId, false);
    }
}

尊重封装。如果您想要在类中添加一些额外的功能,但它不需要访问类的内部成员来运行,并且不保留状态,那么使用扩展方法是可取的。对类内部成员和状态的了解越少,耦合就越少,从长远来看,维护代码就越容易。

缺点:你不能Moq扩展方法很多时候这并不重要。这意味着,为了模拟扩展方法背后的行为,您通常需要知道扩展方法是如何工作的,并模拟它调用的虚拟方法。这将您的测试与扩展方法的实现耦合在一起。如果您的扩展方法很简单且不太可能更改,那么这就很烦人了。如果您的扩展方法封装了一些复杂的调用集,这就非常糟糕了。因此,我通常只对相对简单的行为使用扩展方法。

在c#中,为接口提供扩展方法通常是在尝试近似* mixins。

mixin也可以被看作是一个带有实现方法的接口。

虽然c#不支持mixins,但是为一个接口类提供其他可以实现的扩展方法是一种"附加"功能的好方法。

下面是一个真实世界的例子,一个简单的接口,以mixin的形式提供了附加的功能:

public interface IRandomNumberGenerator
{
    Int32 NextInt();
}
public static class RandomNumberGeneratorExtensions
{
    public static Double NextDouble(this IRandomNumberGenerator instance)
    {
        return (Double)Int32.MaxValue / (Double)instance.NextInt();
    }
}
// Now any class which implements IRandomNumberGenerator will get the NextDouble() method for free...

* c#近似的mixin和真实的东西之间的最大区别在于,在支持的语言中,mixin可以包含私有状态,而c#中接口上的扩展方法显然只能访问公共状态。

考虑扩展方法的一个好方法就像一个插件类型的体系结构——它们使您能够包含/排除特定类型实例的功能。具体回答你的问题:

如果源代码可用,为什么要使用扩展方法而不是继承

对于可选功能,最简单的答案是。使用扩展方法的最常见(可能也是最重要的)原因是能够在不更改任何核心功能的情况下扩展特定类型。为了添加几个方法而派生新类型是多余的,即使您可以控制源代码,将改为partial类型会使更有意义。扩展方法往往用于解决特定场景的问题,并不真正值得进入核心代码库。然而,如果你发现自己在所有地方都使用它们,那么这是一个很好的指示,你可能没有正确使用它们。

例如,考虑以下扩展名:

var epochTime = DateTime.UtcNow.ToEpochTime();

ToEpochTime将返回日期时间作为Unix时间。作为生成时间戳或序列化日期的替代方法,这将非常有用。然而,这是一个相当特定的功能,所以它不会有意义成为DateTime的一部分,但通过使它成为一个扩展方法,它允许我简单地包括这种类型的功能,如果&在需要时。

我认为子类化作为扩展方法的替代方法有几个缺点:

  1. 如果你只想添加几个轻量级的方法,这可能是过度的。
  2. 如果你已经有一个复杂的继承结构(特别是如果你的类已经有子类),或者将来打算有一个,它会产生问题。

我经常使用扩展方法来桥接名称空间之间的边界,以增加可读性并保持关注点分离。例如,myObject.GetDatabaseEntity()读起来很好,但GetDatabaseEntity()的代码应该在代码的数据库部分,而不是在我的业务逻辑中。通过把这段代码放在一个扩展方法中,我可以把所有东西都放在它所属的地方,而不会增加子类化的复杂性。

此外,如果myObject在传递给数据库代码之前在我的业务逻辑中实例化,那么业务逻辑将需要包含数据库名称空间。我希望每个模块的职责都有明确的划分,并且希望我的业务逻辑尽可能少地了解数据库。

还有一些扩展方法很有用的技巧(其中一些已经在其他答案中提到了):

  1. 它们可以应用于接口(LINQ经常使用)。
  2. 它们可以应用于枚举。如果您使用正确的签名创建它们,它们可以用作事件处理程序。(我不建议这样做,因为它可能导致混乱,但它可以节省您存储对象的引用,否则您将需要在某个集合中粘贴-小心泄漏!)

这可能是一个特殊的情况,但我发现扩展方法在为现有类提供各种类型的"规则"时很有用。

假设你有一个类SomeDataContainer,有很多成员和数据,你想在某些情况下导出该数据的某些部分,在其他情况下导出其他部分。

你可以在SomeDataContainer中这样做:

if(ShouldIncludeDataXyz()){
    exportXyz();
}
(...) 
private bool ShouldIncludeDataXyz(){
    // rules here
}
private void ExportXyz(){ (...) }

…但我发现这有时会变得很混乱,特别是如果你有很多类,很多规则等。

在某些情况下,我所做的是将规则放在单独的类中,每个"数据类"有一个"规则类",并将规则创建为扩展类。

这只是在一个地方给了我一个规则的层次结构,与核心数据分离-我发现这种分离无论如何都很有用。

结果代码仍然类似于上面的代码:
// This now calls an extention method, which can be found
// in eg. "SomeDataContainerRules.cs", along with other similar
// "rules"-classes:
if(this.ShouldIncludeDataXyz()){ 
    exportXyz();
}