加入收藏 | 设为首页 | 会员中心 | 我要投稿 宁德站长网 (https://www.0593zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 教程 > 正文

C# 8中的默认接口技巧

发布时间:2021-11-12 16:32:06 所属栏目:教程 来源:互联网
导读:默认接口方法(也称为虚拟扩展方法)是C#8的一项新功能建议,开发人员可以像使用trait那样使用默认方法。trait是面向对象的编程技术,用于提升不相关类之间方法的重用性。 在这篇文章中,我将介绍这个新功能,包括新的C#语法,以及这个功能如何让你的代码更

默认接口方法(也称为虚拟扩展方法)是C#8的一项新功能建议,开发人员可以像使用trait那样使用默认方法。trait是面向对象的编程技术,用于提升不相关类之间方法的重用性。
 
在这篇文章中,我将介绍这个新功能,包括新的C#语法,以及这个功能如何让你的代码更加干净和紧凑。
 
默认方法带来的主要好处是,现在可以在不破坏实现类的情况下给接口添加默认方法。换句话说,这个特性让开发者可以选择是否要覆盖默认方法。
 
下面描述的日志记录示例是该功能的一个非常好的使用场景。ILogger接口有一个抽象的WriteLogCore方法。其他方法都是默认方法,如WriteError和WriteInformation,它们通过不同的参数调用WriteLogCore。ILogger实现类只需要实现WriteLogCore方法即可。
 
可以想象一下,你的继承类为此可以省去多少代码。不过,这个功能虽好,但也存在风险,因为它是一种多重继承。它也存在钻石继承问题,下面将作具体描述。另外,接口方法必须是没有状态的“纯行为”,这意味着接口仍然像过去一样不能直接引用其他字段。
 
接口语法已经经过扩展,可接受下面列出的新关键字。例如,你可以在接口中编写一个私有方法,代码仍然可以通过编译并正常工作。
方法体或索引器、属性、事件访问器
private、protected、internal、public、virtual、abstract、override、sealed、static、extern
静态字段
静态方法、属性、索引器和事件
具有默认访问权限的显式访问修饰符是public的
Override修饰符
不允许出现:
 
实例状态、实例字段、实例自动属性
默认接口方法示例
下面这个简单的例子演示了如何使用这一特性。
 
// ------------------------Default Interface Methods---------------------------
 
  interface IDefaultInterfaceMethod
  {
    public void DefaultMethod()
    {
      Console.WriteLine("I am a default method in the interface!");
    }
  }
 
  class AnyClass : IDefaultInterfaceMethod
  {
  }
 
  class Program
  {
    static void Main()
    {
      IDefaultInterfaceMethod anyClass = new AnyClass();
      anyClass.DefaultMethod();
    }
  }
控制台输出:
 
> I am a default method in the interface!
可以看到,接口提供了默认方法,实现类并不知道接口提供了默认方法,也不包含该接口方法的实现。
 
将IDefaultInterfaceMethod更改为AnyClass,如下所示:
 
AnyClass anyClass = new AnyClass();
  anyClass.DefaultMethod();
上面的代码会产生编译时错误:AnyClass不包含DefaultMethod。
 
这证明了实现类对默认方法一无所知。
 
C# 8中的默认接口方法
 
图1:在类上调用默认方法时的错误消息
 
要访问默认接口方法,必须将其转型成接口:
 
AnyClass anyClass = new AnyClass();
  ((IDefaultInterfaceMethod)anyClass).DefaultMethod();
控制台输出:
 
> I am a default method in the interface!
值得一提的是,相同的功能在Java中已经存在了很长时间,.NET团队已经将Java默认方法文档作为.NET Framework开发人员的参考,例如:
 
“我们应该更深入地了解Java在这方面所做的工作,他们肯定已经积累了很多这方面的见解。” —— C#语言设计笔记 2017年4月11日
 
接口中的修饰符
正如我之前提到的,接口语法现在可以接受以下关键字:protected、internal、public和virtual。默认情况下,默认接口方法是virtual的,除非使用了sealed或private修饰符。类似的,没有方法体的接口成员默认是abstract的。
 
例如:
 
  // ------------------------ Virtual and Abstract---------------------------
 
interface IDefaultInterfaceMethod
  {
    // By default, this method will be virtual, and the virtual keyword can be here used!
    virtual void DefaultMethod()
    {
      Console.WriteLine("I am a default method in the interface!");
    }
 
    // By default, this method will be abstract, and the abstract keyword can be here used
    abstract void Sum();
  }
 
  interface IOverrideDefaultInterfaceMethod : IDefaultInterfaceMethod
  {
    void IDefaultInterfaceMethod.DefaultMethod()
    {
      Console.WriteLine("I am an overridden default method!");
    }
  }
 
  class AnyClass : IDefaultInterfaceMethod, IOverrideDefaultInterfaceMethod
  {
    public void Sum()
    {
    }
  }
 
  class Program
  {
      static void Main()
    {
      IDefaultInterfaceMethod anyClass = new AnyClass();
      anyClass.DefaultMethod();
 
      IOverrideDefaultInterfaceMethod anyClassOverridden = new AnyClass();
      anyClassOverridden.DefaultMethod();
    }
  }
控制台输出:
 
> I am a default method in the interface!
> I am an overridden default method!
关键字virtual和abstract可以从接口中删除,不过删不删除其实对编译后的代码并没有任何影响。
 
注意:在覆盖的方法中不允许出现访问修饰符。
 
覆盖示例:
 
interface IOverrideDefaultInterfaceMethod : IDefaultInterfaceMethod
  {
    public void IDefaultInterfaceMethod.DefaultMethod()
    {
      Console.WriteLine("I am an overridden default method");
    }
  }
上面的代码会产生编译时错误:修饰符“public”在此处无效。
 
C# 8中的默认接口方法
 
图2:修改器在重写的方法中是不允许的
 
钻石继承问题
这个问题指的是因为允许多重继承而产生的模糊性。对于允许多重继承的语言(如C++)来说,这是一个很大的问题。然而,在C#中,类不允许多重继承,接口也只在有限的范围内进行多重继承,而且不包含状态。
 
C# 8中的默认接口方法
 
图3:钻石依赖关系
 
考虑以下情况:
 
// ------------------------Diamond inheritance and classes---------------------------
 
  interface A
  {
    void m();
  }
 
  interface B : A
  {
    void A.m() { System.Console.WriteLine("interface B"); }
  }
 
  interface C : A
  {
    void A.m() { System.Console.WriteLine("interface C"); }
  }
 
  class D : B, C
  {
    static void Main()
    {
      C c = new D();
      c.m();
    }
  }
上面的代码会产生编译时错误,如图4所示:
 
C# 8中的默认接口方法
 
图4:钻石问题的错误消息
 
.NET开发团队决定通过在运行时调用最具体的覆盖方法来解决钻石问题。
 
C# 8中的默认接口方法
 
“实现了接口成员的类应该总是胜过接口提供的默认实现,即使它是从基类继承的。只有当类没有提供具体的实现时,才考虑使用默认实现“
 
如果你想了解更多关于此问题的信息,可以参看提案:默认接口方法和C#语言设计笔记2017年4月19日。
 
回到我们的例子。问题是编译器无法推断出最具体的覆盖方法是哪个。不过,你可以像下面这样在类D中添加方法“m”,现在编译器就可以使用这个类实现来解决钻石问题。
 
  class D : B, C
  {
    // Now the compiler will use the most specific override, which is defined in the class ‘D’
    void A.m()
    {
       System.Console.WriteLine("I am in class D");
    }
 
    static void Main()
    {
      A a = new D();
      a.m();
    }
  }
控制台输出:
 
> I am in class D
this关键字
 
下面的例子演示了如何在接口中使用“this”关键字。
 
public interface IDefaultInterfaceWithThis
  {
    internal int this[int x]
    {
      get
      {
        System.Console.WriteLine(x);
        return x;
      }
      set
      {
        System.Console.WriteLine("SetX");
      }
    }
 
    void CallDefaultThis(int x)
    {
      this[0] = x;
    }
  }
 
  class DefaultMethodWithThis : IDefaultInterfaceWithThis
  {
  }
客户端代码:
 
  IDefaultInterfaceWithThis defaultMethodWithThis = new DefaultMethodWithThis();
  Console.WriteLine(defaultMethodWithThis[0]);  
  defaultMethodWithThis.CallDefaultThis(0);
控制台输出:
 
0
SetX
ILogger示例
ILogger接口是解释默认方法技术的最常用示例。在我的代码示例中,包含了一个名为“WriteCore”的抽象方法,其他方法都有一个默认的实现。ConsoleLogger和TraceLogger实现了ILogger接口。下面的这些代码非常紧凑和干净。在过去,一个类除非是抽象类,否则必须实现接口所有的方法,这可能导致很多重复代码。而使用新的方法,ConsoleLogger将能够继承另一个类层次结构,换句话说,默认方法将为你提供最灵活的设计。
 
  enum LogLevel
  {
    Information,
    Warning,
    Error
  }
 
  interface ILogger
  {
    void WriteCore(LogLevel level, string message);
 
    void WriteInformation(string message)
    {
      WriteCore(LogLevel.Information, message);
    }
 
    void WriteWarning(string message)
    {
      WriteCore(LogLevel.Warning, message);
    }
 
    void WriteError(string message)
    {
      WriteCore(LogLevel.Error, message);
    }
  }
 
  class ConsoleLogger : ILogger
  {
    public void WriteCore(LogLevel level, string message)
    {
      Console.WriteLine($"{level}: {message}");
    }
  }
 
  class TraceLogger : ILogger
  {
    public void WriteCore(LogLevel level, string message)
    {
      switch (level)
      {
        case LogLevel.Information:
          Trace.TraceInformation(message);
          break;
 
        case LogLevel.Warning:
          Trace.TraceWarning(message);
          break;
 
        case LogLevel.Error:
          Trace.TraceError(message);
          break;
      }
    }
  }
客户端代码:
 
      ILogger consoleLogger = new ConsoleLogger();
      consoleLogger.WriteWarning("Cool no code duplication!");  // Output: Warning: Cool no Code duplication!
 
      ILogger traceLogger = new TraceLogger();
      consoleLogger.WriteInformation("Cool no code duplication!");  // Cool no Code duplication!
Player示例
这是一款包含不同类型玩家的游戏。力量型玩家具有更大的攻击力,而限制型玩家具有更小的攻击力。
 
  public interface IPlayer
  {
    int Attack(int amount);
  }
 
  public interface IPowerPlayer: IPlayer
  {
    int IPlayer.Attack(int amount)
    {
      return amount + 50;
    }
  }
 
  public interface ILimitedPlayer: IPlayer
  {
    int IPlayer.Attack(int amount)
    {
      return amount + 10;
    }
  }
 
  public class WeakPlayer : ILimitedPlayer
  {
  }
 
  public class StrongPlayer : IPowerPlayer
  {
  }
客户端代码:
 
  IPlayer powerPlayer = new StrongPlayer();         
  Console.WriteLine(powerPlayer.Attack(5));  // Output 55
 
  IPlayer limitedPlayer = new WakePlayer();
  Console.WriteLine(limitedPlayer.Attack(5));  // Output 15
正如你在上面的代码示例中看到的那样,IPowerPlayer接口和ILimitedPlayer接口包含了默认实现。限制型玩家攻击力更小。如果我们定义一个新的类,例如SuperDuperPlayer(继承自StrongPlayer),那么新类会自动从接口中获得默认的强攻击力行为,如下所示。
 
  public class SuperDuperPlayer: StrongPlayer
  {
  }
 
  IPlayer superDuperPlayer = new SuperDuperPlayer();
  Console.WriteLine(superDuperPlayer.Attack(5)); // Output 55
Generic Filter示例
ApplyFilter是一个默认接口方法,它包含了一个应用在泛型类型上的Predicate。在我的例子中,使用了一个虚拟的过滤器来模拟行为。
 
  interface IGenericFilter<T>
  {
    IEnumerable<T> ApplyFilter(IEnumerable<T> collection, Func<T, bool> predicate)
    {
      foreach (var item in collection)
      {
        if (predicate(item))
        {
          yield return item;
        }
      }
    }
  }
 
  interface IDummyFilter<T> : IGenericFilter<T>
  {
    IEnumerable<T> IGenericFilter<T>.ApplyFilter(IEnumerable<T> collection, Func<T, bool> predicate)
    {
      return default;
    }
  }
 
  public class GenericFilterExample: IGenericFilter<int>, IDummyFilter<int>
  {
  }
客户端代码:
 
      IGenericFilter<int> genericFilter = new GenericFilterExample();
      var result = genericFilter.ApplyFilter(new Collection<int>() { 1, 2, 3 }, x => x > 1);
控制台输出:
 
2, 3
客户端代码:
 
      IDummyFilter<int> dummyFilter = new GenericFilterExample();
      var emptyResult = dummyFilter.ApplyFilter(new Collection<int>() { 1, 2, 3 }, x => x > 1);
控制台输出:
 
0
你可以将此通用过滤器概念应用在其他设计上。
 
限制
在接口中使用修饰符关键字时,首先需要了解一些限制和注意事项。在很多情况下,编译器会为我们检测常见错误(例如下面列出的错误)。
 
例如下面的代码:
 
  interface IAbstractInterface
  {
    abstract void M1() { }
    abstract private void M2() { }
    abstract static void M3() { }
    static extern void M4() { }
  }
 
  class TestMe : IAbstractInterface
  {
    void IAbstractInterface.M1() { }
    void IAbstractInterface.M2() { }
    void IAbstractInterface.M3() { }
    void IAbstractInterface.M4() { }
  }
上面的代码将产生下面列出的编译时错误:
 
error CS0500: 'IAbstractInterface.M1()' cannot declare a body because it is marked abstract
error CS0621: 'IAbstractInterface.M2()': virtual or abstract members cannot be private
error CS0112: A static member 'IAbstractInterface.M3()' cannot be marked as override, virtual, or abstract
error CS0179: 'IAbstractInterface.M4()' cannot be extern and declare a body
error CS0122: 'IAbstractInterface.M2()' is inaccessible due to its protection level
错误CS0500表示默认方法“IAbstractInterface.M3()”不能是抽象的,因为它有方法体。错误CS0621表示该方法不能是既是private又是abstract的。

(编辑:宁德站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    热点阅读