为什么这个接口必须显式实现?

13

几年后回到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<T>的定义:
[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<T> 接口。从我的理解来看,如果'X'实现'Y',那么'X'就是一个'Y',因此 Collection<String> 就是一个 IEnumerable<String>,所以我不确定为什么它不能满足。


它并没有直接使用,而是有一个隐式转换(上转)被编译器转化为一定量的代码。 - Ben Voigt
“普通的Children属性不能满足要求吗?” - 不行。根据我的理解,实现必须完全匹配接口签名,包括返回类型。 - Thangadurai
1
C# 不支持返回类型协变。 - user4003407
1
@Guy:但是这样他就没有办法从类内部将Collection对象作为Collection使用了 :( - Ben Voigt
Damien,如果你能a)把那个放在一个答案里,并且b)从中摘录解释以上内容,我会将其标记为答案。 - Mark A. Donohoe
显示剩余4条评论
4个回答

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

我们不能写成这样:

public interface IDataItem {

    void DoStuff(string value);
}

public class DataItem : IDataItem {

    public void DoStuff(object value) { }
}

你的例子与此相同,只是你要求返回类型而不是参数(显然采用缩小转换而不是扩大转换)。尽管如此,相同的原则适用。当涉及匹配签名时,类型必须完全匹配3
你可以要求使用允许这种情况发生的语言,这样的语言可能存在。但事实是,这些是C#的规则。

1除了一些涉及泛型和接口/代理的协变和逆变的有限支持外。

2有人可能会争论在这里使用"签名"这个词是否恰当,因为在这种情况下,返回类型与参数类型、泛型数量等同样重要;在大多数其他情况下,当某人谈论C#方法签名时,他们会明确地忽略返回类型,因为他们(明示或暗示地)在考虑什么是"重载"规则,而对于重载,返回类型不是签名的一部分。

尽管如此,我对我在这里使用"签名"这个词感到满意。在C#规范中,签名没有正式定义,它经常用来指出哪些签名部分不能考虑重载。

3更不用说如果您的Children方法返回了一个实现了IEnumerable<string>struct,那么会引发哪些问题。现在您有一个返回值类型的方法和一个调用者(通过IDataItem接口),他们期望接收到一个对象的引用

所以甚至不能直接使用该方法。我们必须(在这种情况下)进行隐藏的装箱转换来实现接口。当C#的这部分规范被制定时,我相信他们试图避免对您可以轻松编写的代码进行过多的“隐藏魔法”。


1
“我们”是谁?我理解你的意思是“事情就是这样”,你是对的。我认为OP真正想问的是“为什么不允许在接口实现中使用逆变返回类型?”答案“它们只是不能”虽然事实上是正确的,但却没有抓住重点。至于为什么语言没有以这种方式创建,我只能进行推测。 - Jodrell
@Jodrell - 这是许多“为什么”问题的普遍问题。我能做的最好的事情就是指出,这不是一些意外的 emergent property,而是真正融入到语言中的东西。 - Damien_The_Unbeliever
这并不完全相同。我的问题是关于类在内部满足接口中的getter。Collection<string> IEnumerable<string>,因此任何需要接口并期望IEnumerable<string>的地方总是可以使用Collection<string>来满足接口。您的示例不是getter,而是setter(技术上带有参数的方法),这意味着它在外部被传递了一个“object”,并且不能保证被传递的是string,因此无法满足接口。我同意您发布的内容由于这个原因不应该起作用,但再次强调,这并不相同。 - Mark A. Donohoe
@MarkA.Donohoe - 我已经希望在代码示例下面的段落和脚注3中清楚地表明,返回类型与参数类型适用于完全相同的参数 - Damien_The_Unbeliever
但这并不是一个逻辑限制,而是编译器的限制。比如,Swift 就可以很好地处理这个问题。你甚至可以使用 getter/setter 属性来满足只读接口(即 Swift 术语中的协议)。这是因为在一方面,你是给别人提供东西,而在另一方面,别人是基于某些条件给你提供东西。前者完全由你掌控,但后者不是。前者是100%可预测和“已知”的,如果已知它满足要求,那么编译器应该允许它。我知道它不行。这只是令人沮丧的事情。 - Mark A. Donohoe

5
在您的示例中,“normal”Children属性实际上不符合接口要求。类型是不同的,即使您可以将其强制转换 - 它们仍然不同。
类似的示例可能更明显,如果您使用实际方法返回IEnumerable来实现接口并尝试从实际类中使用ICollection方法,则仍会出现编译时错误。
正如@Ben Voigt所说,转换仍会生成一些代码,如果您想要它 - 您需要隐式添加它。

但是Collection<String>并没有直接实现IEnumerable<String>,因此它不满足要求,这是正确的答案。但你能举一个例子说明隐式地这样做不好的情况吗?有什么不利之处呢? - Mark A. Donohoe
可以这样论述:“如果‘X’实现了‘Y’,那么从所有意义上来说,‘X’就是一个‘Y’!”因此,我仍然认为这应该被隐式地捕捉到。 - Mark A. Donohoe
@MarqueIV:编译器可以自动生成两个版本,一个返回IEnumerable以满足接口,另一个返回Collection以匹配您声明的签名。C++就是这样做的,这就是为什么我们说它支持返回类型协变。.NET选择不这样做,也许是因为额外的技巧会通过反射变得可见,并且会使试图使用动态绑定的人们感到困惑(而C++则不必担心)。 - Ben Voigt
但是,如果我错了,请纠正我,返回类型 IEnumerable<string>,因为Collection<string>直接实现它。这不是隐式的。我认为协变是另一回事。 - Mark A. Donohoe
1
编译器本来可以做到这一点,但它没有这样做,问题是为什么。没有权威来源,我们只能猜测。我们需要设计团队中有一个记日记的人来回答。答案可能是“我们认为这会使事情变得太难理解”,或者“这很困难,我们认为这不值得努力”,或者两者的结合。 - Jodrell
只是重新审视一下这个问题。请记住,这明确指的是getter。编译器不需要导出两种情况,只需要一种,因为Collection<string> 一个IEnumerable<string>。编译器没有任何“额外”的东西要发出。再看看我的代码。没有强制转换。在类型为Collection<string>的getter永远不可能无法满足IEnumerable<string>的getter的情况下,永远不会出现这种情况。当然,对于setter来说并非如此。这在接口中期望任何IEnumerable<string>的setter不能被仅接受Collection<string>的setter所满足,这是有道理的。 - Mark A. Donohoe

0
问题已经在这里得到了回答(您必须匹配接口类型),我们可以通过一个示例来演示问题。如果这样做有效:
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 类的属性 - 适当地为你的接口命名。


2
x.Children = new Queue<string>(); 需要 Children 具有一个 setter。 - Chris
没错。如果你实现了一个setter,你就不能使用Collection作为后备存储,因为正如你所指出的,没有保证该类型会有ClearItems属性。但在这种情况下,没有setter,因此该类型只能是Collection,因此我认为它应该隐式地实现它。 - Mark A. Donohoe
我想,为了让这个功能百分之百地正常工作,编译器内部可能需要进行相当多的工作。如果您仍然认为,如果您希望对象公开除接口属性以外的其他内容,则可能使用不当。 - Paddy
不能说我同意最后那个陈述。仅仅因为某个东西实现了一个接口并不意味着它只是这个接口。相反,仅仅因为某个东西可以满足一个接口并不意味着这个接口拥有它。首先,Children是类的一个属性。事实上,即使你删除这个接口要求,Children仍然适用,这只是一个愉快的巧合,因为它也用于该接口。(请记住,这是简化的代码,以说明问题。) - Mark A. Donohoe

-1

实现该接口的任何类都必须包含与接口指定的签名匹配的方法定义。接口仅定义签名。这种方式下,C#中的接口类似于所有方法均为抽象的抽象类。

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

关于接口的好文章在这里。 C#中的接口

希望它能帮助您进一步理解。


3
问题不在于什么是接口,而是Collection<String>已经实现了IEnumerable<String>(至少我是这样认为的),这意味着它会被IDataItem隐式地选中,但事实并非如此。明白我的意思吗? - Mark A. Donohoe

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