代码之家  ›  专栏  ›  技术社区  ›  MartyIX

C中多个演员的规则是什么?

  •  3
  • MartyIX  · 技术社区  · 15 年前

    我有这个代码片段,我想知道为什么输出如下面的注释所写:

    interface I {   
        void m1();
        void m2();      
        void m3();      
    }
    
    class A : I {
        public void m1() { Console.WriteLine("A.m1()"); }
            public virtual void m2() { Console.WriteLine("A.m2()"); }
            public virtual void m3() { Console.WriteLine("A.m3()"); }
    }
    
    class C : A, I {
        public new void m1() { Console.WriteLine("C.m1()"); }
        public override void m2() { Console.WriteLine("C.m2()"); }
        public new void m3() { Console.WriteLine("C.m3()"); }
    }
    
    ------
    C c = new C();
    
    ((I) ((A) c)).m1();  //"C.m1()"
    ((I) ((A) c)).m2();  //"C.m2()"
    ((I) ((A) c)).m3();  //"C.m3()"
    

    对产出的最初猜测是:

    A. M1();
    c.m2();
    A. M3();

    4 回复  |  直到 12 年前
        1
  •  1
  •   Anton Gogolev    15 年前

    所有这些类型转换都是多余的,我确信编译器会对它们进行优化。

    这就是我的意思。 (A) c 是多余的,因为 C A 我们只剩下 (I)c ,这也是多余的,因为 C 器具 I . 因此,我们只有一个 C 类,编译器为其应用常规解析规则。

    编辑

    结果,我完全错了。 This 文档描述了发生的情况:

    类或结构的接口映射 C 定位在的基类列表中指定的每个接口的每个成员的实现 C . 特定接口成员的实现 I.M 在哪里 是成员所在的接口 M 通过检查每个类或结构来确定 S 开始 C 对每个连续的 C ,直到找到匹配项:

    • 如果 S 包含与匹配的显式接口成员实现的声明 ,则此成员是 I.M .
    • 否则,如果 S 包含与匹配的非静态公共成员的声明 ,则此成员是 I.M .
        2
  •  4
  •   Sergey Teplyakov    15 年前

    将C类改为:

    class C : A {
        public new void m1() { Console.WriteLine("C.m1()"); }
        public override void m2() { Console.WriteLine("C.m2()"); }
        public new void m3() { Console.WriteLine("C.m3()"); }
    }
    

    杰弗里·里克特的解释:

    C编译器要求实现接口的方法标记为public。这个 clr要求将接口方法标记为virtual。如果你没有明确的标记 方法在源代码中是虚拟的,编译器将该方法标记为虚拟的和 密封;这将防止派生类重写接口方法。如果你明确 将该方法标记为虚方法,编译器将该方法标记为虚方法(并保留它 未密封);这允许派生类重写接口方法。

    如果接口方法是密封的,则派生类不能重写该方法。然而,A 派生类可以重新继承同一个接口,并且可以为 接口的方法。当对对象调用接口的方法时,实现 调用与对象类型关联的。

    因为C类也实现了I接口,比我们在C对象上调用接口方法时,我们将调用与对象类型对应的实现被调用(即C类方法),而不是从C的基类(而不是类方法)派生的方法。

        3
  •  1
  •   Eric Lippert    15 年前

    我认为您误解了Cast运算符对引用转换的作用。

    假设您引用了堆栈上的C实例。这是一组特定的位。你把堆栈上的东西投射到A上。位会改变吗?不,没什么变化。它是对同一对象的相同引用。现在你把它投给我。这次比特会改变吗?不。 相同的位。相同的参考。相同的对象。

    通过类似这样的强制转换进行的隐式引用转换 编译程序 使用不同的规则 在编译时找出要调用的方法 .

    所以对“a”的强制转换是完全不相关的,被编译器忽略了。编译器知道或关心的只是您有一个I类型的表达式,并且您正在对它调用一个方法。编译器生成一个调用,该调用表示“在运行时,查看堆栈上的对象引用,并调用对象“i.m1”槽中的任何对象。

    解决这个问题的方法是考虑插槽。每个接口和类都定义了一定数量的“插槽”。在运行时,类的每个实例都有这些槽,其中填充了对方法的引用。编译器生成的代码说“调用该对象的槽3中的任何内容”,这就是运行时所做的——在槽中查找,调用那里的内容。

    在您的示例中,有各种各样的槽。接口需要三个槽,基类提供更多,派生类的“新”方法提供更多。构造派生类的一个实例时,将填充所有这些槽,并且可以理解,与i关联的槽中填充了派生类的匹配成员。

    这有道理吗?

        4
  •  0
  •   Mvt    15 年前

    铸造并不重要,因为C已经包含A和I。

    对于((i)((a)c)).m1()和((i)((a)c)).m3():
    -“new”关键字隐藏a.m1实现(实际上,如果没有new关键字,则会收到警告)。因此选择C.M1和C.M3。

    对于((i)((a)c)).m2():
    -覆盖注意选择了C.M2实现。