明确标记派生类实现基类接口

28
interface IBase
{
    string Name { get; }
}

class Base : IBase
{
    public Base() => this.Name = "Base";
    public string Name { get; }
}

class Derived : Base//, IBase
{
    public Derived() => this.Name = "Derived";
    public new string Name { get; }
}


class Program
{
    static void Main(string[] args)
    {
        IBase o = new Derived();
        Console.WriteLine(o.Name);
    }
}
在这种情况下,输出将是“Base”。 如果我明确声明Derived实现IBase(事实上已经由基类Base实现,并且这样的注释似乎是无用的),输出将是“Derived”
class Derived : Base, IBase
{
    public Derived() => this.Name = "Derived";
    public new string Name { get; }
}
为什么会出现这种行为?VS 15.3.5,C# 7。

2
为什么它应该表现得不同呢?你的期望是什么? - H H
1
期望 - 相同的输出,访问相同的成员。我不明白为什么在类定义中添加接口,当基类已经实现了这样的接口时,会改变事情。 - yevgenijz
1
你理解 public new string Name { get; } 对于 Base 上的 Name 做了什么吗? - James Thorpe
嵌套类的初始化很重要。 - Renatas M.
1
IBase o = new Derived(); 中,编译器有两个选择,它会选择最佳匹配。 - H H
显示剩余2条评论
2个回答

25

这在C# 5规范的13.4.4至13.4.6节中有解释。下面引用了相关部分,但基本上,如果您明确声明一个类实现了一个接口,则会再次触发接口映射,因此编译器会将类视为用于确定每个接口成员映射到哪个实现的类。

13.4.4 Interface mapping

A class or struct must provide implementations of all members of the interfaces that are listed in the base class list of the class or struct. The process of locating implementations of interface members in an implementing class or struct is known as interface mapping.

Interface mapping for a class or struct C locates an implementation for each member of each interface specified in the base class list of C. The implementation of a particular interface member I.M, where I is the interface in which the member M is declared, is determined by examining each class or struct S, starting with C and repeating for each successive base class of C, until a match is located:

  • If S contains a declaration of an explicit interface member implementation that matches I and M, then this member is the implementation of I.M.
  • Otherwise, if S contains a declaration of a non-static public member that matches M, then this member is the implementation of I.M. If more than one member matches, it is unspecified which member is the implementation of I.M. This situation can only occur if S is a constructed type where the two members as declared in the generic type have different signatures, but the type arguments make their signatures identical.

...

13.4.5 Interface implementation inheritance

A class inherits all interface implementations provided by its base classes. Without explicitly re-implementing an interface, a derived class cannot in any way alter the interface mappings it inherits from its base classes. For example, in the declarations

interface IControl
{
    void Paint();
}
class Control: IControl
{
    public void Paint() {...}
}
class TextBox: Control
{
    new public void Paint() {...}
}

the Paint method in TextBox hides the Paint method in Control, but it does not alter the mapping of Control.Paint onto IControl.Paint, and calls to Paint through class instances and interface instances will have the following effects

Control c = new Control();
TextBox t = new TextBox();
IControl ic = c;
IControl it = t;
c.Paint();            // invokes Control.Paint();
t.Paint();            // invokes TextBox.Paint();
ic.Paint();           // invokes Control.Paint();
it.Paint();           // invokes Control.Paint();

...

13.4.6 Interface reimplementation

A class that inherits an interface implementation is permitted to re-implement the interface by including it in the base class list.

A re-implementation of an interface follows exactly the same interface mapping rules as an initial implementation of an interface. Thus, the inherited interface mapping has no effect whatsoever on the interface mapping established for the re-implementation of the interface. For example, in the declarations

interface IControl
{
    void Paint();
}
class Control: IControl
{
    void IControl.Paint() {...}
}
class MyControl: Control, IControl
{
    public void Paint() {}
}

the fact that Control maps IControl.Paint onto Control.IControl.Paint doesn’t affect the re-implementation in MyControl, which maps IControl.Paint onto MyControl.Paint.


你的介绍比引用更具解释性。但是有一个问题,当你说“再次触发接口映射”时,这里的“再次”是什么意思?接口映射的顺序是什么,不是从子类到父类,就像构造函数被触发的顺序一样吗?这意味着第一个接口映射只是“获胜”了吗? - nl-x
感谢提供如此详尽的答案。令人困惑的是,可以通过将接口混入类定义中,在两个类(Base、Derived)中都不必显式地实现接口就可以重新实现接口(从语法上)。 - yevgenijz
我的意思是在编译Base时执行接口映射,然后在编译Derived时再次执行。这是一种编译时选择,因此它没有执行顺序...我不确定您的问题是什么。 - Jon Skeet
我不应该提到构造函数的(运行时)执行顺序,那很令人困惑。但如果我理解正确的话,接口成员映射到父类成员和子类成员。这个映射会被覆盖吗?还是两个映射都存在,当调用时子映射“获胜”? - nl-x
1
@nl-x:它不会同时映射到两个 - 对于 Derived 而言,映射是从头开始完成的,因为它明确表示实现了该接口。你可以认为每个类都对其实现的所有接口(包括继承的接口)进行了完整的映射;如果一个类没有声明实现某个接口,则映射是从基类继承而来的。 - Jon Skeet

1
如果派生类(Derived)没有实现基接口(IBase),并声明了新的名称属性(new string Name),那么就意味着 Derived.Name 和 IBase.Name 在逻辑上不同。因此,当你访问 IBase.Name 时,它会在实现 IBase 的 Base 类中查找。如果你删除 new string Name 属性,则输出结果将是 Derived,因为此时Derived.Name = Base.Name = IBase.Name。如果你显式地实现了 IBase,输出结果也将是 Derived,因为此时Derived.Name = IBase.Name。如果你将 o 强制转换为 Derived,则输出结果也将是 Derived,因为此时你正在访问 Derived.Name 而不是 IBase.Name。

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