为什么接口的默认实现有用?

我的上一篇文章中,我承诺要谈论一些情况,在这种情况下,考虑在接口中使用默认实现是有意义的。 当然,此功能不会取消许多现有的代码编写约定,但是我发现在某些情况下使用默认实现会导致代码更简洁,更具可读性(至少在我看来)。

具有向后兼容性的扩展接口


该文件说:
最常见的情况是将方法安全地添加到已经由无数客户端发布和使用的接口中。
解决的问题是,从接口继承的每个类都必须为新方法提供一个实现。 当仅由您自己的代码使用该接口时,这不是很困难,但是如果该接口在公共库中或由其他命令使用,则添加新的接口元素可能会引起很大的麻烦。

考虑一个例子:

interface ICar { string Make { get; } } public class Avalon : ICar { public string Make => "Toyota"; } 

如果要向此接口添加新的GetTopSpeed()方法,则需要在Avalon中添加其实现:

 interface ICar { string Make { get; } int GetTopSpeed(); } public class Avalon : ICar { public string Make => "Toyota"; public int GetTopSpeed() => 130; } 

但是,如果我在ICar中创建GetTopSpeed()方法的默认实现, 则不必将其添加到每个继承的类中。

 interface ICar { string Make { get; } public int GetTopSpeed() => 150; } public class Avalon : ICar { public string Make => "Toyota"; } 

如有必要,我仍然可以在默认值不适合的类中重载实现:

 interface ICar { string Make { get; } public int GetTopSpeed() => 150; } public class Avalon : ICar { public string Make => "Toyota"; public int GetTopSpeed() => 130; } 

重要的是要考虑默认方法GetTopSpeed()仅对强制转换ICar的变量可用,而对没有重载的Avalon不可用。 这意味着,如果您正在使用接口,则此技术最有用(否则,您的代码将被大量强制转换为接口,以获得对方法的默认实现的访问权限)。

Mixins和特征(或类似的东西)


mixintraits的类似语言概念描述了通过合成扩展对象行为的方式,而无需多重继承。

Wikipedia报告了以下关于混合的信息:
Mixin也可以被视为默认方法的接口
听起来像那样吗?

但是,即使使用默认实现, C#中的接口也不是mixins。 不同之处在于它们还可以包含没有实现的方法,支持从其他接口的继承,可以被专门化(显然,这是指模板限制。-大约翻译) 。依此类推。 但是,如果我们创建一个仅包含具有默认实现的方法的接口,那么实际上它将是传统的mixin。

考虑以下代码,该代码向对象“运动”添加功能并跟踪其位置(例如,在游戏开发人员中):

 public interface IMovable { public (int, int) Location { get; set; } public int Angle { get; set; } public int Speed { get; set; } // ,         public void Move() => Location = ...; } public class Car : IMovable { public string Make => "Toyota"; } 

! 在开始编写本文并尝试编译示例之前,我没有注意到此代码中的问题。 接口(即使具有默认实现的接口)也不能存储状态。 因此,接口不支持自动属性。 从文档中
接口无法存储实例状态。 尽管现在接口中允许使用静态字段,但仍然不能使用实例字段。 因此,您不能使用自动属性,因为它们隐式使用隐藏字段。
在此C#接口中,mixin的概念与之矛盾(据我所知,mixin可以在概念上存储状态),但是我们仍然可以实现最初的目标:

 public interface IMovable { public (int, int) Location { get; set; } public int Angle { get; set; } public int Speed { get; set; } // A method that changes location // using angle and speed public void Move() => Location = ...; } public class Car : IMovable { public string Make => "Toyota"; // ,         public (int, int) Location { get; set; } public int Angle { get; set; } public int Speed { get; set; } } 

因此,我们通过使Move()方法及其实现可用于实现IMovable接口的所有类来实现所需的功能。 当然,该类仍然需要为属性提供实现,但是至少它们是在IMovable接口中声明的,这允许Move()的默认实现与它们一起使用,并确保实现该接口的任何类都具有正确的状态。

作为更完整,更实际的示例,请考虑用于记录的混合:

 public interface ILogger { public void LogInfo(string message) => LoggerFactory .GetLogger(this.GetType().Name) .LogInfo(message); } public static class LoggerFactory { public static ILogger GetLogger(string name) => new ConsoleLogger(name); } public class ConsoleLogger : ILogger { private readonly string _name; public ConsoleLogger(string name) { _name = name ?? throw new ArgumentNullException(nameof(name)); } public void LogInfo(string message) => Console.WriteLine($"[INFO] {_name}: {message}"); } 

现在,在任何类中,我都可以从ILogger接口继承:

 public class Foo : ILogger { public void DoSomething() { ((ILogger)this).LogInfo("Woot!"); } } 

这样的代码:

 Foo foo = new Foo(); foo.DoSomething(); 

输出:

 [INFO] Foo: Woot! 

替换扩展方法


我发现最有用的应用程序是替换大量的扩展方法。 让我们回到一个简单的日志记录示例:

 public interface ILogger { void Log(string level, string message); } 

通常,在接口实现默认实现之前,我将为此接口编写很多扩展方法,以便在继承的类中,您仅需要实现一个方法,因此用户可以访问许多扩展:

 public static class ILoggerExtensions { public static void LogInfo(this ILogger logger, string message) => logger.Log("INFO", message); public static void LogInfo(this ILogger logger, int id, string message) => logger.Log("INFO", $"[{id}] message"); public static void LogError(this ILogger logger, string message) => logger.Log("ERROR", message); public static void LogError(this ILogger logger, int id, string message) => logger.Log("ERROR", $"[{id}] {message}"); public static void LogError(this ILogger logger, Exception ex) => logger.Log("ERROR", ex.Message); public static void LogError(this ILogger logger, int id, Exception ex) => logger.Log("ERROR", $"[{id}] {ex.Message}"); } 

这种方法效果很好,但并非没有缺陷。 例如,带有扩展名和接口的类名称空间不一定匹配。 加上参数和指向记录器实例的链接形式的烦人的视觉噪声:

 this ILogger logger logger.Log 

现在,我可以将扩展替换为默认实现:

 public interface ILogger { void Log(string level, string message); public void LogInfo(string message) => Log("INFO", message); public void LogInfo(int id, string message) => Log("INFO", $"[{id}] message"); public void LogError(string message) => Log("ERROR", message); public void LogError(int id, string message) => Log("ERROR", $"[{id}] {message}"); public void LogError(Exception ex) => Log("ERROR", ex.Message); public void LogError(int id, Exception ex) => Log("ERROR", $"[{id}] {ex.Message}"); } 

我发现此实现更简洁,更易于阅读(和支持)。

与扩展方法相比,使用默认实现还具有更多优点:

  • 可以用这个
  • 您不仅可以提供方法,还可以提供其他元素:例如,索引器
  • 默认实现可能会重载以阐明行为。

上面的代码使我感到困惑的是,哪个接口成员具有默认实现以及哪些是继承类应实现的合同的一部分,这一点并不清楚。 分开两个块的注释可能会有所帮助,但是我喜欢在这方面严格扩展方法。

为了解决这个问题,我开始将具有默认实现的成员的接口声明为部分接口 (也许非常简单的接口除外)。 然后,将默认实现放在一个单独的文件中,命名约定的形式为“ ILogger.LogInfoDefaults.cs”“ ILogger.LogErrorDefaults.cs”等。 如果默认实现很少,并且不需要其他分组,那么我将文件命名为“ ILogger.Defaults.cs”

这将具有默认实现的成员与不可实现的契约分开,不可实现的契约需要继承的类来实现。 此外,它还允许您剪切很长的文件。 在任何格式的项目中呈现ASP.NET样式的附加文件还有一个棘手的技巧。 为此,将其添加到项目文件或Directory.Build.props中

 <ItemGroup> <ProjectCapability Include="DynamicDependentFile"/> <ProjectCapability Include="DynamicFileNesting"/> </ItemGroup> 

现在,您可以在解决方案资源管理器中选择“文件嵌套” ,所有.Defaults.cs文件将作为“主”界面文件的后代出现。

总之,在几种情况下,首选扩展方法:

  • 如果您通常使用类,而不使用接口(因为必须将对象强制转换为接口以访问默认实现)
  • 如果您经常使用带有模板的扩展名: public static T SomeExt < T >(此T foo)(例如,在Fluent API中

Source: https://habr.com/ru/post/zh-CN467949/


All Articles