欢迎来到.net学习网

欢迎联系站长一起更新本网站!QQ:879621940

您当前所在位置:首页 »  .NET本质论第一卷:公共语言运行库教程 » 正文

本教程章节列表

类型和基类型

创建时间:2012年08月30日 15:19  阅读次数:(10843)
分享到:

除了用多重接口声明兼容性(本节出现了多次兼容(compatibility)的概念,而较少使用继承(inherit),但意思却是一样的。在.NET中,类型(type)的概念更为宽泛,这与以前的编程语言的类型不同,它将接口、类等都统一到类型中(这与Java、smalltalk等面向对象的编程语言也不相同)。这可能也是作者大量使用兼容的原因),一个类型还可以指定最多一个基类型。基类型不能是接口,而且严格地说,它所支持的接口集也不能被认为是声明类型的基类型。此外,接口本身没有基类型。准确地说,一个接口最多有一组所支持接口,这和具体类型一样。

没有指定基类型的非接口类型将使用System.Object作为它们的基类型。有时基类型将从CLR触发不同的运行时语义(例如,引用类型与值类型、按引用封送、委托)。基类型也能用于将通用成员打包为单个类型,这样能够为多个类型所支持。当定义一个类型时,你可以控制该类型是否作为基类型使用。假如将类型声明为sealed,将会禁止将它作为基类型使用。另一方面,假如声明为abstract,那么不允许直接实例化该类型,它的用处仅限作为基类型。接口类型总是隐式的abstract。如果一个类型既不是abstract,也不是sealed,那么,程序员可以把它当作基类型使用,也可以实例化为新的对象。不是abstract的类型经常作为具体concrete)类型被引用。

就跨程序集可访问性而言,基类型的non-private成员成为派生类型的合同的一部分。派生类型的方法能够访问基类型的non-private成员,就如同它们是在派生类型中被显式声明。派生类型中的成员名可能会与基类型中的non-private成员名发生冲突(不论是偶然的还是精心设计的)。如果发生这种情况,那么在派生类型中,这两种成员都会存在。如果这个成员是static,你可以用类型名区分。如果成员是non-static,你可以使用语言相关的关键字(如this或者base)进行限定,要么选择派生类型成员,要么选择基类型成员。例如,考虑下面用C#定义的类型:
public abstract class Mammal {
public double age;
public static int count;
}
public sealed class Human : Mammal {
public double age;
public static int count;
public void Work(int count) {
++age; // Human.age
++this.age; // Human.age
++base.age; // Mammal.age
++count; // count参数
++Human.count; // Human.count
++Mammal.count; // Mammal.count
}
}

在这个示例中,基类型和派生类型都有age字段和count字段。如果要选择派生类型的age字段,就可以使用this关键字。如果选择基类型的age字段,就可以用base关键字。对于静态的情况,可以使用显式的类型名。当从这个类型的外部看它时,事情就变得有趣得多。考虑下面的用法:
Human h = new Human();
Mammal m = h; // legal, Human is compatible with Mammal
h.age = 100; // 访问Human.age
m.age = 200; // 访问Mammal.age

在这个示例中,h和m两者引用同一个对象。然而,由于每个变量的类型是不同的,两个变量将会看到不同的公共合同(public contract)。对于h来说,Human的age定义隐藏了基类型中的定义,因此,Human的age字段会受到影响。相反如果使用m变量,就不用考虑任何派生类型的公共合同。它所知道的只是Mammal(哺乳动物),并且,只能访问Mammal的age字段。
注意,对于前面的例子,C#编译器会发出一个警告,标明派生类型字段隐藏了基类型字段的可见性。你可以在派生类型的字段定义中添加new关键字,用于消除警告,如下所示:
new public double age;
new public static int count;

注意,new关键字的存在与否,决不会影响元数据或执行代码。有趣的是,VB.NET使用了更为形象的关键字Shadows用于同样的目的,而不是像C#那样重载new关键字的意义(我们知道new作为运算符,是用于在堆上创建对象和调用构造函数。而在C#中,new还可以用作修饰符,用于隐藏基类成员的继承成员)。

前面有关名称冲突的讨论说明了在派生类型中重用字段名时所发生的事情。当方法名被重用时,用于处理名称冲突的策略有些差别,原因是方法名可能由重载而被重用。
当基类型和派生类型存在同名的方法时,CLR支持两种基本的策略:按名字隐藏(hide -by-name)和按签名隐藏(hide-by-signature)。通过在派生类型的方法上添加或者不添加hidebysig元数据特性,从而指明方法的声明将采取哪种策略。当使用按签名隐藏hide-by-signature)声明方法时,只有名字相同和签名相同的基类型的方法将被隐藏。对于基类型中的其他同名方法(其他同名方法是指方法名字相同,但签名不同的方法,或者说方法名相同,但方法的形参不同),则在派生类型的合同中是可见的。相比之下,当使用按名字隐藏hide-by-name)声明方法时,派生类型的方法隐藏了基类型的所有同名方法,而不在乎它们的签名。用C++定义的类型默认情况是按名字隐藏的,因为这是C++语言最初定义的方式。用C#定义的类型却不同,它们总是使用按签名隐藏。用VB.NET定义的类型可以采用这两种策略中的任何一个,这取决于该方法是使用了Overloads(按签名隐藏)关键字,还是(名字相同,但签名不同的方法)s(按名字隐藏)关键字。

图3.4:成员重载(Overloading)和隐藏(Shadowing)

图3.4是一个C#编写的示例,其中的两个类型既重载了字段名,也重载方法名。注意,由于C#使用按签名隐藏,因而接受一个int参数的f方法不会隐藏其基类型的接受一个object参数的f方法。当用一个字符串参数调用f方法时,就证实了这一点。如果派生类型采用按名字隐藏,那么,基类型的f方法将是不可见的,这意味着派生类型的合同中将不存在能够接受字符串参数的f方法。不过,由于派生类型是用C#定义的,其方法将被标记为hidebysig,于是,基类型中的其他方法(每一个类型都有一个派生(或者继承)层次图,即它可能有一个派生类型,而它的派生类型也可能存在派生类型,等等。深度派生的类型,是指某个类型派生图中处于叶子位置的类型)将加到派生类型的公共合同中。
对于重载,特别要注意被调用的确切方法是在编译时确定的。运行时不会执行相关测试以确定采用哪个重载版本。不过,CLR的确支持在运行时动态绑定方法代码,第六章将讨论这个主题。

有关基类型的最后一个主题是构造函数方面的问题。当CLR分配一个新的对象,它将调用深度派生(most-derived)的类型(每一个类型都有一个派生(或者继承)层次图,即它可能有一个派生类型,而它的派生类型也可能存在派生类型,等等。深度派生的类型,是指某个类型派生图中处于叶子位置的类型)的构造函数方法。而显式地调用基类型的构造函数方法是派生类型构造函数的工作。这意味着无论什么时候,对象的实际类型是深度派生的类型,甚至当基类型的构造函数正在执行的时候。

刚才描述的行为与Java的工作方式相似,但与C++的工作方式却大相径庭。在C++中,对象是“从里向外延伸式”地构造——那就是说,从基类型到派生类型。另外,类型的构造函数从属于一个C++对象,基类型构造函数是基类型的而非派生类型的。这意味着在基类型构造函数执行期间,可能被调用的任何虚拟方法都不会分发到派生类型的实现中。对于一个CLR基类型,情况却不是这样的。如果基类型的构造函数导致一个虚拟方法被调用,那么,深度派生类型的方法将会被分发,而派生类型构造函数可能还没有完成执行。
为了避免这个问题,强烈推荐不要在一个non-sealed类型的构造函数中调用虚拟方法(前面谈到对象的实际类型是深度派生的类型。基类型的构造函数是通过这个深度派生类型的构造函数显式地调用。因此,如果一个non-sealed类型的构造函数中调用了虚拟方法,那么,由于派生类型的构造函数的执行是在基类型的构造函数之后,也就是说分发该虚拟方法的部分还没有执行到,因而将导致不可预见的问题(关于虚拟方法的分发参见第六章))。其中包括避免一些好像没有问题的事情,例如,传递this或Me引用到WriteLine方法中。

关于派生类型的构造函数和基类型的构造函数是如何协同工作的,C#语言有着自己的解决之道,如图3.5所示。在面对带初始化表达式的实例字段的声明时,编译器产生的.ctor会首先以声明的顺序调用所有的字段初始化器。一旦派生类型的字段初始化器被调用,派生类型构造函数将使用程序员提供的参数调用基类型构造函数(如果使用了base构件)。如果基类型构造函数完成执行,派生类型构造函数会继续执行构造函数的主体(例如,花括号中构造函数的部分)。这意味着当基类型的构造函数执行时,派生类型的构造函数的主体还没有开始执行。

图3.5:派生和构造

一般来说,设计一个用作基类型的类型,比定义一个只用于实例对象的类型难度明显要大一些。出于这个原因,标记所有的类型为sealed是个良好的措施,除非你确定你的类型作为基类型是安全的。同理,如果你能够把握一个类型的所有用它作基类型的类型(也就是将这个类型视为基类型,换言之,就是这个类型的派生类型),就比较容易确定该类型作为基类型是安全的。你可以通过将类型所有的构造函数标记为internal,从而限制这个类型作为基类型使用的范围。这个技巧使得该类型的所有构造函数对于程序集以外的类型是不可访问的,因此禁止了这些外部类型将该类型用作基类型。然而,同一个程序集内的类型可以安全地无限制地将该类型用作基类型。

我们走到那儿了?


类型是CLR程序的基本生成块,并且类型是模块的元数据的最大组成部分。每种编程语言以语言相关的方式将它的局部构件(局部构件(local construct)是指对应特定编程语言的构件,在这里就是指类型。例如,C# 支持两种类型:值类型和引用类型。值类型包括简单类型(如 char、int 和 float)、枚举类型和结构类型。引用类型包括类 (Class) 类型、接口类型、委托类型和数组类型。这些与CLR类型基本上能够进行一一映射)映射到CLR类型。CLR类型主要由字段和方法组成。然而,开发人员可以通过属性和事件的使用,收集到元数据中方法的预期用法。为了支持面向对象编程语言,开发人员可以使用接口和基类型,将CLR类型划分为层次结构。开发人员的大部分工作是以现有类型为基础定义新的类型。b
来源:.net学习网
说明:所有来源为 .net学习网的文章均为原创,如有转载,请在转载处标注本页地址,谢谢!
【编辑:Wyf】

打赏

取消

感谢您的支持,我会做的更好!

扫码支持
扫码打赏,您说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

最新评论

共有评论1条
  • #1楼  评论人:匿名  评论时间:2019-4-7 21:39:59
  • 非常棒
发表评论:
留言人:
内  容:
请输入问题 20+7=? 的结果(结果是:27)
结  果: