使用扩展方法对可以修改的类进行扩展是否可接受?

9
我最近在思考使用扩展方法来实现我控制的类(即在同一个程序中并且可以修改)上的辅助工具。这样做的原因是,很多时候,这些辅助工具只用于特定情况,并不需要访问类的内部值。
例如,假设我有一个 StackExchange 类。它会有像 PostQuestionSearchAnswerQuestion 这样的方法。
现在,如果我想手动计算我的声望以确保 StackOverflow 没有欺骗我,我会实现类似以下的内容:
int rep=0;
foreach(var post in StackExchangeInstance.MyPosts)
{
  rep+=post.RepEarned;
}

我可以给StackExchange类添加一个方法,但它不需要修改任何内部逻辑,并且只在程序中的一两个其他位置使用。

现在想象一下,如果你有10或20个这样特定的助手方法。在某些情况下非常有用,但肯定不适用于一般情况。我的想法是改变类似下面的内容:

public static RepCalcHelpers
{
  public static int CalcRep(StackExchange inst){ ... }
}

转换成类似以下内容:

namespace Mynamespace.Extensions.RepCalculations
{
  public static RepCalcExtensions
  {
    public static int CalcRep(this Stackexchange inst){...}
  }
}

注意命名空间。我建议使用它来在特定场景中分组扩展方法,例如“RepCalculations”,“Statistics”等。
我尝试搜索是否听说过这种模式,但没有找到任何关于除了无法修改的类之外使用扩展方法的证据。
这种“模式”有什么不足之处?我应该坚持继承或组合,还是只需使用一个好的静态帮助器类?

扩展方法是用于在无法修改原始类时使用的。如果您是实现该类的人,则我看不到添加的价值。如果您只想将方法放在单独的代码文件中,则可以使用“partial”关键字。 - Kevin Gosse
只是为了澄清,您这样做是为了通过不包括特定的“using”语句来隐藏某些函数,这些语句包含边缘情况的扩展方法吗?非常有趣! - Michael Bray
你可以查看这个链接,它列出了扩展方法的优缺点。 - Tilak
4
@KooKiz,这背后的意图是为了避免出现我所谓的“Telerik综合症”,即一个类中有超过100个公共元素,这使得使用智能感知对于“发现”类来说几乎没有用处。 - Earlz
就个人而言,我认为你的想法没有任何问题。我只是不喜欢智能感知给我提供一个包含500个方法的特定类的列表。在某些情况下,“打开和关闭”不需要的功能会有很大帮助。 - igrimpe
4个回答

4
我会阅读《框架设计指南》中关于扩展方法的部分。这里是第二版作者之一的文章。你所描述的用例(专业辅助方法)被 Phil Haack 引用为使用扩展方法的有效用例,缺点是需要额外了解 API 才能找到那些“隐藏”的方法。
该文章中未提及但在书中推荐的做法是将扩展方法放入与扩展类不同的命名空间中。否则,它们将始终出现在智能感知中,并且无法关闭。

嗯,我认为一致性是保持它们易于发现的好方法。我认为拥有一个常见的.Extensions.*命名空间会使这个过程相当容易。 - Earlz
@Earlz 一致性绝对是一个优点。但我会权衡一下是否需要添加很多 using 语句才能做任何有用的事情。 - Mike Zboray
理想情况下,您不希望添加超过2个扩展名的语句,因此很难平衡扩展名的粗略分组。 - Earlz

0

我觉得我在其他地方见过这个模式。它可能会很令人困惑,但也非常强大。这样,您可以在库中提供一个类和一组扩展方法,在不同的命名空间中。然后,使用您的库的人可以选择导入带有您的扩展方法的命名空间或提供自己的扩展方法。

如果您有一些仅用于单元测试的扩展方法(例如,比较两个对象是否相等,只需要用于单元测试),那么这个模式是一个很好的选择。


0

你似乎在比较扩展方法和公共实例方法的等价性,但实际上并不是这样。

扩展方法只是一个公共静态实用方法,它恰好具有更方便的语法来调用。

因此,首先我们必须问自己,这个方法是否适合成为类本身的实例方法,还是更适合成为外部类的静态方法。由于该类的很少用户需要此功能,因为它高度局部化,并且不是类本身执行的真正行为,而是外部实体对类执行的行为,这意味着它应该是静态的。主要缺点是,如果某人拥有一个“用户”并想重新计算他们的声望,则可能更难找到其行为。现在,在这种特定情况下,它有点摇摆不定,你也可以选择另一种方式,但我倾向于使用静态方法。

现在我们已经决定它应该是静态的,接下来就是一个完全不同的问题,它是否应该是扩展方法。这更多地涉及主观性和个人偏好领域。这些方法是否可能被链接?如果是,扩展方法比嵌套调用静态方法更加方便。它是否可能在使用它的文件中经常使用?如果是,扩展方法很可能会简化代码,如果不是,它并没有太大帮助,甚至会有所损失。对于玩具示例,我可能会说我个人不会这样做,但我对那些这样做的人一点也不反感(毕竟从语法上讲,你仍然可以像使用常规公共静态方法一样使用扩展方法)。对于非玩具示例,这主要是一个逐案决定。关键是要小心扩展哪些类,并问自己用户是否愿意为了稍微方便一些而使类型的智能感知变得混乱(这又回到了它在每个文件中使用的频率问题)。

值得一提的是,有一些边缘情况下,扩展方法比实例方法更强大,特别是通过利用类型推断。使用常规实例方法很容易接受类型或该类型的任何子类型,但有时返回传入的类型而不是父类型会很有用。这在流畅的API中特别有用。虽然这不是一个非常常见的例子,而且与您的问题只有松散的关系,所以我不会详细介绍。

对于整个静态方法与扩展函数的问题:就这种情况而言,我认为使用扩展方法更容易发现,只要您使用智能提示。首先在扩展命名空间中查找与您所做的事情相关的内容,然后您想要的方法就自动出现在适用的类中。我无法告诉您有多少次我不得不查找静态辅助类,特别是当它们位于不同的不一致命名空间中时。 - Earlz

0

扩展方法在类实现接口且您想避免在其他“未来”实现相同接口的类上实现相同方法时非常有用。例如,StackExchange实现了IStackExchange,而ProgrammersExchange也实现了IStackExchange。您的示例扩展方法对于仅实现CalcRep一次并且不必在两个类上重新实现它非常有用。这正是静态Enumerable类中存在所有扩展方法的原因。

除此之外,我没有看到在可以修改的类上使用扩展方法的强制性理由。如果有什么问题,它会被认为是过度重载解析过程的劣势。


我实际上不认为被认为是超载的帮助程序是一件坏事,在大多数情况下。看看Linq,当然有一个 IEnumerable<T>.Count() 扩展方法,但许多实现 IEnumerable 的东西有比遍历整个列表更快的计数方法。 - Earlz
Enumerable.Count方法非常适合作为扩展方法,原因如我最初提到的那样,并不是因为它在重载决策中晚了。即使它是接口的一部分,每个实现者也可以实现自己快速计数的方法。 - Eren Ersönmez
1
实际上,当前实现的Count() LINQ方法会进行一些类型检查,并且如果源可枚举对象是ICollection,则会转而使用ICollection.Count。然而,采用谓词的方法不会进行此检查,因此,在其他Linq扩展的末尾使用Count()将传递一个不是ICollection的IEnumerable。 - KeithS

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