【问题标题】:Why does this interface have to be explicitly implemented?为什么这个接口必须显式实现?
【发布时间】:2018-07-30 05:13:57
【问题描述】:

几年后回到 C#,所以我有点生疏了。遇到这个(简化的)代码,让我头疼。

为什么必须显式实现IDataItem.Children 属性?普通的Children属性不满足要求吗?毕竟,属性是直接用来满足它的。为什么不隐含?

public interface IDataItem {

    IEnumerable<string> Children { get; }
}

public class DataItem : IDataItem {

    public Collection<string> Children { get; } = new Collection<string>();

    // Why doesn't 'Children' above implement this automatically?!
    // After all it's used directly to satisfy the requirement!
    IEnumerable<string> IDataItem.Children => Children;
}

根据C#源码,这里是Collection&lt;T&gt;的定义:

[System.Runtime.InteropServices.ComVisible(false)]
public class Collection<T> :
    System.Collections.Generic.ICollection<T>,
    System.Collections.Generic.IEnumerable<T>, <-- Right Here
    System.Collections.Generic.IList<T>,
    System.Collections.Generic.IReadOnlyCollection<T>,
    System.Collections.Generic.IReadOnlyList<T>,
    System.Collections.IList

如您所见,它明确实现了IEnumerable&lt;T&gt;,据我了解,如果“X”实现了“Y”,那么“X”一个“Y”,所以Collection&lt;String&gt; IEnumerable&lt;String&gt;,所以我不知道为什么不满意。

【问题讨论】:

  • 它不是“直接”使用的,有一个隐式转换(向上转换),编译器会将其转换为非零代码量。
  • Doesn't the normal Children property satisfy the requirement? - 不。根据我的理解,实现必须与接口签名完全匹配,包括返回类型。
  • C# 不支持返回类型协方差。
  • 你不必明确地使用IDataItem.Childrenpublic IEnumerable&lt;string&gt; Children =&gt; new Collection&lt;string&gt;(); 就足够了。
  • @Guy:但是他无法从类内部将 Collection 对象用作 Collection :(

标签: c# interface


【解决方案1】:

也许这个例子更清楚了。我们希望签名完全匹配1,2,不允许替换,尽管类型之间存在任何继承关系。

我们不能这样写:

public interface IDataItem {

    void DoStuff(string value);
}

public class DataItem : IDataItem {

    public void DoStuff(object value) { }
}

您的示例是相同的,除了您要求返回类型而不是参数(并且出于明显的原因使用缩小而不是扩大转换)。尽管如此,同样的原则也适用。在匹配签名方面,类型必须完全匹配3

您可以要求一种允许此类事情发生的语言,并且此类语言可能存在。但事实是,这些都是 C# 的规则。


1除了对Co- and Contra-variance 的一些有限支持之外,涉及泛型 和接口/代理。

2有些人可能会争论签名是否是在这里使用的正确词,因为在这种情况下,返回类型与参数类型、泛型数量等一样重要;在大多数其他有人谈论 C# 方法签名的情况下,他们会显式忽略返回类型,因为他们(显式或隐式)考虑 重载 规则所说的内容,并且对于重载,返回类型不是签名的一部分。

不过,我很高兴在这里使用“签名”一词。签名没有在 C# 规范中正式定义,并且在使用它时,通常会指出签名的哪些部分考虑重载。

3更不用说如果您的Children 方法返回一个恰好实现IEnumerable&lt;string&gt;struct 会引发的问题。现在你有了一个返回值类型和调用者值的方法(通过IDataItem 接口,它期望接收到一个对象的引用

因此,甚至不能按原样使用该方法。我们必须(在这种情况下)有隐藏装箱转换来实现接口。当指定 C# 的这一部分时,我相信,他们会尽量不让你自己编写的代码有太多“隐藏的魔法”。

【讨论】:

  • 谁是“我们”?我明白你在说“就是这样”,你是对的。我认为 OP 真的在问“为什么不允许接口实现上的逆变返回类型?”答案“他们只是不是”虽然实际上是正确的,但没有抓住重点。我自己,我只能推测为什么语言不是这样创建的。
  • @Jodrell - 这是 lotWhy 问题的普遍问题。我能做的最好的事情就是指出这不是偶然出现的属性,而是真正融入语言的东西。
  • 不太一样。我的问题是关于从内部满足接口中的吸气剂的类。 Collection&lt;string&gt; 一个IEnumerable&lt;string&gt;,因此任何使用接口并期望IEnumerable&lt;string&gt;的地方总是Collection&lt;string&gt;感到满意。您的示例不是getter,而是setter(技术上是带有参数的方法),这意味着它在外部传递了一个“对象”,并且不能保证它传递的是一个字符串,因此它不能满足接口。我同意您发布的内容不应该因此而起作用,但同样,它不一样。
  • @MarkA.Donohoe - 我希望我已经在代码示例下方的段落和脚注 3 中明确说明 exact same arguments 适用于返回类型至于参数类型。
  • 但这不是逻辑限制。它是一个编译器。例如,Swift 处理得很好。您甚至可以使用 getter/setter 属性来满足仅 getter 的接口(即 Swift 用语中的协议。)这是因为一方面你是给别人东西的人,而另一方面是别人给根据某些条件给你一些东西。您可以完全控制前者,但不能控制后者。前者是 100% 可预测和“已知的”,如果已知满足它,编译器真的应该允许它。我知道它没有。真是令人沮丧。
【解决方案2】:

在您的示例中,您的“正常” Children 属性实际上并不满足接口要求。类型不同。你可以施放它并不重要 - 它们是不同的。

类似的例子,也许更明显的是,如果您要使用返回 IEnumerable 的实际方法实现接口,并尝试从实际类中使用 ICollection 方法。仍然存在编译时错误。

正如@Ben Voigt 所说,转换仍然会生成一些代码,如果你想拥有它 - 你需要隐式添加它。

【讨论】:

  • 但是Collection&lt;String&gt;不是直接实现IEnumerable&lt;String&gt;,所以应该满足要求吧?答案当然是“不,不是!”但是你能举一个例子说明这不是一件好事吗?缺点是什么?
  • 并且可以提出论点'如果'X'实现'Y',那么出于所有意图和目的,'X'一个'Y'!'因此,我仍然觉得应该隐含地接受它。
  • @MarqueIV:好吧,编译器可以自动生成两个版本,一个返回IEnumerable 来满足接口,一个返回Collection 来匹配你声明的签名。 C++ 做到了这一点,这就是为什么我们说它支持返回类型协方差。 .NET 选择不这样做,可能是因为额外的诡计会通过反射变得可见,并让尝试使用动态绑定的人感到非常困惑(C++ 不必担心)。
  • 但是,如果我错了,请纠正我,返回类型 IEnumerable&lt;string&gt; 因为Collection&lt;string&gt; 直接实现它。这不是隐含的。我认为协方差是不同的。
  • 编译器可以做到这一点,但它没有,问题是为什么。如果没有权威来源,我们只能推测。我们需要设计团队中的某个人,他记着一本好日记,来插话和回答。答案可能是“我们认为这会让事情变得难以理解”或“这很困难,我们认为不值得努力”或两者兼而有之。
【解决方案3】:

这里已经回答了这个问题(你必须匹配接口类型),我们可以用一个例子来演示这个问题。如果这有效:

public interface IDataItem {

    IEnumerable<string> Children { get; set; }

    void Wipe();
}

public class DataItem : IDataItem {

    public Collection<string> Children { get; } = new Collection<string>(); 

    public void Wipe() {
        Children.ClearItems();  //Property exists on Collection, but not on IEnumerable
    }
}

然后我们像这样使用这段代码:

IDataItem x = new DataItem();

//Should be legal, as Queue implements IEnumerable. Note entirely sure
//how this would work, but this is the system you are asking about.
x.Children = new Queue<string>();

x.Wipe();  // Will fail, as Queue does not have a ClearItems method within

您的意思是该属性只能是可枚举的,或者您需要 Collection 类的属性 - 适当地键入您的接口。

【讨论】:

  • x.Children = new Queue&lt;string&gt;(); 需要Children 才能拥有一个二传手。
  • 没错。而且,如果您实现了一个 setter,则不能将 Collection 用作后备存储,因为正如您所指出的那样,不能保证该类型将具有 ClearItems 属性。但是在这种情况下,没有setter,因此类型只能是Collection,因此它应该隐式实现它恕我直言。
  • 我想,在编译器中需要做相当多的工作才能让它在 100% 的时间里正常工作。如果仍然认为如果您希望您的对象公开接口属性以外的其他内容,那么您可能使用错了。
  • 不能说我同意最后的说法。仅仅因为某些东西实现了一个接口并不意味着它就是唯一的东西。相反,仅仅因为某些东西可以实现一个接口并不意味着该接口拥有它。首先,孩子是班级的财产。它也用于该接口的事实只是一个愉快的巧合,因为即使您删除了该接口要求,Children 仍然适用。 (请记住,这是用于说明问题的简化代码。)
【解决方案4】:

任何实现接口的类都必须包含与接口指定的签名相匹配的方法定义。该接口仅定义签名。这样一来,C# 中的接口类似于抽象类,其中所有方法都是抽象的。

接口可以包含方法、属性、事件、索引器或这四种成员类型的任意组合。

这是关于接口的好读物。 Interfaces in C#

希望能帮助你进一步理解。

【讨论】:

  • 问题不在于接口是什么。正是 Collection 已经实现了 IEnumerable (或者我认为),AFAIT 意味着它将被 IDataItem 隐式拾取,但事实并非如此。有意义吗?
猜你喜欢
  • 2012-12-07
  • 2011-05-05
  • 2010-09-29
  • 2019-10-09
  • 1970-01-01
  • 2013-11-17
  • 1970-01-01
相关资源
最近更新 更多