为什么要显式实现接口?

124

那么,实现接口显式地有什么好处呢?

只是为了让使用该类的人不必在智能提示中查看所有这些方法/属性吗?

11个回答

149
如果你实现了两个接口,这两个接口有相同的方法但不同的实现,那么你必须显式地实现该方法。
public interface IDoItFast
{
    void Go();
}
public interface IDoItSlow
{
    void Go();
}
public class JustDoIt : IDoItFast, IDoItSlow
{
    void IDoItFast.Go()
    {
    }

    void IDoItSlow.Go()
    {
    }
}

是的,确切地说,这是EIMI解决的一个案例。其他问题已经由“Michael B”的回答涵盖了。 - crypted
10
很棒的例子。喜欢界面和类名! :-) - Brian Rogers
11
我不喜欢这个,一个类中有两个具有相同签名但作用非常不同的方法?这是极其危险的事情,并且很可能会在大型开发中造成混乱。如果你的代码像这样,我会说你的分析和设计混乱不堪。 - Mick
4
这些接口可能属于某个API或两个不同的API。也许“喜欢”这个词有点夸张了,但至少我会很高兴明确的实现是可用的。 - TobiMcNamobi
@BrianRogers 还有方法名称 ;-) - Sнаđошƒаӽ

67
隐藏非首选成员是很有用的。例如,如果你同时实现了IComparable<T>IComparable,通常最好隐藏IComparable重载,以免让人们误以为你可以比较不同类型的对象。同样,一些接口不符合CLS标准,比如IConvertible,所以如果你不显式地实现该接口,则需要符合CLS标准的语言的最终用户无法使用你的对象。(如果BCL实现者没有隐藏原始类型的IConvertible成员,那将是非常灾难性的 :))

另一个有趣的注意事项是,通常使用这样的结构意味着明确实现接口的结构只能通过装箱到接口类型来调用它们。你可以通过使用泛型约束来解决这个问题:

void SomeMethod<T>(T obj) where T:IConvertible

如果你传递一个整数给它,它不会将其装箱。


1
你的约束条件中有一个拼写错误。为了澄清,上面的代码确实可以工作。它需要在接口方法签名的初始声明中。原始帖子没有指定这一点。此外,正确的格式是“void SomeMehtod<T>(T obj) where T:IConvertible”。请注意,在“)”和“where”之间有一个额外的冒号,不应该出现。尽管如此,对于巧妙地使用泛型来避免昂贵的装箱,还是要点个赞。 - Zack Jannsen
1
嗨,Michael B.那么为什么在.NET中的字符串实现中有公共的IComparable实现:public int CompareTo(Object value) { if (value == null) { return 1; } if (!(value is String)) { throw new ArgumentException(Environment.GetResourceString("Arg_MustBeString")); } return String.Compare(this,(String)value, StringComparison.CurrentCulture);} 谢谢! - zzfima
在泛型出现之前,string 就已经被广泛使用了。当 .net 2 出现时,他们不想破坏 string 的公共接口,所以他们保留了原样,并加入了保障措施。 - Michael B
@MichaelB 你好,能否详细解释一下为什么需要CLS兼容性的语言的最终用户如果您没有显式实现接口(如IConvertible),就无法使用您的对象呢? - user9623401
@MichaelB,MSDN文档中提到:“通常情况下,公共语言运行时通过Convert类公开IConvertible接口。公共语言运行时还在显式接口实现中内部使用IConvertible接口,以简化用于支持Convert类和基本公共语言运行时类型转换的代码。”但我不确定它如何简化用于支持Convert类中的转换的代码。 - user9623401

36

显式实现接口的一些附加原因:

向后兼容性:如果 ICloneable 接口发生更改,实现方法类成员不必更改其方法签名。

更干净的代码:如果从 ICloneable 中删除 Clone 方法,则会出现编译器错误,但是如果您隐式实现该方法,则可能会出现未使用的“孤立”公共方法。

强类型: 为了说明supercat的故事,这将是我首选的示例代码,显式实现 ICloneable 允许在直接调用MyObject 实例成员时使用强类型的 Clone() :

public class MyObject : ICloneable
{
  public MyObject Clone()
  {
    // my cloning logic;  
  }

  object ICloneable.Clone()
  {
    return this.Clone();
  }
}

对于这个问题,我更喜欢使用interface ICloneable<out T> { T Clone(); T self {get;} }。请注意,T上故意没有ICloneable<T>的约束。虽然通常只有在其基类可以安全克隆时才能安全地克隆对象,但有时我们可能希望从一个可以安全克隆的基类派生出一个无法安全克隆的类的对象。为了实现这一点,我建议不要让可继承的类公开克隆方法,而是让可继承的类具有一个protected的克隆方法,并且从它们派生的密封类可以公开克隆方法。 - supercat
当然,那样会更好,但是在BCL中没有ICloneable的协变版本,所以你需要创建一个,对吧? - Wiebe Tijsma
所有三个例子都依赖于不太可能的情况,并违反了接口最佳实践。 - user585968

13

另一种有用的技巧是使函数的公共方法实现返回比接口规定的更具体的值。

例如,一个对象可以实现 ICloneable 接口,但仍然可以让它公开可见的 Clone 方法返回其自身类型。

同样地,一个 IAutomobileFactory 可能会有一个返回 AutomobileManufacture 方法,但实现 IAutomobileFactoryFordExplorerFactory 可以让它的 Manufacture 方法返回一个 FordExplorer(它派生自 Automobile)。知道自己有一个 FordExplorerFactory 的代码可以使用由 FordExplorerFactory 返回的对象上的 FordExplorer 特定属性,而仅知道自己拥有某种类型的 IAutomobileFactory 的代码则只会将其返回视为 Automobile 处理。


3
这将是我首选的显式接口实现用法,不过一个小的代码示例可能比这个故事更清晰 :) - Wiebe Tijsma

7

当你有两个接口具有相同的成员名称和签名,但想要根据使用情况更改其行为时,它也是非常有用的。(我不建议编写这样的代码):

interface Cat
{
    string Name {get;}
}

interface Dog
{
    string Name{get;}
}

public class Animal : Cat, Dog
{
    string Cat.Name
    {
        get
        {
            return "Cat";
        }
    }

    string Dog.Name
    {
        get
        {
            return "Dog";
        }
    }
}
static void Main(string[] args)
{
    Animal animal = new Animal();
    Cat cat = animal; //Note the use of the same instance of Animal. All we are doing is picking which interface implementation we want to use.
    Dog dog = animal;
    Console.WriteLine(cat.Name); //Prints Cat
    Console.WriteLine(dog.Name); //Prints Dog
}

62
我见过的最奇怪的面向对象相关示例:public class Animal : Cat, Dog,意思是把Animal类声明为继承自CatDog类。 - mbx
38
如果Animal也实现了Parrot,它将成为一个多态的动物,叫做Polly-morphing animal。 - RenniePet
3
我记得有一个卡通角色,一端是猫,另一端是狗;-) - George Birbilis
2
80年代有一部电视连续剧叫做《人兽大战》。其中一个男人可以变形成为……哦,算了。 - bkwdesign
Morkies 有点像猫,也像忍者猫一样。 - samus
我知道这只是一个演示示例,但实际上什么时候会发生这种情况? - Eakan Gopalakrishnan

6

明确实现接口可以使公共接口更加清晰,例如您的File类可能会明确实现IDisposable并提供一个名为Close()的公共方法,这对消费者来说可能比Dispose()更有意义。

F# 仅提供明确实现接口,因此您始终需要将其转换为特定的接口才能访问其功能,这使得接口的使用非常明确。


我认为大多数版本的VB也只支持显式接口定义。 - Gabe
1
@Gabe - 对于VB来说,情况比那更微妙——实现接口的成员的命名和可访问性是独立的,与表明它们是实现的一部分无关。因此,在VB中,参考@Iain的答案(当前最佳答案),你可以使用公共成员“GoFast”和“GoSlow”分别实现IDoItFast和IDoItSlow。 - Damien_The_Unbeliever
2
我不喜欢你的特定示例(在我看来,唯一应该隐藏“Dispose”的是永远不需要清理的东西);更好的示例可能是不可变集合实现IList<T>.Add - supercat
你总是需要将对象转换为特定的接口才能访问其功能。我不明白为什么:只要你按照接口编程,而不是按照实现编程,使用你的对象的客户端就不需要知道它收到了哪个特定的实现,并且不需要进行强制类型转换,只需按照接口来使用即可。 - Arialdo Martini

5

显式实现的另一个原因是为了可维护性

当一个类变得“繁忙”时,拥有显式实现可以清楚地表明方法存在于其中以满足接口合同。

因此,它提高了代码的“可读性”。


在我看来,更重要的是决定类是否应该向其客户端公开方法。这决定了是显式还是隐式。为了记录几个方法彼此相关,例如因为它们满足一个合同 - 这就是 #region 的作用,需要适当的标题字符串。同时,在方法上添加注释。 - ToolmakerSteve

5
如果您有一个内部接口,不想公开实现类上的成员,则应该明确实现它们。隐式实现必须是公共的。

好的,这就解释了为什么项目无法使用隐式实现进行编译。 - pauloya

2
另一个例子是由 System.Collections.Immutable 提供的,在其中作者选择使用这种技术来保留集合类型的熟悉 API,同时去除对其新类型没有意义的接口部分。
具体来说,ImmutableList<T> 实现了 IList<T> 接口和 ICollection<T> 接口(为了 更方便地与旧代码一起使用),但是对于一个不可变的列表来说,void ICollection<T>.Add(T item) 没有意义:由于向不可变列表添加元素不应更改现有列表,因此 ImmutableList<T> 还继承自 IImmutableList<T>,其 IImmutableList<T> Add(T item) 可以用于不可变列表。
因此,在Add的情况下,ImmutableList<T>中的实现如下所示:
public ImmutableList<T> Add(T item)
{
    // Create a new list with the added item
}

IImmutableList<T> IImmutableList<T>.Add(T value) => this.Add(value);

void ICollection<T>.Add(T item) => throw new NotSupportedException();

int IList.Add(object value) => throw new NotSupportedException();

0

这是我们如何创建显式接口: 如果我们有两个接口,且两个接口都有相同的方法,并且一个类继承了这两个接口,那么当我们调用一个接口方法时,编译器会混淆应该调用哪个方法,因此我们可以使用显式接口来解决这个问题。 下面是一个示例。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace oops3
{
    interface I5
    {
        void getdata();    
    }
    interface I6
    {
        void getdata();    
    }

    class MyClass:I5,I6
    {
        void I5.getdata()
        {
           Console.WriteLine("I5 getdata called");
        }
        void I6.getdata()
        {
            Console.WriteLine("I6 getdata called");
        }
        static void Main(string[] args)
        {
            MyClass obj = new MyClass();
            ((I5)obj).getdata();                     

            Console.ReadLine();    
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接