如果源代码可用,为什么要使用扩展方法而不是继承
本文关键字:方法 扩展 继承 源代码 为什么 如果 | 更新日期: 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
的一部分,但通过使它成为一个扩展方法,它允许我简单地包括这种类型的功能,如果&在需要时。
我认为子类化作为扩展方法的替代方法有几个缺点:
- 如果你只想添加几个轻量级的方法,这可能是过度的。
- 如果你已经有一个复杂的继承结构(特别是如果你的类已经有子类),或者将来打算有一个,它会产生问题。
我经常使用扩展方法来桥接名称空间之间的边界,以增加可读性并保持关注点分离。例如,myObject.GetDatabaseEntity()
读起来很好,但GetDatabaseEntity()
的代码应该在代码的数据库部分,而不是在我的业务逻辑中。通过把这段代码放在一个扩展方法中,我可以把所有东西都放在它所属的地方,而不会增加子类化的复杂性。
此外,如果myObject
在传递给数据库代码之前在我的业务逻辑中实例化,那么业务逻辑将需要包含数据库名称空间。我希望每个模块的职责都有明确的划分,并且希望我的业务逻辑尽可能少地了解数据库。
还有一些扩展方法很有用的技巧(其中一些已经在其他答案中提到了):
- 它们可以应用于接口(LINQ经常使用)。
- 它们可以应用于枚举。如果您使用正确的签名创建它们,它们可以用作事件处理程序。(我不建议这样做,因为它可能导致混乱,但它可以节省您存储对象的引用,否则您将需要在某个集合中粘贴-小心泄漏!)
这可能是一个特殊的情况,但我发现扩展方法在为现有类提供各种类型的"规则"时很有用。
假设你有一个类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();
}