C# Interfaces. Implicit implementation versus Explicit implementation
在C中隐式和显式实现接口有什么区别?
什么时候应该使用隐式?什么时候应该使用显式?
这两者有什么利弊吗?
微软的官方指南(来自第一版框架设计指南)指出,不建议使用显式实现,因为它会给代码带来意想不到的行为。
我认为在国际奥委会之前的时间里,当你不把事情作为接口传递时,这个准则是非常有效的。
有人也能在这方面做些什么吗?
隐式是指通过类上的成员定义接口时。显式是指在接口上的类内定义方法。我知道这听起来很困惑,但我的意思是:
1 2 3 4 |
并明确表示为:
1 2 3 4 |
不同之处在于,当您创建的类被转换为该类以及当它被转换为接口时,可以通过它隐式地访问。显式实现只允许将其作为接口本身强制转换时才可访问。
1 2 3 | MyClass myClass = new MyClass(); // Declared as concrete class myclass.CopyTo //invalid with explicit ((IList)myClass).CopyTo //valid with explicit. |
我主要使用显式来保持实现的整洁,或者当我需要两个实现时。但不管怎样,我很少用它。
我相信有更多的理由使用它/不使用它,其他人将张贴。
请参阅本文的下一篇文章,了解每一篇文章背后的优秀推理。
隐式定义就是将接口直接要求的方法/属性等作为公共方法添加到类中。
显式定义仅在直接使用接口而不是底层实现时强制成员公开。在大多数情况下,这是首选。
除了已经提供了很好的答案之外,还有一些情况下编译器需要显式实现才能确定需要什么。以
下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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 } |
在这里,
IE.
1 2 3 4 5 6 7 8 | 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的显式定义中的一小部分间接作用是因为在函数内部,编译器知道变量的实际类型是一个字符串列表,这就是它如何解析函数调用的原因。对于实现一些抽象层,一些.NET核心接口似乎已经积累了一些漂亮的小事实。
原因1
当我不鼓励"编程到实现"(设计模式中的设计原则)时,我倾向于使用显式接口实现。
例如,在基于MVP的Web应用程序中:
1 2 3 4 5 6 7 8 9 | public interface INavigator { void Redirect(string url); } public sealed class StandardNavigator : INavigator { void INavigator.Redirect(string url) { Response.Redirect(url); } } |
现在,另一个类(如Presenter)不太可能依赖于StandardNavigator实现,更可能依赖于Inavigator接口(因为需要将实现强制转换到接口以使用重定向方法)。
原因2我使用显式接口实现的另一个原因是保持类的"默认"接口更干净。例如,如果我正在开发一个ASP.NET服务器控件,我可能需要两个接口:
下面是一个简单的例子。它是一个组合框控件,用于列出客户。在本例中,网页开发人员对填充列表不感兴趣;相反,他们只希望能够按guid选择客户或获取所选客户的guid。演示者将在第一页加载时填充该框,并且该演示者由控件封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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; } } |
演示者填充数据源,网页开发人员永远不需要知道它的存在。
但它不是一个银色的炮弹吗?我不建议总是使用显式接口实现。这只是两个可能有用的例子。
从clr通过c引用jeffrey richter#(eimi表示显式接口方法实现)
It is critically important for you to
understand some ramifications that
exist when using EIMIs. And because of
these ramifications, you should try to
avoid EIMIs as much as possible.
Fortunately, generic interfaces help
you avoid EIMIs quite a bit. But there
may still be times when you will need
to use them (such as implementing two
interface methods with the same name
and signature). Here are the big
problems with EIMIs:
- There is no documentation explaining how a type specifically
implements an EIMI method, and there
is no Microsoft Visual Studio
IntelliSense support.- Value type instances are boxed when cast to an interface.
- An EIMI cannot be called by a derived type.
如果使用接口引用,任何虚拟链都可以在任何派生类上显式替换为eimi,并且当此类类型的对象强制转换到接口时,将忽略您的虚拟链并调用显式实现。这不是多态性。
EIMIS还可以用于从基本框架接口的实现(如IEnumerable
除了已经说明的其他原因之外,这是一个类正在实现两个不同的接口,这些接口具有相同名称和签名的属性/方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | /// <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 } |
此代码编译并运行正常,但标题属性是共享的。
很明显,我们希望标题的值返回取决于我们是将Class1视为一本书还是一个人。这是我们可以使用显式接口的时候。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | string IBook.Title { get { return"The Hitchhikers Guide to the Galaxy"; } } string IPerson.Title { get { return"Mr"; } } public string Title { get { return"Still shared"; } } |
请注意,显式接口定义被推断为公共的,因此您不能显式地将它们声明为公共的(或其他)。
还要注意,您仍然可以有一个"共享"版本(如上图所示),但尽管这是可能的,但这种属性的存在是值得怀疑的。也许它可以用作标题的默认实现——这样就不必修改现有代码来将class1强制转换为ibook或iperson。
如果您没有定义"共享"(隐式)标题,class1的使用者必须首先显式地将class1的实例强制转换为ibook或iperson,否则代码将无法编译。
我经常使用显式接口实现。主要原因如下。好的。
重构更安全好的。
在更改接口时,最好由编译器检查。这对于隐式实现来说更难。好的。
我们想到了两个常见的例子:好的。
向接口中添加一个函数,在该接口中,实现该接口的现有类恰好具有与新方法具有相同签名的方法。这可能会导致意想不到的行为,并已咬了我好几次。调试时很难"看到",因为该函数可能与文件中的其他接口方法不在一起(下面提到的自文档问题)。好的。
从接口中删除函数。隐式实现的方法会突然失效,但显式实现的方法会被编译错误捕获。即使死掉的代码很好保存,我也希望被强制检查并升级它。好的。
不幸的是,C没有一个关键字强制我们将一个方法标记为隐式实现,因此编译器可以执行额外的检查。由于需要使用"override"和"new",虚拟方法没有上述任何一个问题。好的。
注意:对于固定或很少更改的接口(通常来自供应商API),这不是问题。但是,对于我自己的接口,我无法预测它们何时/如何改变。好的。
这是自我记录好的。
如果我在类中看到"public bool execute()",就需要额外的工作来确定它是接口的一部分。有人可能不得不这样评论它,或者把它放在一组其他的接口实现中,所有这些都放在一个区域或者说是"ITask的实现"的分组注释下。当然,只有当组标题不在屏幕外时,这才有效。好的。
而:"bool itask.execute()"是清晰的。好的。
接口实现的清晰分离好的。
我认为接口比公共方法更"公共",因为它们的设计只是为了公开一点具体类型的表面积。它们将类型减少到一种能力、一种行为、一组特性等,在实现中,我认为保持这种分离是有用的。好的。
当我浏览一个类的代码时,当我遇到显式的接口实现时,我的大脑转变为"代码契约"模式。通常,这些实现只是转发到其他方法,但有时它们会执行额外的状态/参数检查、传入参数的转换以更好地匹配内部需求,甚至出于版本控制的目的进行转换(即,多代接口——所有这些都归结为通用实现)。好的。
(我认识到public也是代码契约,但是接口更强大,特别是在接口驱动的代码库中,直接使用具体类型通常是内部代码的标志。)好的。
相关:乔恩的上述原因2。好的。
等等好的。
加上其他答案中已经提到的优势:好的。
- 需要时,根据消歧或需要内部接口
- 不鼓励"编程到实现"(Jon的原因1)
问题
不是所有的乐趣和幸福。在某些情况下,我坚持认为这是牵连的:好的。
- 值类型,因为这需要装箱和降低性能。这不是一个严格的规则,它取决于接口以及如何使用它。可比喻的?隐性的。IFormattable?可能是明确的。
- 具有经常直接调用的方法(如IDisposable.Dispose)的普通系统接口。
此外,当您确实具有具体的类型并希望调用显式接口方法时,执行强制转换可能会很困难。我用以下两种方式之一处理:好的。
好啊。
如果显式实现,则只能通过接口类型的引用引用引用接口成员。实现类类型的引用不会公开这些接口成员。
如果您的实现类不是公共的,除了用于创建类(可以是工厂或IOC容器)的方法,以及除接口方法(当然)之外,我看不到显式实现接口的任何优势。
否则,显式实现接口可确保不使用对具体实现类的引用,从而允许您稍后更改该实现。"我想,要确保"优势"。一个分解良好的实现可以在没有显式实现的情况下完成这一点。
在我看来,缺点是,您将发现自己在实现代码中向/从接口中强制转换类型,该代码确实可以访问非公共成员。
像许多事情一样,优势是劣势(反之亦然)。显式实现接口将确保不公开具体的类实现代码。
隐式接口实现是指具有与接口相同签名的方法。
显式接口实现是显式声明方法属于哪个接口的地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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接口声明编写方式的声明,例如。
1 | Public Overridable Function Foo() As Integer Implements IFoo.Foo |
尽管类成员的名称通常与接口成员的名称匹配,并且类成员通常是公共的,但这两者都不是必需的。也可以声明:
1 | Protected Overridable Function IFoo_Foo() As Integer Implements IFoo.Foo |
在这种情况下,将允许类及其派生类使用名称
1 2 | int IFoo.Foo() { return IFoo_Foo(); } protected virtual int IFoo_Foo() { ... real code goes here ... } |
不太好。
显式接口实现的一个重要用途是当需要实现具有混合可见性的接口时。
问题和解决方案在C内部接口一文中有很好的解释。
例如,如果要保护应用程序层之间对象的泄漏,则此技术允许您指定可能导致泄漏的成员的不同可见性。