.NET 框架程序设计 第Ⅲ部分 类型设计

类型和成员基础

类型成员:常数,字段,实例构造器,类型构造器,方法,重载操作符,转换操作符,属性,事件,嵌套类型

访问限定修饰符:Private(C# private),Family(C# protected),Family与Assembly(C# 不支持),Assembly(C# internal),Family或Assembly(C# protected internal),Public(C# public)

类型成员(包括嵌套类型)可以使用以上六种访问限定修饰符中的任何一种(不能共用),对于非嵌套类型只能使用Public(c# public)或者Assembly(C# internal)

对类型成员来说,访问限定修饰符C#默认为Private(C# private)。对于非嵌套类型,访问限定修饰符C#默认为Assembly(C# internal)

类型预定义特性:Abstract(C# abstract),Sealed(C# sealed),Abstract与Sealed(C# static)可使用三者任意一个来修饰类型,但不能同时使用。也可以都不使用(类型预定义特性没有默认形式)

字段预定义特性:Static(C# static),InitOnly(C# readonly),可以使用两者任意一个来修饰字段,也允许同时使用。也可以都不使用(字段预定义特性没有默认形式)。常数总被认为是Static,也不能用InitOnly来修饰

方法预定义特性:
1、Static(C# static)
2、Instance(C# 默认)
3、Virtual(C# virtual),仅应用于实例方法
4、Newslot(C# new),仅应用于虚方法
5、Override(C# override),仅应用于虚方法
6、Abstract(C# abstract),仅应用于虚方法
7、Final(C# sealed),仅应用于虚方法

任何一个虚方法(C# virtual)都可以被标记为Abstract(C# abstract)或者Final(C# sealed),但两者不能同时使用。将一个虚方法标识为Final意味着不再允许任何派生类型重写该方法——这种情况并非经常发生

设计指导:尽可能将类定义成密封类,尽可能减小类中虚方法的数量

Partial Class的目的:源代码控制,代码拆分器(Web Form)

常数与字段

常数的类型只能是那些编译器认为的基元类型(由于枚举类型以基元类型形式存储,故也可以用来定义常量,这是个特例)

因为常数的值是直接嵌入到代码中的,所以常数在运行时不再需要任何的内存分配。另外,我们也不能获得常数的地址,或者以引用的方式来传递一个常数。这些约束还意味着常数没有一个良好的跨模块版本特性。也就是说只有当确信一个符号的值永远不会改变时,我们才应该使用常数来定义他们

如果要求一个模块中的数值能够在运行时(而不是编译时)被另一个模块获取,那么就不应该使用常数。相反,我们应该使用只读字段

只读字段只能在构造器内赋值(构造器在对象初次创建时被执行,且只执行一次)

C#对字段的内联初始化仅仅是一种简化的表达方式,实际上它们的初始化是在构造器内完成的

方法:构造器、操作符、转换操作符和参数

当创建一个引用类型的实例时,系统将执行三个步骤:首先为该实例分配内存,然后初始化对象的附加成员(即方法表指针和一个SyncBlockIndex),最后调用类型的实例构造器设置对象的初始状态

在少数的几种情况下,类型实例的创建不需要调用实例构造器。例如,调用Object的MemberwiseClone方法将执行以下几步:为对象分配内存,初始化对象的附加成员,将源对象的字节拷贝到新创建的对象中。另外,在反序列化一个对象时,通常也不会调用构造器

C#提供的内联方式初始化实例字段的简化语法,实际上都会被转换成构造器中的代码。所以,如果我们有一些需要初始化的实例字段和许多重载的构造器方法,我们应该考虑在定义字段的时候避免同时对它们进行初始化,相反我们应该将这些公共的初始化语句放在一个初始化构造器中,然后使其它的构造器显示的调用这个初始化构造器

一个类型的实例构造器在访问其基类的继承字段之前,必须调用其基类的实例构造器。默认情况下C#编译器会自动产生对基类型默认构造器(如果有的话)的调用代码

引用类型必须定义至少一个构造器,如果我们没有显示定义,C#编译器将为我们定义一个无参构造器(默认构造器)

如果类的修饰符为abstract,编译器生成的默认构造器的可访问性为protected,否则构造器的可访问性为public。如果基类没有提供无参构造器,那么派生类必须显示的调用基类的构造器,否则编译器会报错。如果类的修饰符为static(sealed和abstract),那么编译器就根本不会在类的定义中生成一个默认构造器。

值类型不强制要求定义构造器,也没有默认构造器。并且C#不允许为一个值类型显示定义无参构造器(CLR允许),但可以定义带参数的构造器

一个值类型的构造器只有当被显示调用时才会执行

严格的讲,只有值类型字段被嵌入到一个引用类型中时,CLR才保证值类型字段被初始化为0或null。基于堆栈的值类型字段一般不能保证得到0或null。但是,出于代码的可验证性,所有基于堆栈的值类型都必须在读取之前得到赋值

由于可验证代码要求所有的值类型字段在被读取之前首先进行初始化,所以我们为值类型定义的任何构造器都必须要初始化其所有字段

类型构造器适用于引用类型和值类型。但C#不支持为接口定义类型构造器

默认情况下,一个类型中没有定义类型构造器。如果要定义,也只能定义一个。类型构造器不能有任何参数。同时类型构造器都默认为Private,并且不能显示定义任何访问修饰符

类型构造器的调用总是由CLR负责。CLR会选择下列时间之一来调用类型构造器:
1、在类型的第一个实例被创建之前,或者在类型的非继承字段或成员第一次被访问之前——注意这个“之前”有一个精确邻接(just before)的意思。因为CLR会在确定的时刻调用类型构造器,故这被称作精确语义(precise semantics)
2、在非继承静态字段被第一次访问之前的某个时刻。因为CLR只保证静态构造器总是在静态字段被访问之前的某个时刻运行(可能运行的非常早),故这被称作字段初始化前语义(before-field-init semantics)

因为是CLR在负责调用类型构造器,所以我们应该避免编写需要以特殊顺序调用类型构造器的代码

CLR只保证类型构造器开始执行——它并不保证类型构造器完成执行

如果一个类型构造器抛出一个未处理异常,CLR将认为该类型不可用

类型构造器不应该调用其基类型的类型构造器。不需要这样做是因为基类型中的静态字段并没有被派生类型所继承

CLR希望确保在每个应用程序域内只执行一次类型构造器

操作符重载
public static Complex operator+(Complex c1,Complex c2){...}

转换操作符
//隐式转换
public static implicit operator Complex(Int32 value{...}
//显式转换
public static explicit operator Int32(Complex c){...}

对于那些转换过程中不可能丢失精度或者数量级的转型操作,我们应该为其定义一个隐式的转换操作符。而对于那些转换过程中可能丢失精度或数量级的操作,我们应该为其定义一个显式的转换操作符

在C#中不能为一种转型操作同时定义隐式转换操作符和显示转换操作符

操作符重载和转换操作符可以参考System.Decimal的实现

引用参数的两种形式:out,ref

out和ref的不同之处在于哪个方法负责初始化参数。如果一个方法的参数被标识为out,那么调用代码在调用该方法之前可以不初始化该参数,并且被调用方法不能直接读取参数的值。它必须在返回之前为该参数赋值。如果一个方法的参数被标识为ref,那么调用代码在调用该方法之前必须首先初始化该参数。被调用方法则可以任意读取该参数、或者为该参数赋值

从IL或者CLR的角度来看,out和ref关键字的行为实际上是一样的:它们都会导致指向实例的指针被传递给方法。两者的不同之处在于编译器会根据它们选择不同的机制来确保我们的代码是正确的

另外CLR允许我们根据out和ref参数来重载方法。但out和ref本身没有差异

在值类型参数上使用out和ref关键字与用传值的方式来传递引用类型的参数在某种程度上具有相同的行为。对于前一种情况,out和ref关键字允许被调用方法直接操作一个值类型实例。调用代码必须为该实例分配内存,而被调用方法操作该内存。对于后一种情况,调用代码负责为引用类型对象分配内存,而被调用方法通过传入的引用(指针)来操作对象。基于这种行为,只有当一个方法要“返回”一个它已知的对象引用时,在引用类型参数上使用out和ref关键字才有意义

按引用方式传递的变量必须和方法声明的参数类型完全相同。之所以这样做,是为了类型安全

只有方法的最后一个参数才可以用params关键字来标记。该参数必须为一个一维数组,但类型可以任意

CLR提供了两个IL指令来调用方法:call和callvirt。其中call指令根据引用变量的类型(静态类型,声明类型)来调用一个方法,而callvirt指令根据引用变量指向的对象类型(动态类型,运行时实际类型)来调用一个方法

设计指导:尽可能将方法的参数类型指定为最弱的类型,尽可能将方法的返回类型指定为最强的类型

属性

因为访问属性和访问字段有着相同的语法表达,所以只有对那些执行时间比较短的操作,我们才应该使用属性。按通常的习惯,采用这种语法的代码不宜花费太长的执行时间。对于那些执行时间较长的操作,则应该使用方法来完成

事件

event { add {} remove {} },EventArgs,EventHandler<T>

System.ComponentModel.EventHandlerList(节省内存)

posted on 2006-05-28 17:11  冰火  阅读(532)  评论(0)    收藏  举报