.NET 框架程序设计 第V部分 类型管理

异常

异常对象相较于32位的错误码值有着诸多的优势
1、异常对象可以包含更丰富的描述信息(message,stack trace等),这些信息可以帮助我们改善代码,也使我们更容易判断是什么原因导致了异常
2、在异常出现的地方,我们不必捕获或检测它们。这会极大的简化编码工作,因为我们不必再为每一个可能失败的语句或者方法调用都添加错误检测和矫正代码
3、我们不能轻易忽略一个异常。如果一个方法抛出了一个异常,该方法将不能再按正常的方式继续运行。如果应用程序没有捕获该异常,CLR将中断应用程序

C#只能抛出与CLS兼容的异常——即从System.Exception继承的异常类型。但是,CLR允许抛出任何类型的对象。C#为我们提供了一种特殊的catch块来捕获与CLS不兼容的异常:catch{...},注意这里没有指定异常筛选器。C#2.0也可以用catch(Exception e){...}来捕获所有异常,因为CLR将自动包装与CLS不兼容的异常。

在处理异常时,在catch块的末尾,我们有三种选择:
1、重新抛出所捕获的异常,向更高一层的调用堆栈中的代码通知该异常的发生
2、抛出一个不同的异常,向更高一层的调用堆栈中的代码提供更多的异常信息
3、让线程从catch块的底部退出

记住,finally块中的代码是清理代码,它们只应该撤销try块中发起的操作。我们应该避免将那些可能抛出异常的代码放在finally块中。但是,如果finally块中抛出了异常,系统的异常机制也会继续工作

很多开发人员都为异常处理(exception handling)这个术语所误导。它们总是认为异常(exception)这个词和事情的发生频率有关。另一种常见的误解是“异常”就是“错误”

异常的真正含义是对程序接口的隐含假设的一种违反。当动作成员不能完成任务时,成员应抛出异常。异常意味着动作成员没有完成它应执行的由其名称指示的动作。

在设计一个类型时,我们应该首先假设类型最常见的使用方式,然后设计其接口使之能够很好的处理这种情况。最后再考虑接口带来的隐含假设,并且当任何这些假设被违反时便抛出异常

如果我们的类型要用于许多不同的情形,将接口设计的使其适应所有的情况是不可能的。在这种情况下,我们必须尽我们最大的努力,并从用户那里获取反馈信息,从而在设计类型接口的下一个版本时将情况考虑进去

另外,我们也必须清楚系统在搜索处理异常的捕获筛选器时,应用程序的性能是会受到一些负面影响的。所以应用程序抛出的异常越少,它运行的速度也将越快

我们有时可能只是希望知道是否有异常抛出,这可以通过简单的捕获System.Exception来实现。所以使所有的异常类型都继承自Exception是有意义的。另外让DirectoryNotFoundException、EndOfStreamException、FileLoadException、FileNotFoundException都继承自IOException也是有意义的

但是,在异常层次结构中定义SystemException和ApplicationException基类型似乎没有什么太大价值。实际上它们可能只是使人感到迷惑

另外,将ExceptionEngineException和StackOverflowException这两个特殊的异常放在一个特殊的层次结构中可能是有意义的,因为它们和其它的异常不同

我们的非私有方法应该总是对传入的参数进行校验,如果有参数不能满足方法的隐含假设,我们就应该抛出一个异常。在这种情况下,推荐抛出FCL定义的下列异常:ArgumentNullException、ArgumentOutOfRangeException或DuplicateWaitObjectException

当希望抛出异常时,强烈建议抛出一个具体的异常类型即一个没有其它类型继承的异常类型。例如,不要抛出一个ArgumentException,因为它的表意不是很清楚,他可能意味着其它三种派生类型,它也没有为异常捕获者提供尽可能多的信息。我们永远都不应该抛出Exception、ApplicationException或SystemException类型

在定义一个新的异常类型时,我们应该仔细考虑应用程序代码会怎样捕获它(或者它的基类型),然后选择一个对调用者来说负面影响最小的类型作为它的基类型

在定义自己的异常类型时,我们可以自由的定义自己的子层次结构,只要它们对于我们所做的事情来说合适就行了。我们可以让它们直接继承自Exception或者其它的基类型。另外要确保我们所构造的子层次结构的位置对于调用者来说有意义。作为一个一般的原则,异常层次结构应该宽而且浅:异常类型应该继承自一个与Exception相近的类型,并且继承深度一般不应该超过2到3层。如果我们定义的异常类型不打算作为其他类型的基类型,我们应该将其标识为sealed

所有的异常类型都应该是可序列化的(SerializableAttribute,ISerializable),因为只有这样异常对象才能在跨越应用程序域(AppDomain)或机器边界时得到封送处理(marshal)、并在客户端重新抛出

在任何情况下,我们都不应该捕获所有异常的同时忽略它们,即catch(Exception){...}后不抛出异常,因为任何系统都有可能抛出一个StackOverflowException或者OutOfMemoryException

当我们抛出一个异常时,CLR会重新设置异常的起始点(throw e)。相反,当我们重新抛出一个异常对象时,CLR将不会重新设置其堆栈的起始点(throw)

自动内存管理(垃圾收集)

访问一个资源所需要的几个步骤:
1、调用中间语言(IL)中的newobj指令,为表示某个特定资源的类型实例分配一定的内存空间
2、初始化上一步所得的内存,设置资源的初始状态,从而使其可以为程序所用
3、通过访问类型成员来使用资源,这根据需要会有一些反复
4、销毁资源状态,执行清理工作
5、释放内存

对于大多数类型,如Int32、Point、Rectangle、String、ArrayList以及SerializationInfo,表示的资源并不需要任何特殊的清理操作。但对于一个封装着非托管(操作系统)资源(如文件、数据库连接、套接字、互斥体、位图、图标等等)的类型,在其对象被销毁时,就必须执行一些清理代码

在设计一个类型时,我们应该尽可能的避免使用Finalize方法,原因是:
1、终止化对象(类型本身或者其基类型重写了System.Object的Finalize方法的对象)的代龄会被提升,这会增加内存的压力,并会在垃圾收集器判定对象为可收集的垃圾时阻止回收对象的内存。另外,所有被终止化对象直接引用或者间接引用的对象的代龄也将被提升
2、终止化对象的分配花费的时间较长,因为指向它们的指针必须被放在终止化链表上
3、强制垃圾收集器执行Finalize方法会极大的损伤应用程序的性能
4、一个终止化对象可能会引用其它的对象(包括终止化对象和非终止化对象),从而不必要的延长它们的生存期。实际上我们可以考虑将一个类型拆分成两个不同的类型,一个是包含Finalize方法的轻量级类型,其中不要引用任何其它的对象。另一个是不包括Finalize方法的类型,其中可以引用任何其它对象
5、我们并不能控制Finalize方法何时执行
6、CLR不对Finalize方法的执行顺序做任何保证

如果我们决定为自己的类型实现Finalize方法,应该确保其中的代码能够尽可能快的执行,而避免那些可能阻塞Finalize方法的行为,包括任何线程同步操作。另外,如果有任何异常在Finalize方法中未经捕获而逃脱,那么CLR会忽略它,并继续调用其它对象的Finalize方法

实现一个Finalize方法最常见的原因是释放对象所占有的非托管资源。实际上,在终止化操作中,我们应该避免编写代码访问其它的托管对象或者托管静态方法。避免访问其它托管对象的原因是这些对象的类型也可能实现了Finalize方法,而它们可能首先被调用,从而将这些对象置于一个不可预期的状态。避免调用托管静态方法的原因是这些方法内部可能会访问已经执行了终止化操作的对象,从而也会把这些方法置于一种不可预期的状态

另外需要注意的是即使一个类型的实例构造器抛出了异常,它的Finalize方法也会被调用。所以我们实现的Finalize方法不应想当然的认为对象会处在一个良好的、一致的状态

有4种事件会导致一个对象的Finalize方法被调用:
1、第0代对象充满
2、显式调用System.GC的静态方法Collect

3、Windows报告内存不足
4、CLR卸载应用程序域
5、CLR被关闭

所有定义了Finalize方法的类型都应该实现Dispose模式,以给用户更多的控制权。但是,一个类型也可以实现Dispose模式,而不定义Finalize方法

Dispose模式:
public class BaseClass:IDisposable{
    ~BaseClass(){
        Dispose(false);
    }

    public void Close(){
        Dispose();
    }

    public void Dispose(){
        GC.SuppressFinalize(this);

        Dispose(true);
    }

    protected void Dispose(Boolean disposing){
        lock(this){
            if(disposing){
                //显式释放/关闭,而非执行终止化,在此可以安全的引用其它对象的字段
            }
            //对象被显式释放/关闭、或者被执行终止化,在此不应该引用其它对象的字段,因为其它对象可能已经执行终止化了
        }
    }
}

public class DeriveClass:BaseClass{
    protected override void Dispose(Boolean disposing){
        lock(this){
            try{
                if(disposing){
                    //对象被显式释放/关闭,而非执行终止化,在此可以安全的引用其它对象的字段
                }
                //对象被显式释放/关闭、或者被执行终止化,在此不应该引用其它对象的字段,因为其它对象可能已经执行终止化了
            }
            finally{
                base.Dispose(disposing);
            }
        }
    }
}

在为我们自己的类型实现Dispose模式时,如果对象占用的非托管资源被执行了清理,那么对于那些只有在对象占用的非托管资源有效的情况下才能成功执行的方法都应该抛出一个System.ObjectDisposedException异常。但是Dispose和Close方法在多次调用的情况下不应该抛出System.ObjectDisposedException异常,遇到这种情况它们应该不执行任何操作而直接返回

一般情况下,建议不要调用Dispose或者Close方法。因为CLR的垃圾收集器已经实现的很好了,我们完全可以将工作交给它来做。但是,在需要显式清理对象占用的非托管资源(如试图删除一个打开的文件)、或者确信操作是安全的情况下,我们仍建议调用Dispose或者Close方法,因为这可以阻止由于Finalize方法运行所导致的对象代龄的提升,从而提高应用程序的性能

如前面说的我们只应该在需要资源清理的地方调用Dispose或者Close方法。因此,我们也应该谨慎的使用C#的using语句

弱引用(System.WeakRefrence)的目的是节省内存

弱引用允许垃圾收集器收集对象,同时也允许引用程序访问该对象,结果是哪一个要取决于时间

我们可以将不追踪对象复苏的弱引用称为一个短弱引用(short weak reference),而将追踪对象复苏的弱引用称为长弱引用(long weak reference)。如果一个对象的类型没有重写Finalize方法,那么短弱引用和长弱引用的行为是一样的。强烈建议不要使用长弱引用,因为长弱引用在一个对象执行终止化后仍允许我们使该对象重新复苏,而这会导致对象状态的不可预期

一旦我们创建了一个对象的弱引用,我们通常要将该对象的强引用设置为null。如果有任何该对象的强引用存在,垃圾收集器都不会对该对象执行垃圾收集。为了再次使用对象,我们必须将弱引用转换为一个强引用,这可以通过查询WeakReference对象的Target属性,并将结果赋值给应用程序的一个根来完成

CLR寄宿和应用程序域

一个应用程序域是一组程序集的一个逻辑容器。CLR初始化时创建的第一个应用程序域称作默认应用程序域(default AppDomain),该应用程序域只有在Windows进程中断时才会被销毁

应用程序域的三个特点:
1、应用程序域之间是相互隔离的
2、应用程序域可以被卸载
3、应用程序域可以单独实施安全策略和配制策略

默认情况下,一个程序集会被加载到独立域(single-domain)中。但有时我们也有一些程序集是期望被几个应用程序域所公用的。这样的程序集可以被加载到中立域(domain-netural)。如MSCorLib.dll程序集

强命名程序集是以中立域的方式来加载的,因为这样可以节省操作系统的资源

程序集的加载与反射

当需要显式加载程序集时,应该尽量使用System.Reflection.Assembly的Load方法,而避免使用LoadFrom。其中一个原因就是LoadFrom方法的效率要比Load方法低许多,LoadFrom方法内部会调用到Load方法。另一个原因是LoadFrom会将程序集看作为一种“数据文件”来加载,如果我们的应用程序域从不同的路径上加载了两个相同的程序集文件,那么将有许多内存被浪费,系统运行时的性能也会降低。而调用Load可以确保得到最好的性能,并且相同的程序集不会被多次加载到同一应用程序域中

获取Type对象的几种方法:
1、System.Object的实例方法GetType
2、System.Type提供的静态方法GetType
3、System.Type提供的实例方法GetNestedType和GetNestedTypes
4、System.Reflection.Assembly提供的实例方法GetType、GetTypes和GetExportedTypes
5、System.Reflection.Module提供的实例方法GetType、GetTypes和FindTypes
6、编程语言提供的操作符,如C#的typeof

我们应该尽可能的使用操作符来获取一个Type对象,因为操作符一般会产生更快的代码

创建类型实例的几种方法:
1、System.Activator提供的静态方法CreateInstance和CreateInstanceFrom方法
2、System.AppDomain提供的实例方法CreateInstance、CreateInstanceAndUnwrap、CreateInstanceFrom、CreateInstanceFromAndUnwrap
3、System.Type提供的实例方法InvokeMember
4、System.Reflection.ConstructorInfo提供的实例方法Invoke

CLR对值类型是否定义构造器并没有做强制的要求。这就出现了一个问题,因为前面列出的所有机制都要求通过调用构造器来构造一个类型的实例。为了解决这个问题,微软“加强”了Activator的CreateInstance方法中某些重载版本的功能,允许它们不用调用构造器就可以创建一个值类型的实例。如果希望不用调用构造器就可以创建一个值类型的实例,我们只能从以下两个版本的方法中选择其一:即参数为Type的那个版本的CreateInstance方法、或者参数为Type和Boolean的那个版本的CreateInstance方法

另外,前面列出的几种创建类型实例的方法不能用来创建数组(继承自System.Array的类型)和委托(继承自System.MulticastDelegate的类型)

要创建一个数组实例,我们应该调用Array的静态方法CreateInstance
要创建一个委托实例,我们应该调用Delegate的静态方法CreateDelegate

执行异步操作

使用线程池执行受计算限制的异步操作:System.Threading.ThreadPool.QueueUserWorkItem(WaitCallback callBack)

显示创建线程执行特定的受计算限制的异步操作:new System.Threading.Thread(ParameterizedThreadStart start)。通常不建议这么做。

定期执行受计算限制的异步操作:System.Threading.Timer。

APM(Asynchronous Programming Model)的三种使用方式:

1. 等待直至完成

FileStream fs = new FileStream(...);

byte[] data = new byte[100];

IAsyncResult ar = fs.BeginRead(data, 0, data.Length, null, null);

//在这里执行一些代码......

int bytesRead = fs.EndRead(ar);

fs.Close();

Console.WriteLine("Number of bytes read={0}", bytesRead);

Console.WriteLine(BitConverter.ToString(data, 0, bytesRead));

2. 轮询

FileStream fs = new FileStream(...);

byte[] data = new byte[100];

IAsyncResult ar = fs.BeginRead(data, 0, data.Length, null, null);

while (!ar.IsCompleted) {

    Thread.Sleep(10);

}

//while (!ar.AsyncWaitHandle.WaitOne(10, false)){}

int bytesRead = fs.EndRead(ar);

fs.Close();

Console.WriteLine("Number of bytes read={0}", bytesRead);

Console.WriteLine(BitConverter.ToString(data, 0, bytesRead));

3. 方法回调

FileStream fs = new FileStream(...);

byte[] data = new byte[100];

fs.BeginRead(data, 0, data.Length,

    delegate(IAsyncResult ar)

    {

        int bytesRead=fs.EndRead(ar);

        fs.Close();

        Console.WriteLine("Number of bytes read={0}", bytesRead);

        Console.WriteLine(BitConverter.ToString(data, 0, bytesRead));

    }, null);

//在这里执行一些代码......

线程同步

internal sealed class TransactionWithLockObject {
    private object _lock = new object();

    private DateTime _timeOfLastTransaction;

    public void PerformTransaction() {
        lock (m_lock) {
            _timeOfLastTransaction = DateTime.Now;
        }
    }

    public DateTime LastTransaction {
        get {
            lock (_lock) {
                return _timeOfLastTransaction;
            }
        }
    }
}

posted on 2006-06-14 21:59  冰火  阅读(519)  评论(0)    收藏  举报