教程:在 C# 8.0 中使用默认接口方法更新接口Tutorial: Update interfaces with default interface methods in C# 8.0

从 .NET Core 3.0 上的 C# 8.0 开始,可以在声明接口成员时定义实现。Beginning with C# 8.0 on .NET Core 3.0, you can define an implementation when you declare a member of an interface. 最常见的方案是安全地将成员添加到已经由无数客户端发布并使用的接口。The most common scenario is to safely add members to an interface already released and used by innumerable clients.

在本教程中,你将了解:In this tutorial, you'll learn how to:

  • 通过使用实现添加方法,安全地扩展接口。Extend interfaces safely by adding methods with implementations.
  • 创建参数化实现以提供更大的灵活性。Create parameterized implementations to provide greater flexibility.
  • 使实现器能够以替代的形式提供更具体的实现。Enable implementers to provide a more specific implementation in the form of an override.

先决条件Prerequisites

需要将计算机设置为运行 .NET Core,包括 C# 8.0 编译器。You’ll need to set up your machine to run .NET Core, including the C# 8.0 compiler. Visual Studio 2019 版本 16.3.NET Core 3.0 SDK 起,开始随附 C# 8.0 编译器。The C# 8.0 compiler is available starting with Visual Studio 2019 version 16.3 or the .NET Core 3.0 SDK.

方案概述Scenario overview

本教程从客户关系库版本 1 开始。This tutorial starts with version 1 of a customer relationship library. 可以在 GitHub 上的示例存储库中获取入门应用程序。You can get the starter application on our samples repo on GitHub. 生成此库的公司希望拥有现有应用程序的客户采用其库。The company that built this library intended customers with existing applications to adopt their library. 他们为使用其库的用户提供最小接口定义供其实现。They provided minimal interface definitions for users of their library to implement. 以下是客户的接口定义:Here's the interface definition for a customer:

public interface ICustomer
{
    IEnumerable<IOrder> PreviousOrders { get; }

    DateTime DateJoined { get; }
    DateTime? LastOrder { get; }
    string Name { get; }
    IDictionary<DateTime, string> Reminders { get; }
}

他们定义了表示订单的第二个接口:They defined a second interface that represents an order:

public interface IOrder
{
    DateTime Purchased { get; }
    decimal Cost { get; }
}

通过这些接口,团队可以为其用户生成一个库,以便为其客户创造更好的体验。From those interfaces, the team could build a library for their users to create a better experience for their customers. 他们的目标是与现有客户建立更深入的关系,并改善他们与新客户的关系。Their goal was to create a deeper relationship with existing customers and improve their relationships with new customers.

现在,是时候为下一版本升级库了。Now, it's time to upgrade the library for the next release. 其中一个请求的功能可以为拥有大量订单的客户提供忠实客户折扣。One of the requested features enables a loyalty discount for customers that have lots of orders. 无论客户何时下单,都会应用这一新的忠实客户折扣。This new loyalty discount gets applied whenever a customer makes an order. 该特定折扣是每位客户的财产。The specific discount is a property of each individual customer. ICustomer 的每个实现都可以为忠实客户折扣设置不同的规则。Each implementation of ICustomer can set different rules for the loyalty discount.

添加此功能的最自然方式是使用用于应用任何忠实客户折扣的方法来增强 ICustomer 接口。The most natural way to add this functionality is to enhance the ICustomer interface with a method to apply any loyalty discount. 此设计建议引起了经验丰富的开发人员的关注:“一旦发布,接口就是固定不变的!This design suggestion caused concern among experienced developers: "Interfaces are immutable once they've been released! 这是一项突破性的变革!”This is a breaking change!" C# 8.0 添加了默认接口实现用于升级接口。C# 8.0 adds default interface implementations for upgrading interfaces. 库作者可以向接口添加新成员,并为这些成员提供默认实现。The library authors can add new members to the interface and provide a default implementation for those members.

默认接口实现使开发人员能够升级接口,同时仍允许任何实现器替代该实现。Default interface implementations enable developers to upgrade an interface while still enabling any implementors to override that implementation. 库的用户可以接受默认实现作为非中断性变更。Users of the library can accept the default implementation as a non-breaking change. 如果他们的业务规则不同,则可以进行替代。If their business rules are different, they can override.

使用默认接口方法升级Upgrade with default interface methods

团队就最有可能的默认实现达成一致:针对客户的忠实客户折扣。The team agreed on the most likely default implementation: a loyalty discount for customers.

升级应提供用于设置两个属性的功能:符合折扣条件所需的订单数量以及折扣百分比。The upgrade should provide the functionality to set two properties: the number of orders needed to be eligible for the discount, and the percentage of the discount. 这使其成为用于默认接口成员的完美方案。This makes it a perfect scenario for default interface methods. 可以向 ICustomer 接口添加方法,并提供最有可能的实现。You can add a method to the ICustomer interface, and provide the most likely implementation. 所有现有的和任何新的实现都可以使用默认实现,或者提供其自己的实现。All existing, and any new implementations can use the default implementation, or provide their own.

首先,将新方法添加到接口,包括方法的主体:First, add the new method to the interface, including the body of the method:

// Version 1:
public decimal ComputeLoyaltyDiscount()
{
    DateTime TwoYearsAgo = DateTime.Now.AddYears(-2);
    if ((DateJoined < TwoYearsAgo) && (PreviousOrders.Count() > 10))
    {
        return 0.10m;
    }
    return 0;
}

库作者编写了用于检查实现的第一个测试:The library author wrote a first test to check the implementation:

SampleCustomer c = new SampleCustomer("customer one", new DateTime(2010, 5, 31))
{
    Reminders =
    {
        { new DateTime(2010, 08, 12), "childs's birthday" },
        { new DateTime(1012, 11, 15), "anniversary" }
    }
};

SampleOrder o = new SampleOrder(new DateTime(2012, 6, 1), 5m);
c.AddOrder(o);

o = new SampleOrder(new DateTime(2103, 7, 4), 25m);
c.AddOrder(o);

// Check the discount:
ICustomer theCustomer = c;
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");

注意测试的以下部分:Notice the following portion of the test:

// Check the discount:
ICustomer theCustomer = c;
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");

SampleCustomerICustomer 的强制转换是必需的。That cast from SampleCustomer to ICustomer is necessary. SampleCustomer 类不需要为 ComputeLoyaltyDiscount 提供实现;这由 ICustomer 接口提供。The SampleCustomer class doesn't need to provide an implementation for ComputeLoyaltyDiscount; that's provided by the ICustomer interface. 但是,SampleCustomer 类不会从其接口继承成员。However, the SampleCustomer class doesn't inherit members from its interfaces. 该规则没有更改。That rule hasn't changed. 若要调用在接口中声明和实现的任何方法,该变量的类型必须是接口的类型,在本示例中为 ICustomerIn order to call any method declared and implemented in the interface, the variable must be the type of the interface, ICustomer in this example.

提供参数化Provide parameterization

这是一个好的开始。That's a good start. 但是,默认实现存在太多限制。But, the default implementation is too restrictive. 此系统的许多使用者可能会选择不同的购买数量阈值、不同的会员资格时长或不同的折扣百分比。Many consumers of this system may choose different thresholds for number of purchases, a different length of membership, or a different percentage discount. 通过提供用于设置这些参数的方法,可为更多客户提供更好的升级体验。You can provide a better upgrade experience for more customers by providing a way to set those parameters. 让我们添加一个静态方法,该方法可设置控制默认实现的三个参数:Let's add a static method that sets those three parameters controlling the default implementation:

// Version 2:
public static void SetLoyaltyThresholds(
    TimeSpan ago,
    int minimumOrders = 10,
    decimal percentageDiscount = 0.10m)
{
    length = ago;
    orderCount = minimumOrders;
    discountPercent = percentageDiscount;
}
private static TimeSpan length = new TimeSpan(365 * 2, 0,0,0); // two years
private static int orderCount = 10;
private static decimal discountPercent = 0.10m;

public decimal ComputeLoyaltyDiscount()
{
    DateTime start = DateTime.Now - length;

    if ((DateJoined < start) && (PreviousOrders.Count() > orderCount))
    {
        return discountPercent;
    }
    return 0;
}

这个小代码片段中展示了许多新的语言功能。There are many new language capabilities shown in that small code fragment. 接口现在可以包含静态成员,其中包括字段和方法。Interfaces can now include static members, including fields and methods. 还启用了不同的访问修饰符。Different access modifiers are also enabled. 其他字段是专用的,新方法是公共的。The additional fields are private, the new method is public. 接口成员允许使用任何修饰符。Any of the modifiers are allowed on interface members.

使用常规公式计算忠实客户折扣但参数有所不同的应用程序不需要提供自定义实现;它们可以通过静态方法设置自变量。Applications that use the general formula for computing the loyalty discount, but different parameters, don't need to provide a custom implementation; they can set the arguments through a static method. 例如,以下代码设置“客户答谢”,奖励任何成为会员超过一个月的客户:For example, the following code sets a "customer appreciation" that rewards any customer with more than one month's membership:

ICustomer.SetLoyaltyThresholds(new TimeSpan(30, 0, 0, 0), 1, 0.25m);
Console.WriteLine($"Current discount: {theCustomer.ComputeLoyaltyDiscount()}");

扩展默认实现Extend the default implementation

目前添加的代码提供了方便的实现,可用于用户需要类似默认实现的项目的方案,或用于提供一组不相关的规则。The code you've added so far has provided a convenient implementation for those scenarios where users want something like the default implementation, or to provide an unrelated set of rules. 对于最后一个功能,让我们稍微重构一下代码,以实现用户可能需要基于默认实现进行生成的方案。For a final feature, let's refactor the code a bit to enable scenarios where users may want to build on the default implementation.

假设有一家想要吸引新客户的初创企业。Consider a startup that wants to attract new customers. 他们为新客户的第一笔订单提供 50% 的折扣,They offer a 50% discount off a new customer's first order. 而现有客户则会获得标准折扣。Otherwise, existing customers get the standard discount. 库作者需要将默认实现移入 protected static 方法,以便实现此接口的任何类都可以在其实现中重用代码。The library author needs to move the default implementation into a protected static method so that any class implementing this interface can reuse the code in their implementation. 接口成员的默认实现也调用此共享方法:The default implementation of the interface member calls this shared method as well:

public decimal ComputeLoyaltyDiscount() => DefaultLoyaltyDiscount(this);
protected static decimal DefaultLoyaltyDiscount(ICustomer c)
{
    DateTime start = DateTime.Now - length;

    if ((c.DateJoined < start) && (c.PreviousOrders.Count() > orderCount))
    {
        return discountPercent;
    }
    return 0;
}

在实现此接口的类的实现中,替代可以调用静态帮助程序方法,并扩展该逻辑以提供“新客户”折扣:In an implementation of a class that implements this interface, the override can call the static helper method, and extend that logic to provide the "new customer" discount:

public decimal ComputeLoyaltyDiscount()
{
   if (PreviousOrders.Any() == false)
        return 0.50m;
    else
        return ICustomer.DefaultLoyaltyDiscount(this);
}

可以在 GitHub 上的示例存储库中查看整个完成的代码。You can see the entire finished code in our samples repo on GitHub. 可以在 GitHub 上的示例存储库中获取入门应用程序。You can get the starter application on our samples repo on GitHub.

这些新功能意味着,当这些新成员拥有合理的默认实现时,接口可以安全地更新。These new features mean that interfaces can be updated safely when there's a reasonable default implementation for those new members. 精心设计接口,以表达可由多个类实现的单个功能概念。Carefully design interfaces to express single functional ideas that can be implemented by multiple classes. 这样一来,在发现针对同一功能概念的新要求时,可以更轻松地升级这些接口定义。That makes it easier to upgrade those interface definitions when new requirements are discovered for that same functional idea.