C#小测试(二):嵌套子类带来的困惑

这里有个很有意思的题目,先别运行程序,猜猜它会输出什么?

public class A<T>
{
    public class B : A<int>
    {
        public void M()
        {
            Console.WriteLine(typeof(T).ToString());
        }
        public class C : B { }
    }
}
 
class Program
{
    static void Main(string[] args)
    {
        A<string>.B.C c = new A<string>.B.C();
        c.M();
    }
} 

这里的T是int、string还是其它的什么?还是程序压根儿就不能通过编译?

...

...

...

答案是System.Int32,你猜对了吗?很可能这跟你的期望值并不一致。我们来看看它背后的玄妙之处。

有人认为c.M方法输出Int32,因为B的声明B : A<int>告诉B,T将一直是int。这好像是有道理的,却不是真正的原因。我们先不管C,执行这一行代码(new A<string>.B()).M(),它会输出:String。B是A<int>的子类并不意味着B中的T总是int

问题的根本在于声明class C : B会产生不明确的内容:即是class C : A<T>.B 还是class C : A<int>.B?我们得首先搞清楚这个问题。

先看继承和嵌套类的区别:

public class X
{
    public void M() { }
}
 
public class Y
{
    public void N() { }
    public class Z : X { }
}

...

(new Y.Z()).M(); // 合法,M是继承自基类的方法
(new Y.Z()).N(); // 不合法,外部类的成员不是内部类的方法

在我们的测试题中,我们调用了A<string>.B.C类的方法M,它本质上则调用了基类的M方法。如果基类是A<T>.B,那么应调用A<T>.B.M,即输出T的当前值:String;如果基类是A<int>.B,那么应调用A<int>.B.M,即总是输出:Int32。

而程序的结果告诉我们C#选择了后者。是不是有些不可思议?

也许程序中的泛型扰乱我们的直觉。那就看一个不使用泛型的例子:

public class D
{
    public class E { }
}
 
public class F
{
    public class E { }
    public class G
    {
        public E e; // 很明显这里是 F.E
    }
}
 
public class H : D
{
    public E e; // 很明显这里是 D.E
}

OK,目前为止,一切都是合法的,而且也没什么异议。当我们通过类型名称来引用类型时,类型可以来自于基类(H.e),也可以来自于外部类(G.e)。但是如果这两种情况同时出现会如何呢?

public class J
{
    public class E { }
    public class K : D
    {
        public E e; // 是J.E 还是D.E?
    }
}

这种情况是合法的吗?答案是肯定的。此时C#认为基类要优先于外部类。这也合乎常理,派生类与基类的关系是“is-a”,内部类与外部类的关系是“包含于”,前者要比后者更为紧密。

还可以这么理解:从基类继承下来的成员都属于“当前的作用域”,因此从“外部的作用域”获得的成员的优先级要低一些。

一般地,对于一个类型S,在其上下文中对一个名称进行解析的算法是:

  • S的类型参数(type parameter)
  • S可以访问的内部类
  • S的基类中可以访问的内部类(访问基类的顺序是由近及远)
  • S的外部类

现在回到开始的问题。我们看看在解析C的基类的时候发生了什么。我们调用的是B.M(),所以问题可以转化为找到正确的B。首先C没有类型参数,也没有内部类。

A<T>.B的基类是A<int>,而外部类则是A<T>,两者都有一个名称为B的内部类。选哪一个呢?根据上面的算法,基类优先,即A<int>.B。

这个过程实在是非常的绕,虽然跟着文章得出了结论,还是有些模糊。。。

参考:

An Inheritance Puzzle, Part One

An Inheritance Puzzle, Part Two

Tag标签: .NET Framework,C#
posted @ 2008-08-04 11:53 Anders Cui 阅读(1617) 评论(20)  编辑 收藏

  回复  引用    
#1楼 2008-07-20 19:39 | hhh [未注册用户]
为何要嵌套?
  回复  引用    
#2楼 2008-07-20 19:43 | gaga [未注册用户]
Mmm...
理解了这种困惑之后可以用来做人工代码混淆。
自己明白,但让别人看晕。*_*
  回复  引用  查看    
#3楼 2008-07-20 22:29 | 路西菲尔      
理解了,感谢
  回复  引用  查看    
#4楼 [楼主]2008-07-20 22:46 | Anders Cui      
@hhh
我虽然不喜欢嵌套
但是有时可能还是要用
另外你也不能阻止别人用,你得看懂他的代码 :)
  回复  引用  查看    
#5楼 [楼主]2008-07-20 22:46 | Anders Cui      
@gaga
@路西菲尔
:)
  回复  引用  查看    
#6楼 2008-07-21 09:03 | 麒麟.NET      
看得有点晕呼……
  回复  引用  查看    
#7楼 2008-07-21 09:46 | 胡伟雄      
这样的程序你也敢写,小心你老板打你屁股!
不过你的精神可加
你可能是在编代码把自己搞糊涂了,再花了很大功夫弄清为什么的。
  回复  引用  查看    
#8楼 2008-07-21 10:15 | Allie      
-,,- 有助于深层理解~~ 平时编码这样写 同事会KK死你的 呵呵
  回复  引用  查看    
#9楼 2008-07-21 10:22 | Junhui Yan      
讲得还是蛮清楚的,继承优先于外部类
  回复  引用  查看    
#10楼 2008-07-21 10:36 | Allie      
看反编译的代码 new B()的时候
IL_0001: call instance void class ConsoleApplication1.A`1<int32>::.ctor()

结果就是Int32.... 生成B实例的时候T的类型就被确认了

  回复  引用  查看    
#11楼 [楼主]2008-07-21 13:28 | Anders Cui      
@麒麟.NET
我也是,绕来绕去好不容易看懂了...

  回复  引用  查看    
#12楼 [楼主]2008-07-21 13:28 | Anders Cui      
@胡伟雄
我基本上不会这么写的
不过这样的题目会帮我们加深对C#的理解
  回复  引用  查看    
#13楼 [楼主]2008-07-21 13:35 | Anders Cui      
@Allie
@Junhui Yan
:)
  回复  引用  查看    
#14楼 [楼主]2008-07-21 13:37 | Anders Cui      
@Allie
对,这说明对A<int>.B的解析已经完成
  回复  引用    
#15楼 2008-07-21 15:26 | !A.Z [未注册用户]
public class A<T>
{
public class B : A<int>
{
public virtual void M()
{
Console.WriteLine(typeof(T).ToString());
}
public class C : B
{
public override void M()
{
base.M();
Console.WriteLine(typeof(T).ToString());
}
}
}
}

class Program
{
static void Main(string[] args)
{
var c = new A<string>.B.C();
var b = new A<string>.B();
c.M();
b.M();
}
}

  回复  引用  查看    
#16楼 2008-07-21 15:41 | Seattle      
本人无聊至极,所以把楼主的代码改了,其实就是给每一个类制定了一个公共的构造器,但是发现了奇怪的现象。我发想,C类中的T居然是string类型。
代码如下:
public class A<T>
{
public A()
{
Console.WriteLine("A: {0}",typeof(T));
}

public class B : A<int>
{
public B()
{
Console.WriteLine("B: {0}",typeof(T));
}
public void M()
{
Console.WriteLine(typeof(T).ToString());
}
public class C : B
{
public C()
{
Console.WriteLine("C: {0}",typeof(T));
}
}
}
}
  回复  引用  查看    
#17楼 2008-07-21 15:43 | Seattle      
运行结果为:
A: System.Int32
B: System.Int32
C: System.String
System.Int32
  回复  引用  查看    
#18楼 2008-07-21 15:45 | Seattle      
这个应该跟继承有关,即,构造器调用的顺序。C->B->A;
  回复  引用  查看    
#19楼 [楼主]2008-07-21 20:53 | Anders Cui      
@Seattle
我不这么觉得
构造函数的顺序确实这样的
但关键还是在于B到底该如何解析 :)
  回复  引用  查看    
#20楼 [楼主]2008-08-03 19:27 | Anders Cui      
不知为何,我修改了一下,特意没修改时间
怎么还是跑到前面了

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      


相关链接: