在C#中实现隐式和显式接口有什么区别?
什么时候应该使用隐式?什么时候应该使用显式?
是否有任何优点和/或缺点?
Microsoft的官方指南(来自第一版Framework Design Guidelines)声明不建议使用显式实现,因为它会给代码带来意外行为。
我认为这个指南在IoC时间之前非常有效,当你没有作为接口传递时。
也有人可以触及这个方面吗?
答案 0 :(得分:464)
隐含是指您通过班级成员定义界面的时间。 显式是指在接口上定义类中的方法。我知道这听起来令人困惑,但这就是我的意思: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.
我主要使用explicit来保持实现干净,或者当我需要两个实现时。但无论我很少使用它。
我确信有更多理由使用它/不使用其他人会发布的。
请参阅此主题中的next post,了解每个主题背后的理由。
答案 1 :(得分:187)
隐式定义只是将接口所需的方法/属性等直接添加到类中作为公共方法。
显式定义仅在您直接使用接口而不是底层实现时强制公开成员。在大多数情况下,这是首选。
答案 2 :(得分:65)
除了已经提供的优秀答案之外,还有一些情况需要显式实现,以便编译器能够找出所需的内容。看看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();
PS:IEnumerable的显式定义中的小部分间接作用,因为在函数内部,编译器知道变量的实际类型是StringList,这就是它如何解析函数调用。实现某些抽象层的一些微不足道的事实似乎已经积累了一些.NET核心接口。
答案 3 :(得分:31)
当我想阻止“编程实现”( Design Principles 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);
}
}
现在另一个类(例如presenter)不太可能依赖于StandardNavigator实现,更可能依赖于INavigator接口(因为实现需要强制转换为接口才能使用重定向方法)。
我可能采用显式接口实现的另一个原因是保持类的“默认”接口更清晰。例如,如果我正在开发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; }
}
演示者填充数据源,网页开发人员永远不需要知道它的存在。
我不建议总是使用显式接口实现。这只是两个可能有用的例子。
答案 4 :(得分:30)
通过C#引用CLR的Jeffrey Richter
( EIMI 表示 E xplicit 我 nterface M ethod 我实施)
对你来说至关重要 了解一些后果 使用EIMI时存在。因为 这些后果,你应该尝试 尽可能避免使用EIMI。 幸运的是,通用接口有帮助 你避免使用EIMI。但那里 可能仍然是你需要的时候 使用它们(例如实施两个 具有相同名称的接口方法 和签名)。这是大事 EIMI的问题:
- 没有专门解释类型的文档 实现了一个EIMI方法 不是Microsoft Visual Studio IntelliSense支持。
- 当转换为接口时,值类型实例会被装箱。
- 无法通过派生类型调用EIMI。
如果使用接口引用ANY虚拟链可以在任何派生类上显式替换为EIMI,并且当将此类型的对象强制转换为接口时,将忽略您的虚拟链并调用显式实现。这不过是多态的。
EIMI还可用于隐藏基本框架接口实现中的非强类型接口成员,例如IEnumerable&lt; T&gt;。所以你的类不直接公开非强类型方法,但语法正确。
答案 5 :(得分:18)
除了已经说明的其他原因之外,这是一个类正在实现两个具有相同名称和签名的属性/方法的不同接口的情况。
/// <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 - 因此您无法将它们声明为公开(或其他)显式。
另请注意,您仍然可以拥有“共享”版本(如上所示),但尽管这是可能的,但这种属性的存在是值得怀疑的。也许它可以用作Title的默认实现 - 这样就不必修改现有代码就可以将Class1强制转换为IBook或IPerson。
如果您没有定义“共享”(隐式)标题,则Class1 的使用者必须首先将Class1的实例显式地转换为IBook或IPerson,否则代码将无法编译。
答案 6 :(得分:14)
我大部分时间都使用显式接口实现。以下是主要原因。
重构更安全
更改界面时,编译器可以检查它是否更好。隐式实现会更难。
我想到了两个常见的案例:
向接口添加一个函数,其中实现此接口的现有类恰好具有与新接口具有相同签名的方法。这可能导致意外行为,并且几次困扰我。调试时很难“看到”,因为该函数很可能与文件中的其他接口方法无关(下面提到的自我记录问题)。
从界面中删除功能。隐式实现的方法将突然死代码,但显式实现的方法将被编译错误捕获。即使死代码保持良好,我也希望被强制审查并推广它。
不幸的是,C#没有强制我们将方法标记为隐式实现的关键字,因此编译器可以进行额外的检查。由于需要使用'override'和'new',虚方法没有上述任何一个问题。
注意:对于固定或很少更改的接口(通常来自供应商API),这不是问题。但是对于我自己的界面,我无法预测它们何时/如何改变。
这是自我记录
如果我在一个类中看到'public bool Execute()',那么需要额外的工作来弄清楚它是接口的一部分。有人可能不得不评论它这样说,或者把它放在一组其他接口实现中,所有这些都在一个区域或分组评论中说“ITask的实现”。当然,这仅在组头不在屏幕外时才有效。
然而:'bool ITask.Execute()'清晰且明确。
明确分离界面实施
我认为接口比公共方法更“公开”,因为它们被设计为仅露出具体类型的一些表面区域。它们将类型简化为一种能力,一种行为,一组特征等。在实施中,我认为保持这种分离是有用的。
当我查看类的代码时,当我遇到显式接口实现时,我的大脑转变为“代码契约”模式。这些实现通常只是转发到其他方法,但有时它们会进行额外的状态/参数检查,传入参数的转换以更好地匹配内部需求,甚至转换用于版本控制目的(即多代接口都归结为常见的实现)。
(我意识到公共也是代码契约,但接口更强大,特别是在接口驱动的代码库中,直接使用具体类型通常是内部代码的标志。)
依此类推
再加上其他答案中已经提到的优点:
并非所有的乐趣和快乐。在某些情况下,我坚持使用含义:
此外,当你确实具有具体类型并且想要调用显式接口方法时,执行转换可能会很痛苦。我用以下两种方式处理这个问题:
public IMyInterface I { get { return this; } }
(应该内联)并致电foo.I.InterfaceMethod()
。如果需要此功能的多个接口,请将名称扩展到I之外(根据我的经验,我很少有这种需求)。答案 7 :(得分:8)
如果明确实现,则只能通过接口类型的引用引用接口成员。作为实现类类型的引用不会公开这些接口成员。
如果你的实现类不是公共的,除了用于创建类的方法(可以是工厂或IoC容器),除了接口方法(当然),那我就不要看到明确实现接口的任何好处。
否则,显式实现接口可确保不使用对具体实现类的引用,从而允许您稍后更改该实现。我认为,“确定”是“优势”。一个考虑周全的实现可以在没有明确实现的情况下实现这一点。
在我看来,缺点是您会发现自己在实现代码中向接口输入类型,从而可以访问非公共成员。
与许多事情一样,优点是缺点(反之亦然)。显式实现接口将确保不暴露您的具体类实现代码。
答案 8 :(得分:6)
隐式接口实现是指具有相同签名的接口的方法。
显式接口实现是您显式声明方法所属的接口的地方。
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()");
}
}
答案 9 :(得分:5)
实现接口的每个类成员都会导出一个声明,该声明在语义上类似于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
方法的指定行为是抛出NotSupportedException
]。不幸的是,在C#中实现接口的唯一正确方法是:
int IFoo.Foo() { return IFoo_Foo(); }
protected virtual int IFoo_Foo() { ... real code goes here ... }
不太好。
答案 10 :(得分:2)
先前的答案解释了为什么在C#中显式实现接口可能是更可取的(出于正式原因)。但是,在一种情况下,显式实现是强制性的:为了避免在接口非public
但实现类为public
时泄漏封装。 / p>
// 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# specification,“所有接口成员都隐式具有公共访问权限”。结果,即使接口本身例如是,隐式实现也必须给予public
访问权限。 internal
。
C#中的隐式接口实现非常方便。在实践中,许多程序员始终/无处不在使用它,而无需进一步考虑。这最好导致混乱的表面,最坏导致封装泄漏。其他语言,例如F#,don't even allow it。
答案 11 :(得分:1)
显式接口实现的一个重要用途是需要使用混合可见性实现接口。
C# Internal Interface 一文中详细解释了问题和解决方案。
例如,如果要保护应用程序层之间的对象泄漏,此技术允许您指定可能导致泄漏的成员的不同可见性。