在C#中,实现接口的方式有隐式和显式两种方式,它们之间有什么区别?
何时使用隐式和何时使用显式?
使用其中一种方法有哪些利弊呢?
微软的官方指南(取自第一版Framework Design Guidelines)指出不建议使用显式实现,因为这会导致代码出现意外行为。
我认为这个指南在非IoC时代是非常有效的,当你不以接口方式传递事物时。
有人可以讨论一下这个方面吗?
在C#中,实现接口的方式有隐式和显式两种方式,它们之间有什么区别?
何时使用隐式和何时使用显式?
使用其中一种方法有哪些利弊呢?
微软的官方指南(取自第一版Framework Design Guidelines)指出不建议使用显式实现,因为这会导致代码出现意外行为。
我认为这个指南在非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.
我主要使用显式定义以保持实现的清晰,或者当我需要两个实现时使用。不管怎样,我很少使用它。
我相信还有更多使用/不使用显式定义的原因,其他人会发布。
请参见此线程中下一篇文章,其中包含每个原因的优秀论据。
public
关键字...否则你会收到错误信息。 - jharr100UISwitch IScoreRegPlayerViewCell.markerSwitch { get { return markerSwitch; } }
。 - ToolmakerSteveIEnumerator<T>.Current
、IEnumerable<T>.GetEnumerator()
和 ISet<T>.Add(T)
一样。这在 另一个答案 中提到过。 - phoog隐式定义是将接口要求的方法/属性等直接作为公共方法添加到类中。
显式定义强制将成员仅在直接使用接口时才暴露,而不是底层实现。这在大多数情况下都是首选。
除了已经提供的优秀答案之外,有一些情况下需要显式实现,这样编译器才能弄清所需内容。 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#忽略返回类型)。这是完全合法和正常的。编译器如何解析要使用哪一个?它强制你只有最多一个隐式定义,然后它可以解析它需要的任何内容。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();
注意:IEnumerable的显式定义中的那个小小的间接性质之所以有效,是因为在函数内部编译器知道变量的实际类型是StringList,从而解析函数调用。这是一些.NET核心接口似乎已经积累了几层抽象的巧妙小事实。
引用来自CLR via C#的Jeffrey Richter的话:
(EIMI代表Explicit Interface Method Implementation)
使用EIMI时,您需要理解一些相关影响的重要性。由于这些影响,您应该尽可能避免使用EIMI。幸运的是,通用接口可以帮助您避免使用EIMI。但是在某些情况下,您仍然需要使用它们(例如实现两个具有相同名称和签名的接口方法)。以下是EIMI的主要问题:
- 没有文档说明类型如何具体实现EIMI方法,并且没有Microsoft Visual Studio IntelliSense支持。
- 值类型实例在转换为接口时会被装箱。
- EIMI无法被派生类型调用。
如果您使用接口引用,则任何虚拟链都可以在任何派生类上显式替换为EIMI,当将此类对象强制转换为接口时,将忽略您的虚拟链并调用显式实现。这与多态性完全不同。
EIMI还可用于从基本Framework接口实现(例如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。Presenter会在第一个页面加载时填充框,而这个Presenter被该控件封装。
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; }
}
演示者填充数据源,网页开发人员无需知道其存在。
我不建议总是使用显式接口实现。这只是两个可能有帮助的示例。
我大多数情况下使用显式接口实现,以下是主要原因。
重构更加安全
更改接口时,如果编译器可以检查,则更好。这在隐式实现中更难。
有两种常见情况:
向接口添加一个函数,在此之前已经存在实现该接口的类同时具有与新函数相同签名的已有方法。这可能导致意外行为,并且多次让我遭受打击。在调试时很难"看到"它,因为该函数很可能不位于文件中的其他接口方法(下面提到的自注释问题)。
从接口中删除函数。隐式实现的方法将会突然变成死代码,但显式实现的方法将被编译错误捕获。即使死代码保留下来是有益的,我也想强制审查并升级它们。
遗憾的是C#没有一个关键字强制我们将方法标记为隐式实现,所以编译器可以进行额外的检查。由于需要使用 override 和 new,虚拟方法不存在上述任何问题。
注意:对于固定或很少更改的接口(通常来自供应商API),这不是问题。但对于我的接口,我无法预测它们何时/如何更改。
自我注释
如果我在一个类中看到 'public bool Execute()',我将需要额外的工作来确定它是接口的一部分。可能需要有人在注释中写明,或者将其放在其他接口实现的组中,所有这些都在区域或分组注释下面,说明是“ITask的实现”。当然,这仅适用于组头不在屏幕外的情况。
而:'bool ITask.Execute()'是清晰且明确的。
接口实现清晰分离
我认为接口比公共方法更“公开”,因为它们被设计为仅公开具体类型的一小部分表面区域,将类型降低到能力、行为、一组特征等。在实现中,保持这种分离是有用的。
当我查看类的代码时,当我遇到显式接口实现时,我的大脑会进入“代码合同”模式。通常这些实现只是转发到其他方法,但有时它们会进行额外的状态/参数检查,将传入参数转换为更好地匹配内部要求,甚至进行版本控制翻译(即多代接口都对通用实现进行计算)。
(我意识到公共项也是代码合同,但接口更加强大,尤其是在一个以接口为驱动的代码库中,直接使用具体类型通常是内部代码的标志。)
相关:Jon提出的上述原因2。
等等
此外,在其他答案中已经提到的优点:
并非所有情况都愉快。有些情况下我会坚持隐式:
此外,当你确实拥有具体类型并想要调用显式接口方法时,进行转换可能会很麻烦。我有两种处理方式:
public IMyInterface I { get { return this; } }
(应该被内联),然后调用foo.I.InterfaceMethod()
。 如果有多个需要这种能力的接口,请将名称扩展到I之外(据我的经验,我很少需要这种情况)。除了已经提到的其他原因外,这种情况是在一个类实现了两个具有相同名称和签名的属性/方法的不同接口时发生的。
/// <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属性是共享的。
显然,我们希望根据对Class1作为Book还是Person进行处理来返回Title的值。这时候我们可以使用显式接口。
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(或其他)。
还要注意,您仍然可以拥有一个“共享”版本(如上所示),但尽管这是可能的,但存在这样一个属性是值得怀疑的。也许它可以用作 Title 的默认实现,以便现有代码不必修改就能将 Class1 强制转换为 IBook 或 IPerson。
如果您没有定义“共享”(隐式)标题,则 Class1 的使用者必须首先明确地将 Class1 的实例强制转换为 IBook 或 IPerson - 否则,代码将无法编译。
。
Public Overridable Function Foo() As Integer Implements IFoo.Foo
尽管类成员的名称通常与接口成员相匹配,并且类成员通常是公共的,但这些都不是必需的。我们也可以声明:
Protected Overridable Function IFoo_Foo() As Integer Implements IFoo.Foo
int IFoo.Foo() { return IFoo_Foo(); }
protected virtual int IFoo_Foo() { ... real code goes here ... }
隐式接口实现是指您拥有与接口相同签名的方法。
显式接口实现是指您明确声明该方法属于哪个接口。
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: 隐式和显式接口实现