在 C# 中隐式和显式实现接口有什么区别?
什么时候应该使用隐式,什么时候应该使用显式?
两者有什么优点和/或缺点吗?
Microsoft 的官方指南(来自第一版框架设计指南)指出不建议使用显式实现,因为它会给代码带来意想不到的行为。
我认为这个指导方针在 IoC 之前的时代非常有效,当你不把东西作为接口传递时。
任何人都可以触及这方面吗?
隐式是当您通过类中的成员定义接口时。显式是当您在接口上的类中定义方法时。我知道这听起来令人困惑,但我的意思是:IList.CopyTo
将隐式实现为:
public void CopyTo(Array array, int index)
{
throw new NotImplementedException();
}
并明确为:
void ICollection.CopyTo(Array array, int index)
{
throw new NotImplementedException();
}
不同之处在于隐式实现允许您通过您创建的类访问接口,方法是将接口转换为该类和接口本身。显式实现允许您仅通过将接口转换为接口本身来访问接口。
MyClass myClass = new MyClass(); // Declared as concrete class
myclass.CopyTo //invalid with explicit
((IList)myClass).CopyTo //valid with explicit.
我主要使用显式来保持实现干净,或者当我需要两个实现时。无论如何,我很少使用它。
我确信有更多理由使用/不使用其他人会发布的明确内容。
请参阅此线程中的下一篇文章,了解每个文章背后的出色推理。
隐式定义是直接将接口所需的方法/属性等添加到类中作为公共方法。
显式定义仅在您直接使用接口而不是底层实现时才强制公开成员。在大多数情况下,这是首选。
除了已经提供的出色答案之外,在某些情况下,编译器需要显式实现才能弄清楚需要什么。看看IEnumerable<T>
一个可能会经常出现的主要例子。
这是一个例子:
public abstract class StringList : IEnumerable<string>
{
private string[] _list = new string[] {"foo", "bar", "baz"};
// ...
#region IEnumerable<string> Members
public IEnumerator<string> GetEnumerator()
{
foreach (string s in _list)
{ yield return s; }
}
#endregion
#region IEnumerable Members
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
#endregion
}
在这里,IEnumerable<string>
implements IEnumerable
,因此我们也需要。但是等等,通用版本和普通版本都实现了具有相同方法签名的函数(C# 忽略了返回类型)。这是完全合法的。编译器如何解决使用哪个?它迫使您最多只有一个隐式定义,然后它可以解决它需要的任何问题。
IE。
StringList sl = new StringList();
// uses the implicit definition.
IEnumerator<string> enumerableString = sl.GetEnumerator();
// same as above, only a little more explicit.
IEnumerator<string> enumerableString2 = ((IEnumerable<string>)sl).GetEnumerator();
// returns the same as above, but via the explicit definition
IEnumerator enumerableStuff = ((IEnumerable)sl).GetEnumerator();
PS:IEnumerable 的显式定义中的一小部分间接有效,因为在函数内部,编译器知道变量的实际类型是 StringList,这就是它解析函数调用的方式。对于实现一些抽象层,一些 .NET 核心接口似乎已经积累了一些漂亮的小事实。
通过 C# 引用 CLR 中的 Jeffrey Richter
(EIMI表示E xplicit I interface M method I mplementation )
了解使用 EIMI 时存在的一些后果对您来说至关重要。由于这些后果,您应该尽量避免使用 EIMI。幸运的是,通用接口可以帮助您避免 EIMI。但是有时您可能仍然需要使用它们(例如实现两个具有相同名称和签名的接口方法)。以下是 EIMI 的大问题:
- 没有说明类型如何具体实现 EIMI 方法的文档,也没有 Microsoft Visual Studio IntelliSense支持。
- 值类型实例在转换为接口时被装箱。
- 派生类型不能调用 EIMI。
如果您使用接口引用,则可以在任何派生类上将任何虚拟链显式替换为 EIMI,并且当将此类类型的对象强制转换为接口时,您的虚拟链将被忽略并调用显式实现。那不是多态性。
EIMI 还可用于从基本框架接口的实现(例如 IEnumerable<T>)中隐藏非强类型接口成员,因此您的类不会直接公开非强类型方法,但语法正确。
当我想劝阻“为实现编程”(来自设计模式的设计原则)时,我倾向于使用显式接口实现。
例如,在基于MVP的 Web 应用程序中:
public interface INavigator {
void Redirect(string url);
}
public sealed class StandardNavigator : INavigator {
void INavigator.Redirect(string url) {
Response.Redirect(url);
}
}
现在另一个类(例如Presenter)不太可能依赖于 StandardNavigator 实现,而更可能依赖于 INavigator 接口(因为需要将实现转换为接口才能使用 Redirect 方法)。
我可能会使用显式接口实现的另一个原因是保持类的“默认”接口更干净。例如,如果我正在开发一个ASP.NET服务器控件,我可能需要两个接口:
下面是一个简单的例子。这是一个列出客户的组合框控件。在此示例中,网页开发人员对填充列表不感兴趣;相反,他们只想能够通过 GUID 选择客户或获取所选客户的 GUID。演示者将在第一个页面加载时填充该框,并且该演示者由控件封装。
public sealed class CustomerComboBox : ComboBox, ICustomerComboBox {
private readonly CustomerComboBoxPresenter presenter;
public CustomerComboBox() {
presenter = new CustomerComboBoxPresenter(this);
}
protected override void OnLoad() {
if (!Page.IsPostBack) presenter.HandleFirstLoad();
}
// Primary interface used by web page developers
public Guid ClientId {
get { return new Guid(SelectedItem.Value); }
set { SelectedItem.Value = value.ToString(); }
}
// "Hidden" interface used by presenter
IEnumerable<CustomerDto> ICustomerComboBox.DataSource { set; }
}
演示者填充数据源,网页开发人员永远不需要知道它的存在。
我不建议总是使用显式接口实现。这些只是两个可能有用的例子。
除了已经说明的其他原因之外,在这种情况下,一个类正在实现两个不同的接口,这些接口具有具有相同名称和签名的属性/方法。
/// <summary>
/// This is a Book
/// </summary>
interface IBook
{
string Title { get; }
string ISBN { get; }
}
/// <summary>
/// This is a Person
/// </summary>
interface IPerson
{
string Title { get; }
string Forename { get; }
string Surname { get; }
}
/// <summary>
/// This is some freaky book-person.
/// </summary>
class Class1 : IBook, IPerson
{
/// <summary>
/// This method is shared by both Book and Person
/// </summary>
public string Title
{
get
{
string personTitle = "Mr";
string bookTitle = "The Hitchhikers Guide to the Galaxy";
// What do we do here?
return null;
}
}
#region IPerson Members
public string Forename
{
get { return "Lee"; }
}
public string Surname
{
get { return "Oades"; }
}
#endregion
#region IBook Members
public string ISBN
{
get { return "1-904048-46-3"; }
}
#endregion
}
此代码编译并运行正常,但 Title 属性是共享的。
显然,我们希望 Title 的返回值取决于我们是将 Class1 视为 Book 还是 Person。这是我们可以使用显式接口的时候。
string IBook.Title
{
get
{
return "The Hitchhikers Guide to the Galaxy";
}
}
string IPerson.Title
{
get
{
return "Mr";
}
}
public string Title
{
get { return "Still shared"; }
}
请注意,显式接口定义被推断为 Public - 因此您不能将它们显式声明为 public(或以其他方式)。
另请注意,您仍然可以拥有“共享”版本(如上所示),但虽然这是可能的,但这种属性的存在是值得怀疑的。也许它可以用作 Title 的默认实现 - 这样就不必修改现有代码来将 Class1 转换为 IBook 或 IPerson。
如果您没有定义“共享”(隐式)标题,Class1 的使用者必须首先将 Class1 的实例显式转换为 IBook 或 IPerson - 否则代码将无法编译。
我大部分时间都使用显式接口实现。以下是主要原因。
重构更安全
更改接口时,最好编译器可以检查它。这对于隐式实现更难。
我想到了两种常见的情况:
将函数添加到接口,其中实现此接口的现有类恰好具有与新方法具有相同签名的方法。这可能会导致意想不到的行为,并多次咬我。调试时很难“看到”,因为该函数可能不在文件中的其他接口方法中(下面提到的自文档问题)。
从接口中删除功能。隐式实现的方法会突然变成死代码,但显式实现的方法会被编译错误捕获。即使死代码很好保留,我也想被迫审查并推广它。
不幸的是,C# 没有强制我们将方法标记为隐式实现的关键字,因此编译器可以进行额外的检查。由于需要使用“覆盖”和“新”,虚拟方法不存在上述任何一个问题。
注意:对于固定或很少更改的接口(通常来自供应商 API),这不是问题。但是,对于我自己的界面,我无法预测它们何时/如何改变。
它是自我记录的
如果我在一个类中看到“public bool Execute()”,则需要额外的工作才能确定它是接口的一部分。有人可能不得不这样评论它,或者把它放在一组其他接口实现中,所有这些都在一个区域或分组评论下,说“ITask 的实现”。当然,这仅在组标题不在屏幕外的情况下才有效。
鉴于:'bool ITask.Execute()' 清晰明确。
接口实现清晰分离
我认为接口比公共方法更“公共”,因为它们经过精心设计,只暴露了具体类型的一小部分表面区域。他们将类型简化为一种能力、一种行为、一组特征等。在实现中,我认为保持这种分离是有用的。
当我查看一个类的代码时,当我遇到显式接口实现时,我的大脑就会进入“代码契约”模式。通常这些实现只是简单地转发给其他方法,但有时它们会进行额外的状态/参数检查,转换传入参数以更好地匹配内部需求,甚至为版本控制目的进行转换(即多代接口都针对通用实现)。
(我意识到公共也是代码契约,但接口要强大得多,尤其是在接口驱动的代码库中,直接使用具体类型通常是内部代码的标志。)
相关:Jon 以上的原因 2。
等等
加上这里其他答案中已经提到的优势:
这并不全是乐趣和幸福。在某些情况下,我坚持使用隐式:
此外,当您确实具有具体类型并且想要调用显式接口方法时,进行强制转换可能会很痛苦。我以两种方式之一处理这个问题:
public IMyInterface I { get { return this; } }
(应该内联)并调用foo.I.InterfaceMethod()
. 如果有多个接口需要这种能力,把名字扩展到我之外(根据我的经验,我很少有这种需要)。如果显式实现,则只能通过接口类型的引用来引用接口成员。作为实现类的类型的引用不会公开这些接口成员。
如果您的实现类不是公共的,除了用于创建类的方法(可以是工厂或IoC容器)和接口方法(当然)之外,那么我认为显式实现没有任何优势接口。
否则,显式实现接口可确保不使用对具体实现类的引用,从而允许您稍后更改该实现。我想,“确保”是“优势”。一个精心设计的实现可以在没有显式实现的情况下实现这一点。
在我看来,缺点是您会发现自己在可以访问非公共成员的实现代码中向/从接口转换类型。
像许多事情一样,优点就是缺点(反之亦然)。显式实现接口将确保您的具体类实现代码不暴露。
隐式接口实现是您拥有具有相同接口签名的方法。
显式接口实现是您显式声明该方法属于哪个接口的地方。
interface I1
{
void implicitExample();
}
interface I2
{
void explicitExample();
}
class C : I1, I2
{
void implicitExample()
{
Console.WriteLine("I1.implicitExample()");
}
void I2.explicitExample()
{
Console.WriteLine("I2.explicitExample()");
}
}
MSDN:隐式和显式接口实现
每个实现接口的类成员都会导出一个声明,该声明在语义上类似于 VB.NET 接口声明的编写方式,例如
Public Overridable Function Foo() As Integer Implements IFoo.Foo
尽管类成员的名称通常与接口成员的名称匹配,并且类成员通常是公共的,但这些都不是必需的。还可以声明:
Protected Overridable Function IFoo_Foo() As Integer Implements IFoo.Foo
在这种情况下,类及其派生类将被允许使用名称访问类成员IFoo_Foo
,但外部世界只能通过转换为来访问该特定成员IFoo
。这种方法通常适用于接口方法在所有实现上都具有指定行为,但仅对某些实现有用的行为[例如,只读集合IList<T>.Add
方法的指定行为是 throw NotSupportedException
]。不幸的是,在 C# 中实现接口的唯一正确方法是:
int IFoo.Foo() { return IFoo_Foo(); }
protected virtual int IFoo_Foo() { ... real code goes here ... }
没那么好看。
前面的答案解释了为什么在 C# 中显式实现接口可能是可取的(主要是出于形式上的原因)。但是,有一种情况是强制显式实现的:为了避免接口为非但public
实现类为时泄露封装public
。
// Given:
internal interface I { void M(); }
// Then explicit implementation correctly observes encapsulation of I:
// Both ((I)CExplicit).M and CExplicit.M are accessible only internally.
public class CExplicit: I { void I.M() { } }
// However, implicit implementation breaks encapsulation of I, because
// ((I)CImplicit).M is only accessible internally, while CImplicit.M is accessible publicly.
public class CImplicit: I { public void M() { } }
上述泄漏是不可避免的,因为根据C# 规范,“所有接口成员都隐含地具有公共访问权限”。因此,隐式实现也必须提供public
访问权限,即使接口本身是 eg internal
。
C# 中的隐式接口实现非常方便。在实践中,许多程序员一直/无处不在地使用它而没有进一步考虑。这充其量会导致杂乱的类型表面,最坏的情况是泄漏封装。其他语言,例如 F#,甚至不允许它。
显式接口实现的一个重要用途是在需要实现具有混合可见性的接口时。
问题和解决方案在文章C# Internal Interface中有很好的解释。
例如,如果您想保护应用程序层之间的对象泄漏,此技术允许您指定可能导致泄漏的成员的不同可见性。