【问题标题】:Prefer extension methods for encapsulation and reusability?更喜欢封装和可重用性的扩展方法?
【发布时间】:2011-03-03 12:13:03
【问题描述】:

edit4: 已被维基化,因为这似乎更多地演变为讨论而不是具体问题。

在 C++ 编程中,“首选非成员非友元函数”而不是实例方法通常被认为是一种好的做法。这已由 Scott Meyers 在this classic Dr. Dobbs article 中推荐,并由 Herb Sutter 和 Andrei Alexandrescu 在C++ Coding Standards 中重复(第 44 项);一般的论点是,如果一个函数可以仅仅依靠类公开的公共接口来完成它的工作,它实际上增加了封装使其成为外部的。虽然这在某种程度上混淆了类的“包装”,但通常认为这些好处是值得的。

现在,自从我开始使用 C# 编程以来,我就有一种感觉,here 是他们试图实现的概念的终极表达使用“作为类接口一部分的非成员、非友元函数”来实现。 C# 添加了两个关键组件 - 第一个是 interfaces,第二个是 extension methods

  • 接口允许类正式指定其公共契约、它们向世界公开的方法和属性。
  • 任何其他类都可以选择实现相同的接口并履行相同的契约。
  • 扩展方法可以接口上定义,向所有实现者自动提供可以通过接口实现的任何功能。
  • 最重要的是,由于“实例语法”糖和 IDE 支持,它们可以像任何其他实例方法一样以相同的方式调用,从而消除认知开销!

这样你就可以在会员方便的同时获得“非会员、非朋友”功能的封装好处。对我来说似乎是两全其美; .NET 库本身在 LINQ 中提供了一个光辉的例子。然而,无论我看到哪里,我都看到人们警告不要过度使用扩展方法;甚至 MSDN 页面本身也声明:

一般来说,我们建议您谨慎地实施扩展方法,并且仅在必要时才实施。

(edit: 即使在当前的 .NET 库中,我也可以看到使用扩展而不是实例方法有用的地方——例如,@ 的所有实用函数如果将 987654326@(SortBinarySearchFindIndex 等)提升到IList<T>,将会非常有用 - 获得这样的免费奖励功能为实现接口增加了更多好处。)

那么判决结果是什么?扩展方法是封装和代码重用的极致,还是我在自欺欺人?

(edit2: 作为对 Tomas 的回应——虽然 C# 确实从 Java(过度 imo)的 OO 心态开始,但它似乎在每个新版本中都包含更多的多范式编程;主要这个问题的主旨是使用扩展方法来推动风格改变(朝向更通用/功能性 C#)是否有用或值得..)

edit3:可覆盖的扩展方法

到目前为止,这种方法发现的唯一真正问题是,如果需要,您不能专门化扩展方法。我一直在思考这个问题,我想我已经想出了一个解决方案。
假设我有一个接口MyInterface,我想扩展它-

我在MyExtension 静态类中定义我的扩展方法,并将其与另一个接口配对,称为MyExtensionOverriderMyExtension 方法是按照这种模式定义的:

public static int MyMethod(this MyInterface obj, int arg, bool attemptCast=true)
{
    if (attemptCast && obj is MyExtensionOverrider)
    {
        return ((MyExtensionOverrider)obj).MyMethod(arg);
    }
    // regular implementation here
}

覆盖接口镜像MyExtension中定义的所有方法,除了没有thisattemptCast参数:

public interface MyExtensionOverrider
{
    int MyMethod(int arg);
    string MyOtherMethod();
}

现在,任何类都可以实现接口并获得默认的扩展功能:

public class MyClass : MyInterface { ... }

任何想用特定实现覆盖它的人都可以另外实现覆盖接口:

public class MySpecializedClass : MyInterface, MyExtensionOverrider
{
    public int MyMethod(int arg) 
    { 
        //specialized implementation for one method
    }
    public string MyOtherMethod() 
    {   // fallback to default for others
        MyExtension.MyOtherMethod(this, attemptCast: false); 
    }
}

我们开始了:接口上提供的扩展方法,如果需要,可以选择完全可扩展性。也完全通用,接口本身不需要知道扩展/覆盖,并且可以实现多个扩展/覆盖对而不会相互干扰。

我可以看到这种方法存在三个问题 -

  1. 有点脆弱 - 扩展方法和覆盖接口必须手动保持同步。
  2. 有点难看 - 实现覆盖接口涉及到您不想想要专门化的每个函数的样板。
  3. 有点慢 - 每个方法的主线都添加了额外的 bool 比较和强制转换尝试。

尽管如此,我认为这是我们能得到的最好的,直到接口函数的语言支持。想法?

【问题讨论】:

  • 当你说 C# 添加接口时,我觉得 C++ 没有。 C++(主要)通过纯虚函数拥有它们。
  • 如果一个扩展方法如此重要以至于他需要出现在基类的所有实现者中,为什么不首先在基类中呢?
  • @SB:抽象基类不是完全相同的东西。 @ereOn:因为封装最好不允许访问类内部,除非您绝对必须这样做。阅读我在问题中链接的第一篇文章。
  • @ereOn,如果扩展方法是基类上的方法,如何在不重写整个 .Net Framework 和 CLR 的情况下让 LINQ 工作?
  • Joe Duffy 的一篇有趣的文章展示了这种方法的一些优缺点:bluebytesoftware.com/blog/2010/02/10/…

标签: c# c++ extension-methods encapsulation


【解决方案1】:

我认为 C# 遵循稍微不同的逻辑 - 就像在 Java 中一样,C# 中的公理是一切都是对象,所有功能都应该封装在类中(作为方法)。 C# 没有那么严格——有些值类型不是真正的对象,还有一些静态成员,它们也不属于任何对象

扩展方法增加了一项以前不可能的功能 - 您可以向接口添加成员(根据接口的核心成员实现)。这很棒而且非常有用,但我认为只有在需要向接口添加成员时才应该使用它(就像在 LINQ 中的使用一样)。

优先选择扩展方法而不是实例方法的一个可能问题是,您稍后可能会意识到您实际上需要在方法中使用一些私有状态 - 然后您必须将扩展方法更改为实例方法(这会破坏二进制兼容性)或者暴露一些隐私信息...

绝对可以区分直接依赖私有状态的成员和根据公共操作实现的“派生”成员,但我不认为扩展方法对此非常有用。也许可以用一些属性(例如UsesOnlyPublic)标记这些方法并编写一些 FxCop 规则以确保该方法不违反策略......

【讨论】:

  • 虽然 C# 肯定不是一种严格的对象语言,但值类型仍然是对象,它们只是更喜欢堆栈而不是堆......
  • Re: ABI - 虽然这是一个考虑因素,但我认为这不是什么大问题 - 只要您的界面稳定,任何使用它的方法都可以;如果它的变化足以迫使扩展成为成员,那么无论如何你都会破坏 ABI。
【解决方案2】:

扩展方法似乎确实直接解决了您引用的封装原则。但我确实看到了过度使用它们的危险。如果没有此语言功能,您基本上有两种选择来实现补充给定接口的“非成员非朋友”函数:静态实用程序函数或包装器/代理类。

我认为扩展方法方法的问题在于,它基本上是围绕静态实用函数的一种方便的语法结构。它只是为您提供静态实用程序函数的方法调用语义。

但在没有扩展方法的情况下,我认为大多数人都会同意包装/代理类是实现“非成员非朋友”功能的更好方法。我认为这真的归结为你的代码库以面向对象的方式有机增长。您今天作为简单包装器/代理创建的类可能会在明天成为您系统中的一流组件。它是一个真正的类,因此它可以拥有自己的成员,并且可能会随着您不断扩展的用例而扩大范围。

因此,我认为扩展方法存在鼓励基本静态实用程序函数的扩散的危险,而牺牲了类和对象,这是您最想在代码库中培养的构造。

【讨论】:

  • “类和对象,它们是你最想在你的代码库中培养的结构”——它们是吗?函数式或泛型编程的支持者会说不同;我认为(受功能启发的)LINQ 是通过扩展方法实现的,这不是巧合……
  • 你写的不是函数式语言。函数式编程有它的好处,但我想说如果你不想制作对象,你应该切换语言。至于泛型编程,能够在 C# 或 C++ 中进行泛型编程实际上取决于拥有对象。您要不惜一切代价避免创建非常大的对象或接口。如果您有许多具有组织良好的接口的小对象,则可以进行模板编程等等。如果扩展方法不鼓励创建大量小对象,那将是一件坏事。
【解决方案3】:

我通常喜欢扩展方法,特别是在接口上,但我有两个问题:

首先,如果一个实现有一种更有效的方式来实现扩展方法的目的,那么就没有通用的方式来表达它。例如,Enumerable.Count() 明确 知道ICollection/ICollection<T> 并对其进行特殊处理。另一种方法是,如果接口实际上可以直接包含实现,只引用其他接口方法并且声明字段。然后可以在适当的实现中覆盖这些方法。当然,这确实意味着您需要拥有接口……但在某些情况下,它会比当前的扩展方法更干净。 (通过避免引入字段的能力,我相信您可以解决类的多重继承会引入的一些实现问题。)

其次,我不喜欢扩展方法被发现的方式。没有办法说“我想要来自类 X 的扩展方法”而不从同一命名空间中的其他类中拖入扩展方法。我希望你能写:

using static System.Linq.Enumerable;

只选择那些扩展方法。

(顺便说一句,我将在星期四在NDC 2010 详细讨论这两点。希望谈话会被记录下来。)

指定仅依赖于公共接口的通用算法的能力很好。在提供接口的类型上调用这些算法的能力很好。目前的机制只是有一些尖角。

顺便说一句,能够在一个类型编写方法但说“限制我只能使用公共 API”可能会很不错。

【讨论】:

  • 感谢您的回答 :) 专业化问题我在这种方法中看到的唯一严重障碍;让接口能够包含“默认实现”会非常好。 using 的问题很烦人,但 using 的问题比扩展 imo 的问题更大。也许python风格的方法会起作用? from System.Linq use Enumerable; 很清楚,区分了导入类和命名空间。
  • @tzaman:嗯,很清楚 - 但它看起来像 Python(或 LINQ)而不是 C#。我认为“使用静态”会更 C#-y...
  • 我已经尝试过解决接口专业化问题,想看看吗? :)
  • @tzaman:是的,这几乎就是 LINQ 所做的,非常有效。正如你所说,这有点难看:(
猜你喜欢
  • 2016-03-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-27
  • 2016-05-20
  • 2021-08-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多