C#接口。 隐式实现与显式实现
在C#中隐式和显式实现接口有什么区别?
什么时候应该使用隐式,何时应该使用explicit?
这两者有没有优点和/或缺点?
微软官方指南(从第一版的框架devise指南 )指出, 不build议使用显式实现 ,因为它给代码带来了意想不到的行为。
我认为这个指南在IoC之前是非常有效的 ,当你不把它作为接口传递的时候。
任何人都可以碰到这方面呢?
隐含的是当你通过你的类的成员定义你的接口。 显式是在接口上的类中定义方法的时候。 我知道这听起来令人困惑,但这里是我的意思: IList.CopyTo
将隐含实现为:
public void CopyTo(Array array, int index) { throw new NotImplementedException(); }
并明确表示为:
void ICollection.CopyTo(Array array, int index) { throw new NotImplementedException(); }
不同之处在于隐式地通过你所创build的类来访问它,当它被转换为该类时,以及当它被转换为接口时。 显式实现允许它只能在作为接口本身进行转换时才可访问。
MyClass myClass = new MyClass(); // Declared as concrete class myclass.CopyTo //invalid with explicit ((IList)myClass).CopyTo //valid with explicit.
我主要使用显式来保持实现干净,或者当我需要两个实现。 但是,无论我很less使用它。
我相信有更多的理由使用它/不使用它,其他人将张贴。
看到这个线程中的下一篇文章 ,每个背后的优秀推理。
隐式定义只是将接口要求的方法/属性等直接作为公共方法添加到类中。
显式定义强制只在您直接使用接口而不使用底层实现时暴露这些方法。 这在大多数情况下是首选。
- 通过直接使用接口,您不能确认,并将您的代码耦合到底层的实现。
- 如果在代码中已经有一个公共属性名称,并且要实现一个也具有一个Name属性的接口,那么明确地执行它将使两者分开。 即使他们正在做同样的事情,我仍然将明确的调用委托给Name属性。 你永远不知道,你可能想要改变Name对普通类的作用,以及Name,接口属性如何工作。
- 如果你隐式地实现一个接口,那么你的类现在就暴露了新的行为,这些行为可能只是和接口的客户端相关,这意味着你不能保证你的类简洁(我的意见)。
除了已经提供的优秀答案之外,在某些情况下,编译器需要明确的实现,才能弄清楚需要什么。 看看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>
实现IEnumerable
,所以我们也需要。 但是,通用和普通版本都使用相同的方法签名来实现函数 (C#忽略返回types)。 这是完全合法的,罚款。 编译器如何解决使用哪个? 它强制你至多只有一个隐含的定义,那么它可以解决任何需要的。
即。
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的显式定义中,间接性的一小部分工作,因为在函数内部编译器知道variables的实际types是一个StringList,这就是它如何解决函数调用。 某些.NET核心接口实现某些抽象层似乎已经积累了很less的事实。
原因#1
当我想阻止“编程实现”( Design Patterns from Design Patterns )时,我倾向于使用显式接口实现。
例如,在基于MVP的Web应用程序中:
public interface INavigator { void Redirect(string url); } public sealed class StandardNavigator : INavigator { void INavigator.Redirect(string url) { Response.Redirect(url); } }
现在,另一个类(例如演示者 )不太可能依赖于StandardNavigator实现,而更可能依赖于INavigator接口(因为需要将实现转换为接口以使用redirect方法)。
原因#2
另一个我可能会用一个明确的接口实现的原因是保持一个类的“默认”接口清洁。 例如,如果我正在开发一个ASP.NET服务器控件,我可能需要两个接口:
- 该类的主要接口,由网页开发人员使用; 和
- 演示者使用的“隐藏”接口,用于处理控件的逻辑
一个简单的例子如下。 这是一个列出客户的combobox控件。 在这个例子中,网页开发者对填充列表不感兴趣; 相反,他们只是希望能够通过GUIDselect一个客户或获得所选客户的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; } }
演示者填充数据源,网页开发人员不需要知道其存在。
但是这不是一个silverlight的炮弹
我不build议总是使用明确的接口实现。 这些只是两个可能有用的例子。
从CLR引用Jeffrey Richter的C#
( EIMI的意思是简单的方法实现)
了解使用EIMI时存在的一些后果对您来说至关重要。 而由于这些后果,你应该尽量避免使用EIMIs。 幸运的是,通用接口可以帮助您避免EIMI。 但是可能还有些时候需要使用它们(比如实现两个具有相同名称和签名的接口方法)。 以下是EIMI的大问题:
- 没有文档说明types如何专门实现EIMI方法,并且没有Microsoft Visual Studio 智能感知支持。
- 值types实例在投射到接口时被装箱。
- EIMI不能被派生types调用。
如果使用接口引用ANY虚拟链可以在任何派生类上用EIMI显式replace,并且当这种types的对象被转换到接口时,您的虚拟链将被忽略,并且调用显式实现。 这不过是多态而已。
EIMI也可以用来隐藏IEnumerable <T>等基本框架接口实现中非强types的接口成员,所以你的类不直接暴露非强types的方法,但是在语法上是正确的。
除了已经提到的其他原因之外,这是一个类正在实现两个不同的接口,它们具有相同的名称和签名的属性/方法。
/// <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(或以其他方式)。
还要注意,你仍然可以拥有一个“共享”版本(如上所示),但是尽pipe这是可能的,但是这样的属性的存在是有问题的。 也许它可以用作Title的默认实现 – 以便现有代码不必被修改,以将Class1投射到IBook或IPerson。
如果您没有定义“共享”(隐式)标题,则Class1的使用者必须首先明确地将Class1的实例转换为IBook或IPerson,否则代码将无法编译。
大部分时间我都使用显式的接口实现。 主要原因如下。
重构更安全
更改接口时,编译器最好检查一下。 隐式实现更难。
想到两个常见的情况:
-
在一个接口中添加一个函数,在这个接口中,实现这个接口的现有类已经碰巧有一个与新的接口相同的方法 。 这可能会导致意外的行为,并且困难了好几次。 debugging时很难“看到”,因为该函数可能不在文件中的其他接口方法(下面提到的自我logging问题)中。
-
从界面中删除一个function 。 隐式实现的方法会突然死锁,但是明确实现的方法会被编译错误所捕获。 即使死代码是好的,我想被迫审查并推广它。
不幸的是,C#没有一个关键字迫使我们将一个方法标记为一个隐式的实现,所以编译器可以做额外的检查。 由于需要使用“覆盖”和“新”,虚拟方法没有上述问题。
注意:对于固定或很less更改的接口(通常来自供应商API),这不是问题。 但是,对于我自己的界面,我无法预测何时/如何改变。
这是自我logging
如果我在一个类中看到“public bool Execute()”,则需要花费额外的工作来确定它是否是接口的一部分。 有人可能不得不评论这么说,或者把它放在一组其他接口实现中,所有这些都在一个区域或分组注释中,都是“实现ITask”。 当然,只有组标题不在屏幕外的情况下才有效。
而“bool ITask.Execute()”是清晰而明确的。
界面实现清晰分离
我认为接口比公共方法更“公共”,因为它们只是为了暴露具体types的表面区域而已。 他们把这种types降为一种能力,一种行为,一套特质等。在实施中,我认为保持这种分离是有用的。
当我正在浏览一个类的代码时,当我遇到显式的接口实现时,我的大脑转移到“代码契约”模式。 通常这些实现只是转发给其他方法,但有时他们会做额外的状态/参数检查,传入参数的转换以更好地匹配内部需求,甚至为了版本控制的目的(即多代接口都向下普通实现)进行转换。
(我意识到公共代码也是代码契约,但是接口要强大得多,特别是在接口驱动的代码库中,直接使用具体types通常是内部代码的标志。)
相关: 由Jon以上的原因2 。
等等
加上其他答案中已经提到的优点:
- 根据需要,根据消歧或需要内部接口
- 不鼓励“编程实施”( 乔恩的原因1 )
问题
这不是所有的乐趣和快乐。 有些情况下我坚持暗示:
- 值types,因为这将需要拳击和更低的性能。 这不是一个严格的规则,取决于接口以及如何使用它。 IComparable的? 隐。 IFormattable? 可能是明确的。
- 具有经常直接调用方法的普通系统接口(如IDisposable.Dispose)。
而且,当你确实拥有具体的types并且想要调用一个显式的接口方法的时候,执行这个转换可能会很痛苦。 我以两种方式之一处理这个问题:
- 添加public并将接口方法转发给他们执行。 在内部工作时,通常会发生简单的接口。
- (我的首选方法)添加一个
public IMyInterface I { get { return this; } }
public IMyInterface I { get { return this; } }
(应该内联)并调用foo.I.InterfaceMethod()
。 如果有多个接口需要这个function,请将名字扩展到I以外(根据我的经验,我很less有这个需求)。
如果明确实现,则只能通过接口types的引用来引用接口成员。 实现类的引用将不会公开这些接口成员。
如果你的实现类不公开,除了用于创build类(可能是工厂或IoC容器)的方法,除了接口方法(当然),我没有看到明确的实现接口。
否则,显式实现接口会确保不使用对具体实现类的引用,以便稍后更改该实现。 “确定”,我想是“优势”。 一个理想的实现可以在没有明确实现的情况下完成。
在我看来,缺点是你会发现自己在实现代码中的接口上input/输出types,这些接口可以访问非公共成员。
像许多事情一样,优点是缺点(反之亦然)。 明确实现接口将确保您的具体类实现代码不公开。
一个隐含的接口实现就是你拥有一个具有相同接口签名的方法。
一个明确的接口实现是你明确声明方法属于哪个接口的地方。
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
尽pipe类成员的名字通常会和接口成员的名字相匹配,但是类成员通常是公开的,这些东西都不是必需的。 也可以声明:
Protected Overridable Function IFoo_Foo() As Integer Implements IFoo.Foo
在这种情况下,这个类及其派生类将被允许使用名称IFoo_Foo
来访问一个类成员,但外部世界只能通过将其转换为IFoo
来访问该特定成员。 这样的方法在接口方法在所有实现上都具有指定行为的情况下通常是有效的 ,但是对于只读的集合的IList<T>.Add
方法的指定行为是抛出NotSupportedException
。 不幸的是,在C#中实现接口的唯一正确方法是:
int IFoo.Foo() { return IFoo_Foo(); } protected virtual int IFoo_Foo() { ... real code goes here ... }
不太好。
显式接口实现的一个重要用途是需要实现具有混合可见性的接口。
C#内部接口文章很好地解释了这个问题和解决scheme。
例如,如果要防止应用程序层之间的对象泄漏,则可以使用此技术指定可能导致泄漏的成员的不同可见性。