接口继承的声明问题

148 篇文章 1 订阅

接口继承的声明问题

http://www.cnblogs.com/allenlooplee/archive/2004/11/16/64394.html

某天,小新问我这样一个问题:

类System.Collections.CollectionBase是 从IList、ICollection继承而来,IList是从ICollection和IEnumerable继承而来,那 CollectionBase为什么还要从ICollection继承呢?

我们先来看看这些类和接口在MSDN文档中的声明:

public interface IEnumerable

public interface ICollection : IEnumerable

public interface IList : ICollection, IEnumerable

public abstract class CollectionBase : IList, ICollection, IEnumerable

根据接口继承的规则,我们知道CollectionBase只需要声明实现IList,就必须同时实现ICollection,也就必须实现IEnumerable,那么,我们为什么还要明确地把所有的这些接口都写下来呢?

换句话说,下面两种声明没有实质的区别:

// Code #1

public interface IEnumerable

public interface ICollection : IEnumerable

public interface IList : ICollection, IEnumerable

public class ArrayList : IList, ICollection, IEnumerable, ICloneable

// Code #2

public interface IEnumerable

public interface ICollection : IEnumerable

public interface IList : ICollection

public class ArrayList : IList, ICloneable

那为何MSDN要使用上面那种呢?我和小新讨论后,一致认为这样做仅仅为了提高代码的可读性。为了验证我们的想法,我分别发邮件给Eric Gunnerson(Eric是C# Compiler Team的成员)和Kit George(Kit是BCL Team的成员)询问这个问题,他们的回信如下:

Allen,

I think that readability would be the primary reason people would do this.

Eric

Allen, what you’re seeing is simply a doc thing. ArrayList actually only implements IList directly, but we decided in V1.0 of the docs, to highlight the interface hierarchy so you didn’t have to wonder ‘what does IList implement’, you get to see it there on the type.

There is no benefit to ACTUALLY doing this. Try this code, and you’ll see it compiles:

using System;
using System.IO;

public class Test : IBob2
{
    
void IBob.M() {}
}


interface IBob
{
    
void M();
}


interface IBob2 : IBob {}

Regards,

Kit

今天,我查看微软的Rotor源代码,发现ArrayList的声明的确是Code #1的做法,不过Mono(ver. 1.1.2Development Version)就采用了Code #2的做法。

所以,以后如果你再碰到到这样的情况,你可以轻松的笑一声:“这样做是为了提高代码的可读性的!”

基类与接口混合继承的声明问题 [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是怎样实现接口重新映射的原理的话,我推荐你阅读《接口映射的实现及原理》

 

接口映射的实现及原理

我们先看看该代码的类图的层次结构吧


可以看出,每一个“方法()”,实际上都是不同的。
在“有接口的继承”中,接口将“Test.接口.方法()”进行了重新映射。这个映射是如何完成的呢?

看看如下的IL代码:
在采取使用它们自己的类作为访问入口时,代码实现如下:


没有接口的继承,它实现的方式如下:

.method public hidebysig instance void 方法() cil managed
{
      // Code Size: 11 byte(s)
      .maxstack 1
      L_0000: ldstr "\u6211\u53ea\u80fd\u591f\u4f7f\u7528 
new \u6765\u5c4f\u853d\u5b83\uff0c\u4e0d\u8fc7\uff0c\u5728IDesign\u7684
\u7f16\u7a0b\u89c4\u8303\u4e2d\uff0c\u4e0d\u63a8\u8350\u7528 new\u3002"
      L_0005: call void [mscorlib]System.Console::WriteLine(string)
      L_000a: ret 
}



而有接口的继承,实现方式是这样的:

.method private hidebysig newslot virtual final instance void Test.接口.方法() cil managed
{
      .override Test.接口::方法
      // Code Size: 11 byte(s)
      .maxstack 1
      L_0000: ldstr "\u8fd9\u5c31\u53eb\u505a\u63a5\u53e3\u7684\u91cd\u6620
\u5c04\uff0c\u8fd9\u53ea\u662f\u6280\u5de7\u6027\u7684\u4e1c\u897f\u800c\u5df2\u3002"
      L_0005: call void [mscorlib]System.Console::WriteLine(string)
      L_000a: ret 
}



在这里可以看出,有接口的继承实际上对“方法”进行override,但这个override是覆盖的接口的方法的实现成员,并非是类的方法成员。

然后,我们继续看看在客户类中对两者进行调用的IL

没有接口继承的IL
      L_001d: newobj instance void Test.没有接口的继承::.ctor() //创建实例
      L_0022: stloc.2
      L_0023: ldloc.2 
      L_0024: callvirt instance void Test.没有接口的继承::方法()
有接口继承的IL
      L_003a:newobj instance void Test.有接口的继承::.ctor() //创建实例
      L_003f: stloc.s 有接口的继承1
      L_0041: ldloc.s
有接口的继承1
      L_0043: callvirtinstance void Test.基类::方法()

如果是采取接口访问时,则状况如下:

没有接口继承的IL
      L_0029: newobj instance void Test.没有接口的继承::.ctor()
      L_002e: stloc.3
      L_002f:ldloc.3  
      L_0030: callvirt instance void Test.接口::方法()

有接口继承的IL
      L_0048: newobj instance void Test.有接口的继承::.ctor()
      L_004d: stloc.s 接口3
      L_004f:ldloc.s
接口3
      L_0051: callvirtinstance void Test.接口::方法()
----------------------------------------------------------------------------
       注意一下上面标记颜色的部分,可以看出,采用不同的访问方式,实现的结果并不尽相同。也就是说,针对接口实现了的方法,与类本身自带的实现,是两回事,这种情况很类似于采用new关键字进行创建一个新的同名成员方法时遭遇的问题。
      结果之所以会不同,是由于访问的方式不同的原因,如果采用“基类”来访问,很明显,这里获得的就是基类的成员实现。也就是说,这也是多态的一种体现,但并非不可预知或不可控制的。

  看见有人说VB很难实现接口的映射,实际上并不是这样,要解释一下这个问题,这里不得不说一下,强类型的C#语法的含义。
      “基类 objBase =new 基类();” 这种语法,表示的是使用“基类”来访问新建的“基类”的实例,换而言之,“基类 objDevired = new 没有接口的继承();”表示的就是使用基类来访问新建的“没有接口的继承”的实例。
   接口的访问,也是如此,在原理上,将接口看作是一个十分特殊的抽象类,它与一般的抽象类的区别在于强制了成员的实现(接口的语法由编译器来验证的,在 CLR并未提供限制性机制),所以,在有一些设计模式中,也可以看到采用了接口-抽象类-具体类的方式来绕开这类强制成员实现的检查,从而提高灵活性。
      所以,VB.NET照样也可以完成接口映射,因为接口本身的实现机制仍旧是依赖于类的基本特征的。

      示例伪代码如下:     

Friend Interface 接口
      
' Methods
      Sub 方法()
End Interface

Friend Class 基类
      
Implements 接口

      
Public Sub New()
      
Private Sub Test.接口.方法() Implements 接口.方法
      
Public Sub 方法()
End Class

Friend Class 没有接口的继承
      
Inherits 基类

      
Public Sub New()
      
Public Sub 方法()
End Class

 
Friend Class 有接口的继承
      
Inherits 基类
      
Implements 接口

      
Public Sub New()
      
Private Sub Test.接口.方法() Implements 接口.方法
End Class



  但就接口本身来说,是一个便利性的工具,在编程时的认识上,我们不应该将它与类视作“同一类东西”,它是对类的之间关系的一个 描述。对于类本身的机制来说,继承与多态都是提供父子关系的纵向关系描述,而横向的关系描述,则反映得并不是很好,接口通过一定的实现机制,则部分性地弥补了这个空缺。
  像代码中所描述那样,接口仅是作为一个便利性的工具存在,在面向接口的编程中,提供一切都围绕接口而进行,所以,在此类的编程 模式下,可以采用接口为替换一个方法实现提供十分理想的方式,虽然这比起滥用继承并没有什么优势,但它是一个思想与观念上的转变,从本质上来说,接口映射与继承中的new一样,作为技巧用用可以,但它本身并不是一个值得推荐的方式。

 

胡谈...

看了Allen Lee,基类与接口混合继承的声明问题,发表一下个人看法。
先运行如下代码:

namespace Test
{
    
using System ;

    
interface 
接口
    {
 
        void 方法();
    }


    
class 基类:接口
    {
        public void 方法() 
        {
            Console.WriteLine("
这是基类的方法哦!");
        }


        
void 接口.方法()
        {
            Console.WriteLine("
我是接口专门的实现方法哦,你不要逃避嘛。");
        }

    }


    
class 没有接口的继承:基类
    {
        /// <summary>
        
/// 这里只有通过new才能够用新的方法来覆盖
        
/// </summary>

        public new void 
方法()
        {
            Console.WriteLine("
我只能够使用 new 来屏蔽它,不过,在IDesign的编程规范中,不推荐用 new");
        }

    }


    class  有接口的继承基类接口
    {
        void 接口.方法() 
        {
            Console.WriteLine("
这就叫做接口的重映射,这只是技巧性的东西而已。");
        }
 
    }


    
class TestClass
    {
        [STAThread]
        
static void Main(string[] args)
        {
            
基类 c1 = new 基类();
            c1.
方法();

            
接口 i1 = new 基类();
            i1.
方法();

            Console.WriteLine();

            
没有接口的继承 c2 = new 没有接口的继承();
            c2.
方法();

            
接口 i2 = new 没有接口的继承();
            i2.
方法();

            Console.WriteLine();

            
有接口的继承 c3 = new 有接口的继承();
            c3.
方法();

            
接口 i3 = new 有接口的继承();
            i3.
方法();

            Console.Read();
        }

    }

}


运行结果如下:
这是基类的方法哦
我是接口专门的实现方法哦,你不要逃避嘛。

我只能够使用 new 来屏蔽它,不过,在IDesign的编程规范中,不推荐用 new。
我是接口专门的实现方法哦,你不要逃避嘛。

这是基类的方法哦
这就叫做接口的重映射,这只是技巧性的东西而已。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值