.NET 框架程序设计 第Ⅳ部分 基本类型
字符,字符串和文本
有三种方式允许我们在数值和Char实例之间进行转换,按推荐使用的优先顺序如下:
1、转型,这种方式效率最高,因为编译器会直接产生IL指令来执行转换,而不会有任何方法的调用
2、使用Convert类型的静态转换方法,这些方法的执行转换都为checked操作,即如果转换过程出现数据丢失,将会抛出OverflowException异常
3、使用IConvertible接口,这种方法执行起来的效率不如上面的两种方法好,因为在一个值类型上调用接口的方法会导致该值类型被装箱
String对象最重要的特性就是其恒定性
对于所有字符串都是文本常量字符串使用+操作符,编译器会在编译时就将它们连接为一个字符串,并只将连接后的字符串放在生成的元数据中。对于那些不是文本常量的字符串使用+操作符将使连接操作在运行时执行。要在运行时连接几个字符串,建议大家不要使用+操作符,因为它会在要执行垃圾收集的托管堆上创建多个字符串对象。相反,我们应该使用System.Text.StringBuilder类型来执行字符串连接操作
出于编程目的而比较字符串时,应该总是使用StringComparison.Ordinal或者StringComparison.OrdinalIgnoreCase。这是执行字符串比较时最快的一种方式,因为在执行比较时,不需要考虑语言文化信息。
字符串驻留:
String s1="Hello";
Object.ReferenceEquals("Hello",s1));//返回true
String s2="Hel";
String s3=s2+"lo";
Object.ReferenceEquals(s1,s3));//返回false
字符串池的目的:减少生成的元数据大小
两种最常用的编码方式:
1、UTF-16编码将16位的字符编码为2个字节
2、UTF-8编码将16位的字符编码后的可能结果,分别为1个字节,2个字节,3个字节和4个字节
枚举类型与位标记
枚举类型定义的符号是一些常数值。这意味着在编译时,编译器会将代码中引用的枚举类型符号转换为一个数值。这种情况下,代码编译后生成的元数据中将没有枚举类型的引用,定义该枚举类型的程序集在运行时也不必存在。如果我们的代码中引用了枚举类型本身,而非仅仅引用它们所定义的一些符号,那么在运行时仍然需要定义该枚举类型的程序集。另外,由于枚举类型符号是一些常数,而非只读数值,所以它们会带来一些版本问题
当定义用于标识位标记的枚举类型时,我们应该显式的为每一个符号赋予映射到单个位的数值。同时强烈建议在这样的枚举类型上应用System.FlagsAttribute定制特性
数组
数组本身是引用类型,所以即使数组元素是值类型,保存这些值的内存也是从托管堆中分配的
应该尽可能的使用一维0基数组。一维0基数组有时又被称作SZ数组(single-dimension、zero-based数组),或者向量(vector)。在各种类型的数组中,一维0基数组的性能最好,因为我们可以使用一些特殊的IL指令()来操作它们。另外,一维0基交错数组和通常的一维0基数组有着同样的性能
对于元素为引用类型的数组,CLR允许我们隐式或显式的将源数组中的元素由一种类型转换为另一种类型。要使这样的转型成功,两个数组类型都必须有同样的维数,并且源数组元素类型和目标数组元素类型之间必须存在隐式或者显式的转换。CLR不允许将元素为值类型的数组转型为任何其它类型(但是,通过Array.Copy我们可以创建一个新的数组来达到期望的效果)
当定义一个返回数组引用的方法,而在某些情况下数组又不含任何元素时,强烈建议返回一个0长数组,而不是返回null
动态创建任意上下限数组:Array.CreateInstance
接口
CLR支持单实现继承和多接口继承
接口不会继承自任何System.Oject派生类型。接口仅仅是一个包含着一组虚方法的抽象类型,其中每一个方法都有它们的名称、参数和返回值类型。接口方法不能包括任何实现,因此接口是不完整的(抽象的)。注意接口中也可以定义事件、无参属性以及含参属性(C#中又称索引器),因为它们都只不过是映射到方法上的语法缩写而已。CLR还允许接口包含静态方法、静态字段、常数、以及静态构造器。但是,与CLS兼容的接口类型不允许有任何静态成员,因为一些编程语言不能定义或者访问它们。实际上,C#编译器阻止我们在一个接口中定义任何的静态成员。另外,CLR也不允许接口中包含任何的实例字段或实例构造器
一个接口的非静态方法总被认为是公有的虚方法。这不可以改变。但是,在C#中,如果我们在一个类型内实现接口方法的时候忽略了virtual关键字,那么该方法将被认为是一个密封(sealed)的虚方法——继承了该实现类型的其它类型将不可以再重写该方法
虽然一个接口不能继承其它类型的实现,但是它可以“继承”其它接口所约定的合同。实际上,一个接口可以包含多个接口的合同。当一个类型“继承”某个接口时,它不仅要实现该接口定义的所有方法,还要实现该接口从其它接口中“继承”而来的所有方法
和引用类型相似,一个值类型可以实现0个或多个接口。但是,当我们将一个值类型实例转型为一个接口类型时,该值类型实例必须被执行装箱。这是因为接口总被认为是引用类型,并且它们定义的方法总是虚方法。未装箱形式的值类型没有指向类型方法表的指针。对值类型执行装箱将使得CLR能够查询类型的方法表,从而便可以在其上调用虚方法
该设计一个基类型,还是一个接口类型。并不总是显而易见的。但也存在一些指导原则:
1、IS-A与CAN-DO关系。一个类型仅可以继承一个实现。如果派生类型不能和基类型构成一个IS-A关系,那么就不要使用基类型,而应该采用接口。接口隐含有一个CAN-DO关系。如果认为CAN-DO功能属于多个对象类型,那么也应该采用接口
2、易用性。对于开发人员来说,通过继承一个基类型来定义新的类型要比创建一个接口更为方便。基类型可以提供很多功能,这样我们在派生类型中只需要对它的行为做一些少量的改动即可。但如果是接口,新类型就必须实现其所有的成员
3、一致的实现。不管一个接口的合同在文档中记录的有多好,要每个人都百分之百的正确实现它们是不可能的
4、版本。如果我们向一个基类型中添加了一个方法,那么其派生类型将自动继承新成员的默认实现。实际上,用户的源代码甚至不需要重新编译。而向接口中添加新的成员将会强制要求接口的用户修改源代码,并重新编译
当我们创建可扩展的应用程序时,接口应该处于中心位置。假设我们正在编写一个应用程序,并且希望其它人创建的类型能够被我们的应用程序无缝的加载和使用,下面就提供了设计这样的应用程序的一种方法:
1、创建一个程序集,然后在其中定义接口,接口的方法将用于应用程序和插件组件的通信机制。在为接口方法定义参数和返回值时,我们应该尽可能的使用定义在MSCorLib.dll中的其它接口和类型。但如果确实希望传递、或者返回我们自己定义的数据类型时,则应该把它们也定义在该程序集中。一旦建立好接口定义后,我们应该给该程序指定一个强命名,然后将其打包并部署到合作伙伴和用户那里。这样以后就把该程序集视作为一个恒定不变的程序集——这里“恒定不变”的意思是指其内容不会改变
2、创建一个单独的程序集用于包含我们的应用程序所使用的其它类型。该程序集显然要引用到前一个程序集中定义的接口和类型。我们可以按照我们的意愿任意改变该程序集中的代码。因为插件组件不会引用到该程序集,所以如果需要的话,我们甚至可以每隔一小时提供一个该程序集的新版本,这对插件开发人员不会造成任何影响
3、插件开发人员当然会在它们的程序集中定义自己的类型。它们的程序集也将会引用到我们前面定义的接口程序集中的类型。插件开发人员也可以随时提供新版的程序集,我们的应用程序则可以继续正常使用这些新版的插件组件,而不会遇到任何问题
另外,我们也要避免自己定义的类型的基类型出现在其它的程序集中,虽然“.NET框架设计指南”鼓励这么做。但是,这么做是错误的,因为这样会导致一个程序集和其它程序集的特定版本捆绑在一起。又由于CLR的并存(side-by-side)支持,这将很可能导致一个程序集的好几个版本都被载入到同一个应用程序域(AppDomain)中。这会占用相当可观的内存,从而严重的损伤系统性能。而且还有可能阻止程序集之间的通信,或者至少是使其变得困难。另外,大多数编译器都不允许我们在生成托管模块时引用一个程序集的多个版本。这种限制将使代码很难在利用一些新的特性(由新版的程序集所提供)的同时,还能保持程序集之间的通信(通过旧版的程序集中必要的类型)
实现多个有相同方法的接口:
public interface IWindow{
Object GetMenu();
}
public interface IRestaurant{
Object GetMenu();
}
public class GiuseppePizzaria:IWindow,IRestuarant{
//该方法包含了IWindow接口的GetMenu方法实现
Object IWindow.GetMenu(){...}
//该方法包含了IRestaurant接口的GetMenu方法实现
Object IRestaurant.GetMenu(){...}
//该方法和接口没有任何关系
public Object GetMenu(){...}
}
在上面的代码中,如果将IRestaurant.GetMenu这一接口实现方法删除,没有任何限定名的GetMenu方法将成为IRestaurant接口的实现方法。C#编译器在辨析接口成员实现时,会按照“先完全限定接口成员,后非限定接口成员”的顺序进行辨析
完全限定接口方法不能被声明为public,因为这些方法有着双重身份:它们有时为公有方法,有时又为私有方法。当我们在一个类型中用完全限定接口名来定义一个接口方法时,该方法将被认为是私有方法,因此我们不能通过使用类型本身的引用来调用它。但是,当我们将该类型的引用转型为一个接口时,该接口中定义的方法将可以被调用,这时它又成为一个公有方法
我们应该谨慎的使用显示接口方法实现,因为它们有时会使类型使用起来不是很方便
委托
委托既可以回调静态方法也可以回调实例方法
委托对象一旦被构造,它们就被认为是恒定不变的。当我们调用Combine将一个委托对象组合到一个委托链中时,Combine方法在内部会构造一个新的委托对象,新委托对象和源委托对象有着同样的_target和_methodPtr字段,但是其_prev字段会被设置指向原先的委托链的头部。最后Combine方法返回新委托对象的地址
将一个方法绑定到一个委托时,C#和CLR都允许引用类型的协变(covariance)和反协变(contra-variance)。协变指的是一个方法能返回从委托的返回类型派生的一个类型。反协变指的是一个方法的参数类型可以是委托的参数类型的基类。协变与反协变只能用于引用类型,不能用于值类型或void。
delete object MyCallback(FileStream s);
string SomeMethod(Stream s);//合法
int SomeOtherMethod(Stream s);//非法
C#为委托提供的语法便利:
1.不需要构造委托对象(类型推定)
C#允许我们指定回调方法的名称,不必构造一个委托对象封装器。
2.不需要定义回调方法(匿名方法)(匿名方法可能为静态方法也可能是实例方法)(匿名方法可以访问外部定义类的成员)
C#允许我们以内联的方式写出回调方法的代码。
3.不需要指定回调方法的参数(匿名方法)
如果回调代码不关心参数,C#允许省略回调方法的参数部分。
4.不需要将局部变量人工封装到类中,即可将它们传递给一个回调方法(匿名方法可以访问外部定义方法的局部变量和参数)。
异步委托:BeginInvoke,EndInvoke
动态创建和调用委托:Delegate.CreateDelegate和Delegate.DynamicInvoke
泛型
CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。CLR还允许创建泛型接口和泛型委托。此外,CLR还允许在引用类型,值类型或者接口中的创建泛型方法。
一个引用类型或值类型为了实现一个泛型接口,可以具体指定类型实参,也可以保留类型实参的未指定状态来实现泛型接口。
为了增强代码创建,可读性和可维护性,C#编译器支持在调用一个泛型方法时进行类型推定(type inference)。
在C#中,属性,索引器,事件,操作符方法,构造器和终结器(finalizer)本身不能有类型参数。然而,它们能在一个泛型类型中定义,而且这些成员中的代码能使用类型的类型参数。
泛型约束可以应用于一个泛型类型的类型参数,或者应用于一个泛型方法的类型参数。CLR不允许基于类型参数名称或者约束来进行重载;只能基于arity(也就是类型要求的类型参数的数量)来重载类型或方法。
重写一个virtual泛型方法时,重写过的版本必须指定相同数量的类型参数,而且这些类型参数会继承由基类的方法在它们上面指定的约束。事实上,重写的方法根本不准在它的类型参数上指定任何约束。然而,类型参数的名称是可以改变的。实现一个接口方法也同样。
一个类型参数可以指定0个或1个主要约束(primary constraint)。主要约束可以是一个引用类型,它标识了一个没有密封的类。指定一个引用类型约束时,相当于向编译器承诺一个指定的类型实参要么是与约束类型相同的类型,要么是从约束类型派生的一个类型。有2个特殊的主要约束:class和struct。class约束向编译器承诺一个指定的类型实参是引用类型。任何类类型,接口类型,委托类型或者数组类型都满足这个约束。struct约束向编译器承诺一个指定的类型参数是值类型。包括枚举在内的任何值类型都满足这个约束。
一个类型参数可以指定0个或多个次要约束,次要约束代表的是一个接口类型。指定一个接口类型约束时,是向编译器承诺一个指定的类型实参是实现了接口的一个类型。还有一种辅助约束称为“类型参数约束”(type parameter constraint),有时也称为“裸类型约束”(naked type constraint)。它允许一个泛型类型或者方法规定在指定的类型实参之间必须存在的一个关系。
一个类型参数可以指定0个或1个构造器约束。指定构造器约束相当于向编译器承诺一个指定的类型实参是实现了一个public无参构造器的一个非抽象类型。注意,如果同时指定了构造器约束和struct约束,C#编译器会认为是一个错误,因为这是多余的。
将一个泛型类型变量转型为另一个类型是非法的,除非将其转换为与一个约束兼容的类型。
将泛型类型变量设为null是非法的,除非将泛型类型约束成一个引用类型。C#团队认为有必要允许开发人员将一个变量设为默认值。所以C#编译器允许使用default关键字来实现这个操作。T temp=default(T);
无论泛型类型是否被约束,使用==和!=操作符将一个泛型类型变量与null进行比较都是合法的。
如果泛型类型参数不是一个引用类型,那么对同一个泛型类型的2个变量进行比较是非法的。
最后要注意的是,将操作符应用于泛型类型的操作数,存在大量问题。
定制特性
特性经常被应用于以下一些元数据定义表的条目上:TypeDef(类,结构,枚举,接口,委托),MethodDef(包括构造器),ParamDef,FieldDef,PropertyDef,EventDef,AssemblyDef,以及ModuleDef。虽然很少见,但是特性也可以应用到元数据引用表中的条目上,例如AssemblyRef,ModuleRef,TypeRef,以及MemberRef。最后,定制特性也可以应用到其它一些元数据中,例如安全许可、导出类型以及资源等
C#只允许我们将特性应用于定义了以下构造的源代码中:程序集,模块,类型,字段,方法,方法参数,方法返回值,属性,以及事件
要与通用语言规范(CLS)兼容,定制特性的类型必须直接或间接继承自System.Attribute
一个特性类型的构造器参数被称为定位参数(positional parameter),该参数是强制性的,也就是说在应用特性时必须指定这样的参数。设置字段或属性的“参数”被称为命名参数(named parameter),并且它们是可选的
当定义一个特性类型的实例构造器、字段和属性时,它们的类型被严格的限制在一个很小的子集中。具体而言,合法的数据类型集合仅限于以下类型:Boolean,Char,Byte,SByte,Int16,UInt16,Int32,UInt32,Int64,UInt64,Single,Double,String,Type,Object,或者一个枚举类型。另外,我们也可以使用任何这些类型的一维0基数组
当应用一个特性时,我们必须传递一个和特性类中定义的类型相匹配的编译时常数表达式。在我们定义Type构造器参数,Type字段,或者Type属性的地方,我们必须使用C#的typeof操作符。在我们定义Object构造器参数,Object字段,或者Object属性的地方,我们可以传递一个Int32,一个String,或者任何其它的常数表达式
.NET框架类库定义的某些特性应用的十分频繁,如果将所有这些特性信息都存放到元数据中将导致生成的托管模块的大小急剧膨胀。因此,这些特性在编译时都经过了特殊的处理,它们将以位的形式存放在元数据中。CLR和FCL也知道怎样在元数据中以一种特殊的方式来查找这些伪定制特性(pseudo-custom attribute)
可空值类型
在C#中,int?等价于Nullable<int>。并且C#允许开发人员在可空实例上执行转换和转型。C#还允许向可空实例应用操作符。C#还提供了一个“空合并操作符”(null-coalescing operator) ??。
浙公网安备 33010602011771号