基类与接口混合继承的声明问题 [C#, Design] -Allen Lee's Magic

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是怎样实现接口重新映射的原理的话,我推荐你阅读《接口映射的实现及原理》

 

posted on 2004-11-19 13:12 Allen Lee 阅读(2508) 评论(10)  编辑 收藏 收藏至365Key 所属分类: C#

评论

# re: 基类与接口混合继承的声明问题 [C#, Design] 2004-11-17 08:44 Ninputer

Alan Lee,你的文笔实在太好了!而且你分析得也太好了。
我其实也在思考这个问题,看到你在分析类似问题就忍不住提了出来,呵呵。因为在孙展波的Blog里,我看到了关于实现接口的方法是不是必须是虚方法的讨论,结果是必须virtual而且必是newslot。我想必须virtual的原因是实现接口其实就是和重写虚方法一样的原理。如果基类已经实现了某接口,原理上是基类已经重写了那个接口中的虚方法。那子类再继承这个基类,就也继承了那个重写过的方法,但同时又让子类实现那个接口,它不就又得重写那个接口中虚方法吗?子类同时拥有两套重写自同一虚方法的代码怎么可以呢?可C#就是允许……(VB是禁止这样语法的)所以实现接口的方法必然有特殊的地方,我觉这就是必须有newslot这个标记的原因,它能中断基类实现接口成员的虚方法表的延续。
为什么实现接口的方法总是virtual和newslot的?现在我这个问题就弄清楚了,呵呵。VB2005正在增加这个特性。  回复   

# re: 基类与接口混合继承的声明问题 [C#, Design] 2004-11-17 08:49 Ninputer

天啊,我突然觉得重实现接口是一个难以在VB中实现的特性……因为VB在实现接口上所用的原理和C#不尽相同。  回复   

# re: 基类与接口混合继承的声明问题 [C#, Design] 2004-11-17 12:42 wayfarer

对。我也看了孙展波的Blog。其实直接实现接口方法的类中,该方法相当于是一个sealed virtual。实现接口方法,可以理解为在类中重写了接口的虚方法。这个方法可以被其派生类继承,但不允许被重写,因为该方法已经被sealed了。如果要在派生类中重写该方法,必须在其父类方法中,添加virtual关键字。否则编译不能通过。  回复   

# re: 基类与接口混合继承的声明问题 [C#, Design] 2004-11-18 14:05 寒枫天伤

与你的看法...有点不一样...不是这样的.......:(  回复   

# re: 基类与接口混合继承的声明问题 [C#, Design] 2004-11-18 14:09 Allen Lee

To 寒枫天伤:

请指教。  回复   

# re: 基类与接口混合继承的声明问题 [C#, Design] 2004-11-18 18:24 寒枫天伤

http://www.cnblogs.com/william_fire/archive/2004/11/18/65263.html的回复吧  回复   

# re: 基类与接口混合继承的声明问题 [C#, Design] 2004-11-24 19:03 Allen Lee

点评:

正当大家都热烈地拥抱面向对象的继承时,你是否曾经因为继承的某些微妙效果而感到困惑呢?本文将为你解析接口重新实现(Interface re-impletementation)所带来的微妙效果。  回复   

# re: 基类与接口混合继承的声明问题 [C#, Design] 2005-09-16 09:06 Anders

呵呵,在父类实现接口方法时在方法前加上virtual,子类override就能达到你的效果了  回复   

# re: 基类与接口混合继承的声明问题 [C#, Design] 2005-11-24 09:37 xuzicn

Allen Lee我有如下问题需要你的解答:

有一个interface ABC包括了如下的方法M():
public interface ABC
{
void M();
}

另外有个类Class Class1继承了ABC并且拥有自己的方法N():
public class Class1:ABC
{
public Class1(){}

public void M()
{
Console.WriteLine("Now we are in Class1, using method which inherit from Interface ABC ");
}

public void N()
{
Console.WriteLine(("Now we are in Class1, we used method which not inherit from Interface ABC ");
}
}

程序中有如下语句:
ABC t = new Class1();
t.M();
这种情况下编译可以通过而且会输出正确的结果

但是如果是下面的情况:
ABC t = new Class1();
t.N();
编译就会出错;
所以通过ABC t = new Class1()这句话我们得到的肯定不是Class1的一个实例
但是接口是不可以实例化的

那么ABC t = new Class1()究竟实例化了一个什么东西?

posted @ 2008-05-06 17:34  彷徨......  阅读(432)  评论(0编辑  收藏  举报