抽象类和接口:面试中的两个“大魔王”——第二部分






4.84/5 (44投票s)
本文是系列文章“抽象类与接口:面试中的两个‘恶魔’”的第二部分,重点讲解接口的重要知识点。
引言
在我第一篇文章中,我收到了很多反馈,有积极的,有消极的,也有中立的,但每个人都问的一个问题是:为什么我暗示它们是“恶魔”?我只是想澄清一下,我无意将它们描绘成恶魔。这些是编程中最重要的概念,没有它们,我们就无法获得更好的代码效率。
通常在每次面试中,第一个问题是:“抽象类和接口之间有什么区别?”而你的整个面试都取决于对这个问题的回答。“第一印象是最后的印象”。所以这些概念不是恶魔,但它们在面试中的作用确实如此。关于批评的声音就到这里,让我们继续这个系列的下一部分:接口
。
接口
是软件项目最重要的组成部分,没有它,项目就不完整。很多人可能不会同意我的看法并进行辩论。是的,是否使用接口
是一个有争议的问题。如果使用,那么如何使用?在这篇文章中,我将解释接口
的重要关键方面。就像我上一篇关于抽象类
的文章一样,我在这篇文章中采用了“是什么、为什么和如何”的方法,并试图用最简单的方式来解释它。
路线图
如果您是第一次阅读本文,我请求您先阅读本系列的第一部分
,然后再返回阅读本文。以下是题为“抽象类与接口:面试中的两个‘恶魔’”的系列文章的路线图:
接口
什么是接口?
在现实世界中,接口
意味着一种与某物交互的媒介。确切地说,它是两个系统、主体、组织相遇并交互的点。交互需要遵循一些规则。假设您正在参加程序员职位的面试。只有当面试官和你讲同一种语言时,面试才可能进行。此外,您和面试官拥有相同的编程语言技能集可以讨论。
同样,在编程世界中,接口
意味着一个用于与多个代码模块交互的约定。如果一个类
想与一个接口
通信,它必须实现该接口并定义其成员。把它想象成面试官的问题,如果你想得到这份工作,你需要正确地回答它。
MSDN 库将接口
定义为纯粹的抽象
类。接口
只包含方法、属性、事件或索引器的签名。它本身没有任何实现,只能由类或结构实现。任何实现接口
的两个者都必须提供接口中指定的成员的定义。它就像一个供所有派生类遵守的约定。
接口
使用关键字“interface”
声明。接口成员隐式地是public
和abstract
的,因此我们不能为其添加任何访问修饰符。接口
不能包含字段、常量成员、构造函数、析构函数和static
成员。
我们为什么需要接口?
接口
不是类。它只包含方法签名。它本身没有任何实现,也不能被实例化。其实现逻辑由派生自它的类提供。接口
通常被认为是纯粹的抽象
类。然而,与抽象
类相比,使用接口
的一个优点是“多重继承支持”。在 C# 中,同一个派生类不能继承两个类(抽象类或具体类)。如果两个类具有相同的方法签名,这会在派生类中引起歧义。我们可以使用接口在 C# 中实现多重继承。
接口
在面向服务架构 (SOA) 中起着至关重要的作用。在 WCF 中,我们使用接口来定义服务契约。一个类也可以用来定义服务契约
,而不是接口,但我们无法通过类实现更好的功能。使用接口,单个类可以实现任意数量的服务契约接口。通常,使用接口
作为服务契约
而不是实际类被认为是最佳实践。
大多数设计模式和原则都基于接口
而不是类继承。一些例子是建造者设计模式、工厂模式、接口隔离原则等等。
如何定义接口?
假设我们需要定义一个智能手机
类。该类可以包含操作系统
、应用商店
和通话
等成员。智能手机可以是基于 Android 的,也可以是基于 iOS 的,但不能同时是两者。Android 和 iOS 智能手机之间没有共同的功能,所以我们不需要提供任何默认功能。一种方法是将Smartphone
类设为abstract
,并将其所有成员也设为abstract
。这种方法效果很好,并且像Samsung
、Apple
、HTC
这样的具体类可以从中继承。
现在,几天后,Apple
想为其智能手机添加 Touch ID 功能。我们可以将TouchID
作为SmartPhone
抽象
基类中的一个抽象
方法。但是,如果HTC
和Samsung
都不想要这个功能呢?所以,TouchID
方法不能放在SmartPhone
抽象
类中。另一种方法是定义另一个抽象
类Features
,并将TouchID
方法添加到其中。这也糟糕,因为 C# 不支持将多个类(抽象
或具体)继承到派生类中。
在这种情况下,接口
很有用,并在解决问题中起着至关重要的作用。接口
只提供方法定义,就像抽象
类一样,但可用于多重继承。我们可以将Features
类变成一个接口,并将TouchID
方法添加到其中。它只提供方法签名,继承它的任何类都可以以自己的方式实现它。一个类继承多个接口在 C# 中也是完全有效的。同样,我们可以将SmartPhone
类设为接口而不是抽象
类。与创建纯粹的抽象
类相比,使用接口会更好。
注意:这个例子不是最好的,但我认为它能说明问题。它只是为了理解接口。
让我们考虑上面讨论的例子,并创建一个控制台应用程序。打开 Visual Studio 并添加一个名为“InterfaceDemo”
的新控制台项目。
默认情况下,它会提供一个名为Program
的类,其中包含一个用于代码执行的Main
方法。让我们创建一个抽象
类SmartPhone
,并在其中定义OS
和AppStore
抽象
方法。我们可以通过在类定义前加上“abstract
”关键字来创建抽象
类。如果您不熟悉抽象
类,请参阅本系列第一部分
。
using System;
namespace InterfaceDemo
{
//Abstract Class SmartPhone with only abstract methods in it
abstract class SmartPhone
{
public abstract void OS();
public abstract void AppStore();
}
class Program
{
static void Main(string[] args)
{
}
}
}
现在定义继承自SmartPhone
的具体类Apple
和Samsung
,并为OS
和AppStore
抽象
方法提供定义。
using System;
namespace InterfaceDemo
{
//Abstract Class SmartPhone with only abstract methods in it
abstract class SmartPhone
{
public abstract void OS();
public abstract void AppStore();
}
class Apple : SmartPhone
{
public override void OS()
{
//Some Implementation Here
}
public override void AppStore()
{
//Some Implementation Here
}
}
class Samsung : SmartPhone
{
public override void OS()
{
//Some Implementation Here
}
public override void AppStore()
{
//Some Implementation Here
}
}
class Program
{
static void Main(string[] args)
{
}
}
}
如果我们现在编译代码,它会正常工作。我们的SmartPhone
类由Apple
和Samsung
这两个不同的具体类实现,并根据它们进行定义。现在,假设Apple
想提供 Touch ID 功能给它的智能手机。我们可以在SmartPhone
类中添加另一个抽象
方法TouchID
,然后让Apple
继承并实现它。
using System;
namespace InterfaceDemo
{
//Abstract Class SmartPhone
abstract class SmartPhone
{
public abstract void OS();
public abstract void AppStore();
//TouchID method meant only for Apple Class
public abstract void TouchID();
}
class Apple : SmartPhone
{
public override void OS()
{
//Some Implementation Here
}
public override void AppStore()
{
//Some Implementation Here
}
//Implementing the TouchID feature
public override void TouchID()
{
//Some Implementation Here
}
}
class Samsung : SmartPhone
{
public override void OS()
{
//Some Implementation Here
}
public override void AppStore()
{
//Some Implementation Here
}
}
class Program
{
static void Main(string[] args) { }
}
}
Apple
类继承了TouchID
方法并为其提供了定义。现在让我们编译代码,看看会发生什么。
它抛出一个错误,说Samsung
类没有实现TouchID
方法。根据抽象
类的定义,任何实现它的类都必须为其所有抽象
成员提供定义。TouchID
方法仅用于Apple
类,而Samsung
类不想实现它。这显然表明我们的方法是错误的,因为TouchID
方法不能放在SmartPhone
抽象
类中。
另一种方法是定义另一个抽象
类Features
,并将TouchID
方法定义在其中。这种方法看起来不错,因为任何继承Features
的类都可以实现TouchID
方法。
using System;
namespace InterfaceDemo
{
//Abstract Class SmartPhone
abstract class SmartPhone
{
public abstract void OS();
public abstract void AppStore();
}
//Abstract Class Features for TouchID method
abstract class Features
{
public abstract void TouchID();
}
//Apple Class inherits both SmartPhone and Features
class Apple : SmartPhone, Features
{
public override void OS()
{
//Some Implementation Here
}
public override void AppStore()
{
//Some Implementation Here
}
//Implementation of TouchID method in Apple Class
public override void TouchID()
{
//Some Implementation Here
}
}
class Samsung : SmartPhone
{
public override void OS()
{
//Some Implementation Here
}
public override void AppStore()
{
//Some Implementation Here
}
}
class Program
{
static void Main(string[] args)
{
}
}
}
让我们编译代码,看看会发生什么。
它再次抛出一个错误,说我们不能在派生类中有多个基类。这就是所谓的类多重继承
,在 C# 中是不允许的。所以,我们的第二种方法在实现TouchID
方法时也失败了。这就是接口有用武之地,并有助于解决 C# 中的“多重继承”问题。我们可以将SmartPhone
和Features
都定义为接口,并让类根据需要实现它们。一个类也可以拥有多个接口。这是在 C# 中实现多重继承的唯一方法。
让我们使用接口
重新创建相同的项目。我们可以使用关键字“interface”
来创建接口。通常的做法是在接口名称前加上“I”
前缀,但这观点有争议,选择权在于您。
using System;
namespace InterfaceDemo
{
interface ISmartPhone //Definition of Interface
{
public void OS();
public void AppStore();
}
class Program
{
static void Main(string[] args)
{
}
}
}
我们定义了接口ISmartPhone
,其中包含方法签名OS
和AppStore
。如果我们现在编译代码,它会立即抛出一个错误。
它说我们不能在方法签名前面加上public
修饰符。实际上,接口方法不允许任何访问修饰符。在 C# 中,接口方法隐式地是public
的,因为接口
是一个供其他类使用的约定。此外,当我们为接口成员提供实现时,我们必须在派生类中将这些方法声明为public
。同样,我们也不能将这些方法声明为static
。
using System;
namespace InterfaceDemo
{
interface ISmartPhone //Definition of Interface
{
static void OS();
static void AppStore();
}
class Program
{
static void Main(string[] args)
{
}
}
}
如果我们编译代码,它又会给我们一个错误。
让我们不带任何访问修饰符定义接口方法,并创建一个继承ISmartPhone
接口的具体类Apple
,并为其成员提供定义。
using System;
namespace InterfaceDemo
{
interface ISmartPhone
{
void OS();
void AppStore();
}
class Apple : ISmartPhone
{
//OS Method Implementation
public void OS()
{
Console.WriteLine("OS Method: The OS of this Smartphone is iOS8");
}
//AppStore Method Implementation
public void AppStore()
{
Console.WriteLine("AppStore Method: The Application Store of this Smartphone is iTunes");
}
}
class Program
{
static void Main(string[] args)
{
}
}
}
这里应该注意的一个重要点是,每当我们为派生类中的接口成员提供实现时,访问修饰符必须始终是public
,否则会抛出错误。如果我们使用protected
修饰符而不是public
来修饰OS
方法,编译器会抛出错误。
using System;
namespace InterfaceDemo
{
interface ISmartPhone
{
void OS();
void AppStore();
}
class Apple : ISmartPhone
{
//OS Method kept as Protected
protected void OS()
{
Console.WriteLine("OS Method: The OS of this Smartphone is iOS8");
}
//AppStore Method Implementation
public void AppStore()
{
Console.WriteLine("AppStore Method: The Application Store of this Smartphone is iTunes");
}
}
class Program
{
static void Main(string[] args) { }
}
}
在上面的代码中,我将OS
方法的访问修饰符从public
改为了protected
。让我们编译代码,看看会发生什么。
是的,它抛出了一个错误,说Apple
类不能实现OS
方法,因为它不是public
的。所以,在派生类中始终将方法实现设为public
。我们可以定义另一个具体类Samsung
,它也实现ISmartPhone
接口并为其成员提供定义。
using System;
namespace InterfaceDemo
{
interface ISmartPhone
{
void OS();
void AppStore();
}
class Apple : ISmartPhone
{
//OS Method Implementation
public void OS()
{
Console.WriteLine("OS Method: The OS of this Smartphone is iOS8");
}
//AppStore Method Implementation
public void AppStore()
{
Console.WriteLine("AppStore Method: The Application Store of this smartphone is iTunes");
}
}
class Samsung : ISmartPhone
{
//OS Method Implementation
public void OS()
{
Console.WriteLine("OS Method: The OS of this smartphone is Android");
}
//AppStore Method Implementation
public void AppStore()
{
Console.WriteLine("AppStore Method: The Application Store of this smartphone is Google Play");
}
}
class Program
{
static void Main(string[] args) { }
}
}
这段代码运行良好,因为各种具体类实现了接口,并以自己的方式为成员提供了定义。现在,如果Apple
类想实现TouchID
功能,只需定义另一个接口IFeatures
即可轻松实现。Apple
类可以简单地继承该接口,并将其TouchID
功能添加到其类中。这就是接口比抽象类
有用的情况。
using System;
namespace InterfaceDemo
{
interface ISmartPhone
{
void OS();
void AppStore();
}
//New Interface meant only for Apple Class
interface IFeatures
{
void TouchID();
}
class Apple : ISmartPhone, IFeatures
{
//OS Method Implementation
public void OS()
{
Console.WriteLine("OS Method: The OS of this smartphone is iOS8");
}
//AppStore Method Implementation
public void AppStore()
{
Console.WriteLine("AppStore Method: The Application Store of this smartphone is iTunes");
}
//TouchID Method Implementation
public void TouchID()
{
Console.WriteLine("TouchID Method: This method provides Touch/Gesture control features.");
}
}
class Samsung : ISmartPhone
{
//OS Method Implementation
public void OS()
{
Console.WriteLine("OS Method: The OS of this smartphone is Android");
}
//AppStore Method Implementation
public void AppStore()
{
Console.WriteLine
("AppStore Method: The Application Store of this smartphone is Google Play");
}
}
class Program
{
static void Main(string[] args) { }
}
}
这样,我们就可以在 C# 中实现多重继承了。让我们创建Apple
和Samsung
具体类的对象并构建项目。
using System;
namespace InterfaceDemo
{
interface ISmartPhone
{
void OS();
void AppStore();
}
//New Interface meant only for Apple Class
interface IFeatures
{
void TouchID();
}
class Apple : ISmartPhone, IFeatures
{
//OS Method Implementation
public void OS()
{
Console.WriteLine("OS Method: The OS of this smartphone is iOS8");
}
//AppStore Method Implementation
public void AppStore()
{
Console.WriteLine("AppStore Method: The Application Store of this smartphone is iTunes");
}
//TouchID Method Implementation
public void TouchID()
{
Console.WriteLine("TouchID Method: This method provides Touch/Gesture Control features.");
}
}
class Samsung : ISmartPhone
{
//OS Method Implementation
public void OS()
{
Console.WriteLine("OS Method: The OS of this smartphone is Android");
}
//AppStore Method Implementation
public void AppStore()
{
Console.WriteLine
("AppStore Method: The Application Store of this smartphone is Google Play");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("//////////////////// - Interface Demo - //////////////////// \n");
Console.WriteLine("Apple SmartPhone:");
Apple apple = new Apple();
apple.OS();
apple.AppStore();
apple.TouchID();
Console.WriteLine("\n\n");
Console.WriteLine("Samsung SmartPhone:");
Samsung samsung = new Samsung();
samsung.OS();
samsung.AppStore();
Console.ReadKey();
}
}
}
如果我们现在运行代码,它会完美运行。
这是使用接口
的最简单示例。然而,这只是一个现实世界的类比,并且这种方法可能存在争议。我在这里演示的目的是让初学者了解如何使用接口
。以下是使用接口时应牢记的关键点:
要点
- 接口引用变量:
接口
没有实现,也不能被实例化。但是,它可以引用实现它的类对象。需要注意的是,该对象只能访问接口的继承成员。考虑以下代码using System; namespace InterfaceDemo { interface IDriveable { void Drive(); } class Car : IDriveable { public void Drive() { Console.WriteLine("Car Class: I can drive a Car."); } } class Truck : IDriveable { public void Drive() { Console.WriteLine("Truck Class: I can drive a Truck."); } } class Program { static void Main(string[] args) { Console.WriteLine("//////////////////// - Interface Demo - //////////////////// \n"); IDriveable DriveCar = new Car(); IDriveable DriveTruck = new Truck(); DriveCar.Drive(); //Calls Car's Drive() method DriveTruck.Drive(); //Calls Truck's Drive() method Console.ReadKey(); } } }
代码显示了具有相同
接口
引用但具有不同功能的对象的声明。 - 显式接口实现:在使用
接口
时,可能会出现这种情况:一个类实现了两个接口
,并且这两个接口
都包含一个具有相同签名的成员。当类为接口成员提供定义时,它会混淆哪个成员获得定义,因为两者名称相同。在这种情况下,我们将使用显式接口实现
。假设我们有两个接口ICredtCard
和IDebitCard
,并且这两个接口都具有相同的CardNumber
方法签名,并且Customer
类实现了这两个接口。using System; namespace InterfaceDemo { //First Interface IDebitCard interface IDebitCard { void CardNumber(); } //Second Interface ICreditCard interface ICreditCard { void CardNumber(); } //Customer Class implementing both the Interfaces class Customer : IDebitCard, ICreditCard { } class Program { static void Main(string[] args) { } } }
有两种方法可以为派生类中的
接口
成员提供方法定义。如果您右键单击接口
名称,Visual Studio 会提供两个选项来实现它们。如果我们正常实现
接口
并为CardNumber
方法提供定义,它将导致两个接口都使用CardNumber
作为它们的实现。我们不能为接口成员提供不同的功能。using System; namespace InterfaceDemo { //First Interface IDebitCard interface IDebitCard { void CardNumber(); } //Second Interface ICreditCard interface ICreditCard { void CardNumber(); } //Customer Class implements both the interfaces class Customer : IDebitCard, ICreditCard { public void CardNumber() { Console.WriteLine("Card Number: My Card Number is 12345678901234567890"); } } class Program { static void Main(string[] args) { } } }
如果我们现在编译程序,输出会引起更多混淆,因为我们无法确定实现了哪个接口方法,因为两个
接口
都共享CardNumber
作为它们的方法。在这种情况下,我们需要通过在派生类中使用显式实现
来告诉编译器哪个方法特定于哪个接口。这可以通过在派生类的方法定义前加上接口
名称来完成。需要注意的是,显式接口定义自动是public
的,因此方法定义不允许使用访问修饰符。我们仍然可以在其中包含共享的方法定义。using System; namespace InterfaceDemo { //First Interface IDebitCard interface IDebitCard { void CardNumber(); } //Second Interface ICreditCard interface ICreditCard { void CardNumber(); } //Customer Class implements both the interfaces class Customer : IDebitCard, ICreditCard { void IDebitCard.CardNumber() { Console.WriteLine("Debit Card Number: My Card Number is 12345XXXXX"); } void ICreditCard.CardNumber() { Console.WriteLine("Credit Card Number: My Card Number is 98999XXXXX"); } public void CardNumber() { Console.WriteLine("Customer ID Number: My ID Number is 54545XXXXX"); } } class Program { static void Main(string[] args) { Console.WriteLine("////////////////////- Implicit and Expliction Implementation -//////////////////// \n\n"); Customer customer = new Customer(); IDebitCard DebitCard = new Customer(); ICreditCard CreditCard = new Customer(); customer.CardNumber(); DebitCard.CardNumber(); CreditCard.CardNumber(); Console.ReadKey(); } } }
如果我们现在运行程序,我们可以通过显式实现来区分成员。
- 如果您有一些默认功能要在继承层次结构中的类之间共享,您可以使用
抽象类
。但是,如果您没有任何要共享的默认实现,而只是需要为派生类定义约定;接口
是最优选的选择。 - 这是一个使用
接口
的标准规则,请确保您第一次就做对了。一旦接口
被派生类实现,就很难更新或修改接口
,因为其他人的代码都会出错。
结论
我希望本文能帮助您理解接口
的各种可能性。您的反馈和建设性批评一直受到赞赏,请继续提出。在此之前,请努力在宇宙中留下印记。