• 基类与接口混合继承的声明问题 [C#, Design]


    基类与接口混合继承的声明问题 [C#, Design]

    Updated on Friday, November 19, 2004

    Written by Allen Lee

    1. 问题初现

    今天,查看《接口继承的声明问题》一文的反馈,发现Ninputer留下这样一道题:

    如果有

    class A : Interface1

    那么

    class B : A, Inteface1

    class B : A

    会出现什么不同的情况呢。编译器在IL级别是用什么手段实现这个功能的呢?

    2. 探索问题 & 理解问题

    解决问题的过程既是一个探索的过程也是一个推理论证的过程。OK,下面我尝试用反证法来探索这个问题。

    首先,我假设问题中B类的两种继承方式有着一样的效果,并试着寻找它们的不一样。为了了解这两种方式的效果,我把上面代码补充完整:

    interface IC { }

    class A : IC { }

    class B1 : A { }

    class B2 : A, IC { }

    class Program
    {
        
    static void Main()
        
    {
            A a 
    = new A();
            B1 b1 
    = new B1();
            B2 b2 
    = new B2();

            Console.WriteLine(a 
    is IC);
            Console.WriteLine(b1 
    is A);
            Console.WriteLine(b1 
    is IC);
            Console.WriteLine(b2 
    is A);
            Console.WriteLine(b2 
    is IC);

        }

    }

    代码运行的结果是:

    • True
    • True
    • True
    • True
    • True

    我们对此结果毫无疑问,那么这是否代表着B1和B2之间没有区别?如果上面的代码作为推理前提在客观上已经足够充分,那么答案是肯定的。但我无法知道论据是否已经达到充分的程度。于是,我把上面的代码修改一下,为类和接口其添加一些成员并观察一下它们所表现出来的行为:

    interface IC
    {
        
    void M();
    }


    class A : IC
    {
        
    void IC.M()
        
    {
            Console.WriteLine(
    "In class A");
        }

    }


    class B1 : A { }

    class B2 : A, IC { }

    class Program
    {
        
    static void Main()
        
    {
            List
    <IC> cs = new List<IC>();
            cs.Add(
    new A());
            cs.Add(
    new B1());
            cs.Add(
    new B2());

            
    foreach (IC c in cs)
                c.M();
        }

    }

    程序能够正常编译,运行结果是:

    • In class A
    • In class A
    • In class A

    OH, MY GOD! 怎么效果又一样!难道B1跟B2真的没区别??我再把代码修改一下:

    interface IC
    {
        
    void M();
    }


    class A : IC
    {
        
    void IC.M()
        
    {
            Console.WriteLine(
    "In class A");
        }

    }


    class B1 : A
    {
        
    void IC.M()
        
    {
            Console.WriteLine(
    "In class B1");
        }

    }


    class B2 : A, IC
    {
        
    void IC.M()
        
    {
            Console.WriteLine(
    "In class B2");
        }

    }

    Oh,代码无法编译,编译器发脾气了:

    'B1.IC.M()': containing type does implement interface 'IC'

    换句话,我们不能再B1里面重新实现IC.M方法,我们只能默默地接受从继类继承而来的那一个了!再修改一下:

    interface IC
    {
        
    void M();
    }


    class A : IC
    {
        
    void IC.M()
        
    {
            Console.WriteLine(
    "In class A");
        }

    }


    class B1 : A { }

    class B2 : A, IC
    {
        
    void IC.M()
        
    {
            Console.WriteLine(
    "In class B2");
        }

    }


    class Program
    {
        
    static void Main()
        
    {
            List
    <IC> cs = new List<IC>();
            cs.Add(
    new A());
            cs.Add(
    new B1());
            cs.Add(
    new B2());

            
    foreach (IC c in cs)
                c.M();
        }

    }

    这些编译正常通过了,得到的结果是:

    • In class A
    • In class A
    • In class B2

    3. 得出结论 & 新问题展现

    好吧,有结果了,B1和B2两种继承方式的效果的确不同,具体体现在多态行为上(有关多态的介绍,你可以参见《今天你多态了吗?》一文)。B1是个可怜虫,它必须接受A对IC.M的实现,无法改变这种命运;然而B2就不同,它有权选择接受还是拒绝,当然,拒绝的条件是提供有自己特色的实现。

    4. 探索新问题 & 解决新问题

    那么,我们如何纠正这种非预期的多态行为呢?一个简单的回答就是把B1的声明改成跟B2的一样。但这样,所有继承于A的派生类都必须照做,没得商量!还有其他的办法吗?有的,请先看如下代码:

    interface IC
    {
        
    void M();
    }


    class A : IC
    {
        
    void IC.M()
        
    {
            
    this.M();
        }


        
    public virtual void M()
        
    {
            Console.WriteLine(
    "In class A");
        }

    }


    class B1 : A
    {
        
    public override void M()
        
    {
            Console.WriteLine(
    "In class B1");
        }

    }


    class B2 : A, IC
    {
        
    public override void M()
        
    {
            Console.WriteLine(
    "In class B2");
        }

    }


    class Program
    {
        
    static void Main()
        
    {
            List
    <IC> cs = new List<IC>();
            cs.Add(
    new A());
            cs.Add(
    new B1());
            cs.Add(
    new B2());

            
    foreach (IC c in cs)
                c.M();
        }

    }

    运行结果为:

    • In class A
    • In class B1
    • In class B2

    这样,多态的效果就如我们所愿了!当然,现在B2声明中的IC又显得有点多余了,但你可以轻松把它拿掉!另外,如果测试程序换成:

    class Program
    {
        
    static void Main()
        
    {
            List
    <A> ace = new List<A>();
            ace.Add(
    new A());
            ace.Add(
    new B1());
            ace.Add(
    new B2());

            
    foreach (A a in ace)
                a.M();
        }

    }

    结果还是一样!

    5. 是的,我说谎了。[New]

    或许你已经注意到,在上面的整个过程中,我做了一个最大的假设,那就是我可以任我喜欢修改A的源代码!也因为这样,我可以轻松的纠正这些非预期的多态行为。但实际的情况是,我们不会每次都那么幸运。如果我们仅仅得到一个包含类A和接口IC的程序集呢?那么,我们就需要使用到接口的重新映射了。实际上,B2就是使用这种技巧。还是让我们来看看具体的情况:

    1. 接口IC的规格不变。
    2. 我们只知道类A的声明以及它的成员列表和对应的输出:
    Class
    class A : IC
    Output
    Method
    public void M();
    In class A
    Method
    void IC.M();
    In class A

    现在我需要实现一批继承于A的派生类,但我不希望同时继承A的对应方法的实现,我该怎么做?很简单,首先创建一个类AX继承自类A和接口IC,并在AX里面处理好相关的事宜,接着让那批派生类继承于AX:

    class AX : A, IC
    {
        
    // 这里使用new是声明其与基类的同名方法M没有任何瓜葛。
        
    // 使用virtual是为后代的继承打下铺垫。

        
    public new virtual void M()
        
    {
            Console.WriteLine(
    "In class AX");
        }


        
    void IC.M()
        
    {
            
    this.M();
        }

    }


    class B1 : AX
    {
        
    public override void M()
        
    {
            Console.WriteLine(
    "In class B1");
        }

    }


    class B2 : AX
    {
        
    public override void M()
        
    {
            Console.WriteLine(
    "In class B2");
        }

    }

    好吧,然我们来看看测试程序:

    class Program
    {
        
    static void Main(string[] args)
        
    {
            List
    <IC> cs = new List<IC>();
            cs.Add(
    new A());
            cs.Add(
    new AX());
            cs.Add(
    new B1());
            cs.Add(
    new B2());

            
    foreach (IC c in cs)
                c.M();

            Console.WriteLine();

            List
    <AX> ace = new List<AX>();
            ace.Add(
    new AX());
            ace.Add(
    new B1());
            ace.Add(
    new B2());

            
    foreach (AX a in ace)
                a.M();

            Console.ReadLine();
        }

    }

    我想你已经猜到运行结果了:

    • In class A
    • In class AX
    • In class B1
    • In class B2
    • In class AX
    • In class B1
    • In class B2

    好吧,你辛苦了,如果还没有头晕的话,请再听我说一句。接口重新映射究竟是一个问题还是一种技巧,那要看你实际遭遇的情况。如果你能够灵活运用的话,它的确会为你带来巨大的便利!

    6. 继承问题的一些易混淆的地方

    请留意下面的代码:

    interface IC1 { }

    interface IC2 : IC1 { }

    class A1 : IC1 { }

    class A2 : IC1, IC2 { }

    class B1 : A1 { }

    class B2 : A1, IC1 { }

    其中,A1和A2是没有实质的区别的,详细请看《接口继承的声明问题》一文;而B1和B2却在某些场合表现出不同的行为,为何B1和B2会有这种差异,相信现在的你应该有所了解了吧!

    7. IL呢?[Updated]

    噢,对了,Ninputer的问题还有个“编译器在IL级别是用什么手段实现这个功能的呢?”!如果你看完本文后还嫌不够,希望更加深入了解一下IL层次上,CLR是怎样实现接口重新映射的原理的话,我推荐你阅读《接口映射的实现及原理》

  • 相关阅读:
    Contest (树状数组求逆序对)
    树状数组
    unity3D 笔记 (NENE QUEST 制作中用到的函数)
    Ubuntu 安装gnome桌面及vnc远程连接
    Pillow图像处理
    室内场景数据集
    PyTorch踩坑笔记
    进一步了解pip
    一些概念
    损失函数及评价指标
  • 原文地址:https://www.cnblogs.com/allenlooplee/p/64553.html
Copyright © 2020-2023  润新知