.Net 3.5 中的新扩展允许将功能从接口中分离出来。

例如在.Net 2.0中

public interface IHaveChildren {
    string ParentType { get; }
    int ParentId { get; }

    List<IChild> GetChildren()
}

可以(在 3.5 中)变成:

public interface IHaveChildren {
    string ParentType { get; }
    int ParentId { get; }
}

public static class HaveChildrenExtension {
    public static List<IChild> GetChildren( this IHaveChildren ) {
        //logic to get children by parent type and id
        //shared for all classes implementing IHaveChildren 
    }
}

在我看来,这对于许多接口来说是一个更好的机制。他们不再需要抽象基础来共享此代码,并且在功能上代码的工作原理相同。这可以使代码更易于维护且更易于测试。

唯一的缺点是抽象基实现可以是虚拟的,但是可以解决这个问题(实例方法会隐藏同名的扩展方法吗?这样做会使代码混乱吗?)

还有其他原因不经常使用这种模式吗?


澄清:

是的,我看到扩展方法的趋势是最终到处都是它们。我会特别小心在没有大量同行评审的情况下拥有任何 .Net 值类型(我认为我们在字符串上唯一的类型是 .SplitToDictionary() - 如同 .Split() 但也采用键值分隔符)

我认为那里有一个完整的最佳实践辩论;-)

(顺便:DannySmurf,你的 PM 听起来很可怕。)

我在这里特别询问有关在以前我们有接口方法的地方使用扩展方法。


我试图避免很多级别的抽象基类 - 实现这些模型的类大多已经有基类。我认为这个模型比添加更多的对象层次结构更易于维护并且更少过度耦合。

这就是 MS 对 Linq 的 IEnumerable 和 IQueryable 所做的吗?

有帮助吗?

解决方案

我认为明智地使用扩展方法可以使接口与(抽象)基类处于更平等的位置。


版本控制。 基类相对于接口的一个优点是,您可以轻松地在更高版本中添加新的虚拟成员,而向接口添加成员将破坏针对旧版本库构建的实现者。相反,需要创建具有新成员的新版本接口,并且库将必须解决或限制对仅实现原始接口的遗留对象的访问。

作为一个具体的例子,库的第一个版本可能定义如下接口:

public interface INode {
  INode Root { get; }
  List<INode> GetChildren( );
}

一旦库发布,我们就无法在不破坏当前用户的情况下修改界面。相反,在下一个版本中,我们需要定义一个新接口来添加附加功能:

public interface IChildNode : INode {
  INode Parent { get; }
}

但是,只有新库的用户才能实现新界面。为了使用遗留代码,我们需要调整旧的实现,扩展方法可以很好地处理:

public static class NodeExtensions {
  public INode GetParent( this INode node ) {
    // If the node implements the new interface, call it directly.
    var childNode = node as IChildNode;
    if( !object.ReferenceEquals( childNode, null ) )
      return childNode.Parent;

    // Otherwise, fall back on a default implementation.
    return FindParent( node, node.Root );
  }
}

现在,新库的所有用户都可以同等对待旧版和现代实现。


超载。 扩展方法有用的另一个领域是为接口方法提供重载。您可能有一个带有多个参数的方法来控制其操作,其中在 90% 的情况下只有前一个或两个参数很重要。由于 C# 不允许设置参数的默认值,因此用户要么每次都必须调用完全参数化的方法,要么每次实现都必须为核心方法实现简单的重载。

相反,可以使用扩展方法来提供简单的重载实现:

public interface ILongMethod {
  public bool LongMethod( string s, double d, int i, object o, ... );
}

...
public static LongMethodExtensions {
  public bool LongMethod( this ILongMethod lm, string s, double d ) {
    lm.LongMethod( s, d, 0, null );
  }
  ...
}


请注意,这两种情况都是根据接口提供的操作编写的,并且涉及琐碎或众所周知的默认实现。也就是说,您只能从类继承一次,并且有针对性地使用扩展方法可以提供一种有价值的方法来处理接口所缺乏的基类提供的一些细节:)


编辑: 乔·达菲 (Joe Duffy) 的相关帖子: 扩展方法作为默认接口方法实现

其他提示

扩展方法应该这样使用:扩展。任何与关键结构/设计相关的代码或重要操作都应该放在由类或接口组成/继承的对象中。

一旦另一个对象尝试使用扩展对象,他们将看不到扩展,并且可能必须再次重新实现/重新引用它们。

传统观点是扩展方法只能用于:

  • 正如 Vaibhav 提到的,实用程序类
  • 扩展密封的第三方 API

我认为扩展方法取代的最好的东西是您在每个项目中找到的所有实用程序类。

至少目前,我觉得扩展方法的任何其他使用都会在工作场所引起混乱。

我的两点

我认为使用扩展方法分离域/模型和 UI/视图功能是一件好事,特别是因为它们可以驻留在单独的命名空间中。

例如:

namespace Model
{
    class Person
    {
        public string Title { get; set; }
        public string FirstName { get; set; }
        public string Surname { get; set; }
    }
}

namespace View
{
    static class PersonExtensions
    {
        public static string FullName(this Model.Person p)
        {
            return p.Title + " " + p.FirstName + " " + p.Surname;
        }

        public static string FormalName(this Model.Person p)
        {
            return p.Title + " " + p.FirstName[0] + ". " + p.Surname;
        }
    }
}

这样,扩展方法的使用方式与 XAML 数据模板类似。您无法访问类的私有/受保护成员,但它允许维护数据抽象,而无需在整个应用程序中重复过多的代码。

扩展接口没有任何问题,事实上这就是 LINQ 将扩展方法添加到集合类的方式。

话虽如此,您实际上应该只在需要在实现该接口的所有类中提供相同功能并且该功能不是(并且可能不应该)任何派生的“官方”实现的一部分的情况下才这样做类。如果为每个可能需要新功能的派生类型编写扩展方法是不切实际的,那么扩展接口也是不错的选择。

我看到很多人提倡使用基类来共享通用功能。请小心这一点 - 您应该更喜欢组合而不是继承。仅当从建模的角度来看有意义时,才应将继承用于多态性。它不是代码重用的好工具。

至于问题:执行此操作时请注意限制 - 例如,在所示的代码中,使用扩展方法来实现 GetChildren 有效地“密封”此实现,并且不允许任何 IHaveChildren impl 在需要时提供自己的实现。如果这没问题,那么我不太介意扩展方法方法。它不是一成不变的,当以后需要更多灵活性时通常可以轻松重构。

为了获得更大的灵活性,使用策略模式可能更好。就像是:

public interface IHaveChildren 
{
    string ParentType { get; }
    int ParentId { get; }
}

public interface IChildIterator
{
    IEnumerable<IChild> GetChildren();
}

public void DefaultChildIterator : IChildIterator
{
    private readonly IHaveChildren _parent;

    public DefaultChildIterator(IHaveChildren parent)
    {
        _parent = parent; 
    }

    public IEnumerable<IChild> GetChildren() 
    { 
        // default child iterator impl
    }
}

public class Node : IHaveChildren, IChildIterator
{ 
    // *snip*

    public IEnumerable<IChild> GetChildren()
    {
        return new DefaultChildIterator(this).GetChildren();
    }
}

多一点。

如果多个接口具有相同的扩展方法签名,则需要将调用者显式转换为一种接口类型,然后调用该方法。例如。

((IFirst)this).AmbigousMethod()

哎哟。请不要扩展接口。
接口是类应该实现的干净契约,以及您对所述类的使用 必须 仅限于核心接口中的内容才能正常工作。

这就是为什么您总是将接口声明为类型而不是实际的类。

IInterface variable = new ImplementingClass();

正确的?

如果您确实需要具有一些附加功能的合约,那么抽象类是您的朋友。

Rob Connery(Subsonic 和 MVC Storefront)在他的 Storefront 应用程序中实现了类似 IRepository 的模式。它与上面的模式不完全相同,但确实有一些相似之处。

数据层返回 IQueryable,它允许使用层在此基础上应用过滤和排序表达式。例如,好处是能够指定单个 GetProducts 方法,然后在消费层中适当地决定如何排序、过滤甚至只是特定范围的结果。

这不是传统的方法,但非常酷,绝对是 DRY 的一个例子。

我看到的一个问题是,在一家大公司中,这种模式可能会让代码变得难以(如果不是不可能的话)让任何人理解和使用。如果多个开发人员不断地将自己的方法添加到现有的类中,与这些类分开(上帝保佑我们所有人,甚至添加到 BCL 类中),我可能会看到代码库很快就会失控。

即使在我自己的工作中,我也能看到这种情况的发生,因为我的 PM 希望将我们工作的每一点代码添加到 UI 或数据访问层,我完全可以看到他坚持要添加 20 或 30 个方法仅与字符串处理无关的 System.String。

我需要解决类似的问题:我想要将 List<IIDable> 传递给扩展函数,其中 IIDable 是一个具有长 getId() 函数的接口。我尝试使用 GetIds(this List<IIDable> bla) 但编译器不允许我这样做。我改用模板,然后在函数内部将类型强制转换为接口类型。我需要这个函数来处理一些 linq to sql 生成的类。

我希望这有帮助 :)

    public static List<long> GetIds<T>(this List<T> original){
        List<long> ret = new List<long>();
        if (original == null)
            return ret;

        try
        {
            foreach (T t in original)
            {
                IIDable idable = (IIDable)t;
                ret.Add(idable.getId());
            }
            return ret;
        }
        catch (Exception)
        {
            throw new Exception("Class calling this extension must implement IIDable interface");
        }
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top