C# 中的显式接口与隐式接口?






4.19/5 (37投票s)
在本文中,我们将讨论显式接口,它们与隐式接口有何不同,以及为什么应该避免使用它们。
目录
引言
在本文中,我们将讨论显式接口,它们与隐式接口有何不同,以及为什么应该避免使用它们。
什么是显式和隐式接口?
实现 C# 接口有两种方式:“显式”和“隐式”。
当你隐式实现一个接口时,代码如下所示。“IDal”是接口,“Add”和“Update”是隐式实现的方法。大多数开发者都使用隐式方式实现接口。
public class Person : IDal
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void Add()
{
throw new NotImplementedException();
}
public void Update()
{
throw new NotImplementedException();
}
}
如果你显式实现一个接口,代码如下所示。区别在于“Add”和“Update”方法现在是私有的。
public class Person : IDal
{
public string FirstName { get; set; }
public string LastName { get; set; }
void IDal.Add()
{
throw new NotImplementedException();
}
void IDal.Update()
{
throw new NotImplementedException();
}
}
简单来说,“隐式”接口和“显式”接口的区别在于,隐式实现中接口方法是公开的,而显式实现中方法是私有的。
那么接下来的问题是,为什么会有这种公开与私有的区别,以及我们应该在什么时候使用“隐式”接口,什么时候使用“显式”接口。
我们什么时候需要显式接口?
让我在这里声明一点:-
当你的具体类的抽象不包含接口的抽象时,应该使用“显式”接口。这是一种实现双重抽象的快速而粗糙的方法。
我能感觉到理解上面这句话有多难。
让我们放慢节奏,循序渐进,首先来理解“抽象”这个词。
“抽象”意味着从客户端的角度只显示你的类必要的方法/属性。
如果你对“抽象”这个词不熟悉,我鼓励你观看这个C# 抽象概念的油管视频。
下面是一个简单的“Person”类,包含一些属性和方法。
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void Add()
{
}
public void Update()
{
}
}
现在这个“Person”类(也就是一个库)被两个客户端使用:
“PersonalInformation”和“DataAccessBatchProcess”。
“PersonalInformation”是一个简单的 UI 应用程序,用于处理“Person”信息;“DataBatchProcess”是一个简单的批处理程序,将这些数据添加到数据库中。
在这种情况下,“Person”是库,“DataAccessBatchProcess”和“PersonalInformation”应用程序是这个库的消费者。
让我来谈谈最佳实践。
一个好的软件架构的标志之一是,你会看到它非常尊重根据客户端配置文件进行的“抽象”。
在这个特定场景中,“Personal Information”应用程序只想看到“FirstName”和“LastName”,而数据访问批处理程序只想看到“Add”和“Update”方法。换句话说,同一个类我们有两种不同的抽象。
一个好的架构能以优雅的方式应对变化。在好的架构中,当任何变更请求被执行时,代码的改动应该最少。
只有当抽象被正确实现时,才能做到最少的代码改动。
因为通过抽象,每个消费者只能看到必要的东西,系统中不必要/不相关的变更不会影响到它们。
所以第一步是创建一个只包含“Add”和“Update”方法的 Dal 接口。
public interface IDal
{
void Add();
void Update();
}
然后我们隐式地实现“IDal”接口。
public class Person : IDal
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void Add()
{
// Here goes the data access layer add code
}
public void Update()
{
// Here goes the data access layer update code
}
}
所以现在当我们从“IDal”接口的角度来看“Person”类时,我们只看到“Add”和“Update”方法。对于数据访问层的批处理程序来说,后台是什么业务对象并不重要。
它可以是任何类型的业务对象——person、customer、account——对它来说重要的是“Update”和“Add”方法。
但现在让我们测试一下“Personal Information”应用程序的抽象是否得到了尊重。
你可以看到,当我们创建“Person”类的对象时,我们也能看到“Update”和“Add”方法。换句话说,“Personal Information”应用程序看到了不必要的东西。
所以如果将来对“Update”和“Add”方法进行任何更改,它们不应该不必要地影响到“Personal Information”应用程序。
一个糟糕架构的标志是,当一个变更请求被执行时,不相关的部分也发生了变化。
因此,解决这个问题的快速而粗糙的方法是使用“显式”接口。请注意,“快速而粗糙的方法”这个词被加粗了,我们很快会讨论它。
所以如果我们“显式地”实现“IDal”接口,代码会是下面这个样子。
最重要的一点是,“Add”和“Update”方法变成了私有的。
public class Person : IDal
{
public string FirstName { get; set; }
public string LastName { get; set; }
void IDal.Add()
{
// Add code goes here
}
void IDal.Update()
{
// Update code goes here.
}
}
现在如果我们创建“Person”对象,我们就看不到“Update”和“Add”方法了,这样我们就尊重了“Personal Information”应用程序的抽象。
用数学方式总结一下JJ
类的公共成员 = (类的公共成员 + 隐式接口成员) - 显式接口成员
更清晰的方法:- ISP(接口隔离原则)
上述方法是为类实现多种抽象的一种快速而粗糙的方式。应尽可能避免使用显式接口。
正确的方式是遵循 SOLID 原则并实现 ISP(接口隔离原则)。如果你对 SOLID 原则不熟悉,请阅读这篇文章使用 C# 理解 SOLID 原则。
所以,为 Dal 和 Person 创建两个不同的接口,这样各自的抽象会更加清晰。
子类无法访问显式实现的接口。
如果你显式地实现一个接口,其成员会变成私有的,你的子类就无法重写这些成员,从而破坏了整个继承体系。
在下面的例子中,我的“Person”类被“Employee”继承。但因为接口是显式实现的,“Add()”和“Update()”变成了私有方法,因此我失去了链式多态的好处。
public class Person : IDal
{
void IDal.Add()
{
throw new NotImplementedException();
}
void IDal.Update()
{
throw new NotImplementedException();
}
}
public class Employee : Person
{
}
使代码更难阅读
接口的重点在于它是一种契约。是两个类之间的契约。多年来,开发者们都知道契约通常是公开的。但在这种情况下,接口方法突然变成了私有的,这也会在可读性方面引起混淆。
隐藏不推荐的接口成员
我唯一觉得需要使用显式接口的时候是,当我想让所实现接口的某些方法是私有的,而另一些方法是公开的。
例如,在下面的类中,我正在实现“IEnumerable”接口,我希望客户端能够访问“GetEnumerator”,但不能访问“Enumerable”。
public class Person : IEnumerable<string>
{
public IEnumerator<string> GetEnumerator()
{
throw new NotImplementedException();
}
System.Collections.IEnumeratorSystem.Collections.IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
}
</string></string>
关于显式接口的误解
关于显式接口的一个误解是,当你需要实现来自不同接口的同名方法时,应该使用它。
观看这个视频来理解我说的“来自不同接口的同名方法”是什么意思:https://www.youtube.com/watch?v=6ZxVx93wa7Q。
如果你真的遇到了这种情况,那只能是因为设计糟糕。我不认为这是一个使用显式接口的有力理由。
关于显式接口的结论
- 应该避免使用显式接口,而应遵循 ISP 原则。换句话说,为不同的抽象创建不同的接口。
- 显式接口不能被派生类使用。
- 唯一应该使用它的情况是,出于某种原因,你想要实现接口,但又想通过类隐藏一些方法并显示另一些方法。
我理解我对显式接口做出了一个非常粗鲁的结论。我希望听取社区的意见,获取反馈并加以改进。
请在下面的评论区留下您的评论。
进一步阅读,请观看下面的面试准备视频和分步视频系列。