介绍
【五分钟的dotnet】是一个利用您的碎片化时间来学习和丰富.net知识的博文系列。它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的.net知识等等。
5min+不是超过5分钟的意思,"+"是知识的增加。so,它是让您花费5分钟以下的时间来提升您的知识储备量。
正文
伴随着 .NET Core 3.0 一起发布的 C# 8 ,从发布至今已经过了快大半年了。如果您细心的话,就能发现在C# 8新增的功能中有一条:“默认接口方法” 。半年前当我看到这一新特性的时候,我惊呆了,但是惊讶之余是更多的疑惑。因为对于接口这个东西来说,从C#发布至今的十多年里几乎一直保持它的样子,然而在C# 8之后,它有了巨大的变化。随着而来,也是各种争论的声音。
很早之前我就想写这篇文章了,但是由于各种原因一直拖延到了现在。
先让我们来回顾一下 C# 中原有的接口有什么特点:
接口类似于只有抽象成员的抽象基类。实现接口的任何类或结构都必须实现其所有成员。
接口无法直接进行实例化。其成员由实现接口的任何类或结构来实现。
接口可以包含事件、索引器、方法和属性。
接口不含方法的实现。
一个类或结构可以实现多个接口。一个类可以继承一个基类,还可实现一个或多个接口。
也正是基于这些特点,当我们在接口中为一个方法加上"pulic"等关键字的时候,编译器会提示我们这是一个错误的写法:
复制代码
interface IRepository
{//Compile-time error CS0106 The modifier 'public' is not valid for this item.public void Add();
}
所以更不用谈给方法写一个实现了。这就让它和 C# 中的另外一种事物行成了鲜明的对比,是的,抽象类。不知道大家有没有在各种面试中遇到过这样的提问:“接口能有任何的访问修饰符吗?”,“接口和抽象类的区别是什么?”
曾经您可以和自然的脱口而出答案:“没有修饰符。一个可以有默认方法,一个只能申明方法…………”。但是从现在开始:这些答案是错的了。????
这是微软MSDN中的设计规范截图:
上面的图是我半年前截的图,今天本来想去找对应的链接分享出来,但是发现找不到了。可能…………
新的接口
好了,说了那么多,我们来看看C# 8 为我们改变后的接口是什么样子:
复制代码
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!
这是我在网上摘取的一部分代码。是的,您没有看错,接口可以实现方法了。并且还可以给它添加上访问修饰符:
复制代码
interface IDemoInterface
{public static int staticIntValue = 123; //Rightpublic void PulicMethod(){ } //Right
}
就像您所见的一样,它还可以在内部声明静态的数据。
但是下面的写法依旧会提示错误哦:
复制代码
interface IDemoInterface
{abstract void M1() { } //Error. 因为有abstractabstract private void M2() { } //Errorabstract static void M3() { } //Errorstatic extern void M4() { } //Error.因为有extern
}
争议点
走到这里,也许您会说:“这不挺好的吗?好像对我也没有啥影响。” 确实,假如您不更改接口的签名,无论您是否在接口中增加默认实现还是某些静态数据都不会对已有的应用程序造成任何错误。
但是如果您经常使用抽象类的话,您就会发现,这样的接口是不是和抽象类太像了?甚至有点完全掩盖了抽象类的优势。
当我半年前看到这一新特性时,我就产生了这样的疑惑。这个 “默认方法实现” 的新特性,真的需要吗?如果需要,那我如何选择它和抽象类?
结果我发现,大家都对这一特性产生了困惑:
于时,我抱着怀疑的态度在网上到处搜索答案。最后在C# 官方团队的笔记中我看到了这样一句话:
这句话的意思大致是:我们应该更深入地研究Java在这里所做的事情,Java对接口的实现很好,我们应该…………(有关该说明的github链接可以点击这里)。
我当时心就凉了半截。不过缓了缓,我镇定的思考了一下:好的语言设计被借鉴和参考也是很有必要的。比如现在其它语言都在借鉴C#的await和async。(PS:C#和Typescript怎么越来越像????)。
那么我们真的需要在接口中提供默认实现吗?那什么情况下我需要这样做?毕竟咱们使用了 C# 这么多年,就算接口没有提供默认实现也能设计出很好的系统来。所以为了解决上面的疑问,还是得回到接口和抽象类的本质。
按照咱们以往使用接口和抽象类的情况来看:接口表示的是一种行为,"who can"(比如鸟会飞),而基类表示的是一种类别,"is a"(比如麻雀是鸟)。因此在OOP的世界中,如果咱们细心的来建模的话,我们会把表示行为的共性抽象为一个接口:比如鸟会飞,咱们可以抽象一个IFly的接口。对老版本的 C# 来说,不能提供方法的实现,所以只会有一个Fly() 的方法签名。而现在我们通过新的特性,我们可以给“飞”这个动作提供一个默认的实现,比如 90%的鸟都是“煽动翅膀起飞”,则我们可以将这个大部分 的操作作为默认实现,而对那些10%的 “小众” 进行重写。也正是由于接口更关注的是“行为”,所以接口中不能存在“状态”,因此您会发现虽然可以声明字段了,但是只能声明静态字段。而实例化的状态信息依旧只能通过抽象类来实现。
当然,在现在接口和抽象类建模比较模糊的今天,从技术的实现上来说,其实接口的默认实现并没有带来很多技术编码上的好处。但是如果您坚持好的规范抽象,比如接口开头就是用“I”,将对象的行为进行抽象提升为接口,也许某一刻您会感受到该特性带来的改变。