C++的营养——swap手法

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/longshanks/article/details/2122399

C++的营养

莫华枫
    上一篇《C++的营养——RAII》中介绍了RAII,以及如何在C#中实现。这次介绍另一个重要的基础技术——swap手法。

swap手法

    swap手法不应当是C++独有的技术,很多语言都可以实现,并且从中得到好处。只是C++存在的一些缺陷迫使大牛们发掘,并开始重视这种有用的手法。这 个原本被用来解决C++的资源安全和异常保证问题的技术在使用中逐步体现出越来越多的应用,有助于我们编写更加简洁、优雅和高效的代码。
    接下来,我们先来和swap打个招呼。然后看看在C#里如何玩出swap。最后展示swap手法的几种应用,从中我们将看到它是如何的可爱。
    假设,我要做一个类,实现统计并保存一个字符串中字母的出现次数,以及总的字母和数字的个数。
        class CountStr
        
{
        
public:
            
explicit CountStr(std::string const& val)
                :m_str(val), m_nLetter(
0), m_nNumber(0{
                do_count(val);
            }

            CountStr(CountStr 
const& cs)
                :m_str(cs.m_str), m_counts(cs.m_counts)
                , m_nLetter(cs.m_nLetter), m_nNumber(cs.m_nNumber)
            
{}
               
void swap(CountStr& cs) {
                   std::swap(m_str, cs.m_str);
                   m_counts.swap(m_str);
                   std::swap(m_nLetter, cs.m_nLetter);
                   std::swap(m_nNumber, cs.m_nNumber);
               }

        
private:
            std::
string m_str;
            std::map
<charint> m_counts;
            
int m_nLetter;
            
int m_nNumber;
        }
    在类CountStr中,定义了swap成员函数。swap接受一个CountStr&类型的参数。在函数中,我们可以看到一组函数调用,每一个 对应一个数据成员,其任务是将相对应的数据成员的内容相互交换。此处,我使用了两种调用,一种是使用std::swap()标准函数,另一种是通过 swap成员函数执行这个交换。一般情况下,std::swap()通过一个临时变量实现对象的内容交换。但对于string、map等非平凡的对象,这 种交换会引发至少三次深拷贝,其复杂度将是O(3n)的,性能极差。因此,标准库为这些类定义了swap成员函数,通过成员函数可以实现O(1)的交换操 作。同时将std::swap()针对这些拥有swap()成员函数的标准容器特化,使其可以直接使用swap()成员函数,而避免性能损失。但是,对于 那些拥有swap()成员,但没有被特化的用户定义,或第三方的类,则不应使用std::swap(),而改用swap()成员函数。所以,一般情况下, 为了避免混淆,对于拥有swap()成员函数的类,调用swap(),否则调用标准std::swap()函数。
    顺便提一下,在未来的C++0x中,由于引入了concept机制,可以允许一个函数模板自动识别出所有“具有swap()成员”的类型,并使用相应的特化版本。这样便只需使用std::swap(),而不必考虑是什么样的类型了。
    言归正传。这里,swap()成员函数有两个要求,其一是复杂度为O(1),其二是具备无抛掷的异常保证。前者对于性能而言至关重要,否则swap操作将 会由于性能问题而无法在实际项目中使用。对于后者,是确保强异常保证(commit or rollback语义)的基石。要达到这两个要求,有几个关键要点:首先,对于类型为内置类型或小型POD(8~16字节以内)的成员数据,可以直接使用 std::swap();其次,对于非平凡的类型(拥有资源引用,复制构造和赋值操作会引发深拷贝),并且拥有符合上述要求的swap()成员函数的,直 接使用swap()成员函数;最后,其余的类型,则保有其指针,或智能指针,以确保满足上述两个要求。
    听上去有些复杂,但在实际开发中做到并不难。首先,尽量使用标准库容器,因为标准库容器都拥有满足两个条件的swap()成员。其次,在编写的每一个类中 实现满足两个条件的swap()成员。最后,对于那些不具备swap()成员函数的第三方类型,则使用指针,最好是智能指针。(也就是Sutter所谓的 PImpl手法)。只要坚持这些方针,必能收到很好的效果。
    下面,就来看一下这个swap()的第一个妙用。假设,这个类需要复制。通常可以通过operator=操作符,或者copy(或其他有明确的复制含义 的)成员函数实现,这两者实际上是等价的,只是形式不同而已。这里选择operator=,因为它比较C++:)。
    最直白的实现方式是这样:
        class CountStr
        
{
        
public:
            ...
            CountStr
& operator=(CountStr& val) {
                m_str
=val.m_str;
                m_counts
=val.m_counts;
                m_nLetter
=val.m_nLetter;
                m_nNumber
=val.m_nNumber;
            }

            ...
        }
    很简单,但是不安全,或者说没有满足异常保证。
    先解释一下异常保证。异常保证有三个级别:基本保证、强异常保证和无抛掷保证。基本保证是指异常抛出时,程序的各个部分应当处于有效状态,不能有资源泄 漏。这个级别可以轻而易举地利用RAII确保,这在前一篇已经展示过了。强异常保证则更加严格,要求异常抛出后,程序非但要满足基本保证,其各个部分的数 据应保持原状。也就是要满足“Commit or Rollback”语义,熟悉数据库的人,可以联想一下Transaction的行为。而无抛掷保证要求函数在任何情况下都不会抛出异常。无抛掷保证不是 说用一个catch(...)或throw()把异常统统吞掉。而是说在无抛掷保证的函数中的任何操作,都不会抛出异常。能满足无抛掷保证的操作还是很多 的,比如内置POD类型(int、指针等等)的复制,swap手法便以此为基础。(多说一句,用catch(...)吞掉异常来确保无抛掷并非绝对不行, 在特定情况下,还是可以偶尔一用。不过这等烂事也只能在西构函数中进行,而且也只有在迫不得已的情况下用那么一下)。
    如果这四个赋值操作 中,任意一个抛出异常,便会退出这个函数(操作符)。此时,至少有一个成员数据没有正确修改,而其他的则全部或部分地发生改变。于是,一部分成员数据是新 的,另一部分是旧的,甚至还有一些是不完全的。这在软件中往往会引发很多令人苦恼的bug。无论如何,此时应当运用强异常保证,使得数据要么是新的值,要 么没有改变。那么如何获得强异常保证?在swap()的帮助下,惊人的简单:
        class CountStr
        
{
        
public:
            ...
            CountStr
& operator=(CountStr& val) {
                swap(CountStr(val)); 
// 或者CountStr(val).swap(*this);
                raturn *this;                    
            }

            ...
        }
    我想世上没有比这等代码更加漂亮的了吧!不仅仅具有简洁动人的外表,而且充满了丰富的内涵。这就叫优雅。不过,优雅之下还需要一些解释。在这两个版本中, 都是先用复制构造创建一个临时对象,这个临时对象同传入的参数对象拥有相同的值。然后用swap()成员函数将this对象的内容与临时对象交换。于是, this对象拥有了临时对象的值,也就是与传入的实参对象具有相同的值(复制)。当退出函数的时候,临时对象销毁,自然而然地释放了this对象原先的资 源(前提是CountStr类实现了RAII)。
    那么抛出异常的情况又是怎样的呢?
    先来看看operator=里执行了哪些步骤,并考察这些步骤的异常抛掷的情况。如果将代码改写成另一个等价的形式,就很容易理解了:
            CountStr& operator=(CountStr& val) {
                CountStr t_(val);    
//此处可能抛出异常,但只有t_的值发生变化
                t_.swap(*this);       //由于swap拥有无抛掷保证,所以不会抛出异常
                return *this
            }
    在构造临时对象的时候,可能会抛出异常,因为此时执行了数据的复制和构造。请注意,这时候this对象的内容没有改变。如果此时抛出异常,数据发生改变的 只有t_,this对象并未受到影响。而随着栈清理,t_也将被析构,在RAII的作用下,t_所占用的资源也会依次释放。而下一步,swap()成员的 调用,则是无抛掷保证的,不会抛出异常,this的内容可以得到充分地、原子地交换,不会发生数据值修改一半的情况。
    在C#中,实现swap非常容易,甚至比C++更容易。因为在C#中,大部分对象都在堆上,代码中定义的所谓对象实际上是引用。对于引用的赋值操作是无抛掷的,因此在C#中可以采用同C++几乎一样的代码实现swap:
        class CountStr
        
{
            
public CountStr(string val) {
                m_str
=val;
                m_nLetter
=0;
                m_nNumber
=0;
                do_count(val);
            }

            
public CountStr(CountStr cs) {
                m_str
=new string(cs.m_str);
                m_counts
=new Dictionary<charint>(cs.m_counts);
                m_nLetter
=cs.m_nLetter;
                m_nNumber
=cs.m_nNumber
            }


              
public void swap(CountStr& cs) {
                   utility.swap(
ref m_str, ref cs.m_str);
                   utility.swap(
ref m_counts, ref cs.m_counts);
                   utility.swap(
ref m_nLetter, ref cs.m_nLetter);
                   utility.swap(
ref m_nNumber, ref cs.m_nNumber);
              }

            
public void copy(CountStr& cs) {
                
this.swap(new CountStr(cs));
            }


            
private string m_str;
            
private Dictionary<charint> m_counts;
            
private int m_nLetter;
            
private int m_nNumber;
        }
    这里utility.swap()是一个泛型函数,作用是交换两个参数:
        class utility
        
{
            
public static void swap<T>(ref T lhs, ref T rhs) {
                T t_
=lhs;
                lhs
=rhs;
                rhs
=t_;
            }

        }
    如果类有关键性的资源需要释放,那么可以实现IDisposable接口,然后在copy()中使用using:
            public void copy(CountStr& cs) {
                
using(CountStr t_=new CountStr(cs))
                    
{
                        t_.swap(
this);
                    }

            }
    如此,对象原有的数据和资源被交换到临时对象t_中之后,在退出using作用域的时候,会立即得到释放。这是RAII的一个应用,详细内容参见本系列的前一篇《C++的营养——RAII》。
    swap的基本作用是维持强异常保证语义。但是,作为一种基础性的技术,它还可以拥有更多的用途。下面介绍几种主要的应用,为了节省篇幅,案例直接使用C#,不再给出C++的代码。
    在我们的开发过程中,有时需要是一些对象复位,即回复对象的初始状态。一般情况下,我们会在类中增加一个reset()之类的成员,在这个函数中释放资源,恢复各成员数据的初值。但是,在拥有swap的情况下,这种操作变得非常容易:
        class X
        
{
            
public X() {
                ... 
//初始化对象
            }

            
public X(int v) {
                ... 
//以v初始化对象
            }

            
public void swap(X val) {...}
            
public void reset() {
                
this.swap(new X());
            }

            ...
        }
    reset()用X的默认构造函数创建了一个临时对象,将其内容与this交换,this的内容便成为了初始值。重要的是,这个成员函数也是强异常保证的。如果需要通过一些参数复位,那么同样可以做到:
        class X
        
{
            ...
            
public void reset(int v) {
                
this.swap(new X(v));
            }

            ...
        }
    有时甚至可以不需要reset这个成员,而直接在代码中使用swap复位一个对象:
        X x=new X();
        ... 
//对x的操作,改变了内容
        x.swap(new X()); //复位了
    如果X有资源需要释放,那么只需实现IDispose,然后使用using:
        class X : IDisposable
        
{
            ...
            
public void reset() {
                
using(X t=new X())
                
{
                    
this.swap(t);
                }

            }

            
public void Dispose() {...}
            ...
        }
    上面这些应用都有一个共同点,即重新初始化一个对象,使其恢复到一个初始状态。下面的应用,则反其道而行之,将一个对象切换到另一个状态。
    有时,我们会做一些类,在构造函数中执行一些复杂的操作,比如解析一个文本文件,然后向外公布解析后的结果。之后,我们需要在这个对象上load另一个文 件,那么通常都写一个load成员函数,先释放掉原先占用的资源,然后再加载新的文件。如果有了swap,那么这个load函数同样极其简单:
        class Y : IDisposable
        
{
            
public Y(string filename) {
                ... 
//打开文件,执行解析
            }

            
public void swap(Y val) {...}
            
public load(string filename) {
                
using(Y t=new Y(filename))
                
{
                    
this.swap(t);
                }

            }

            
public void Dispose() {
                ... 
//关闭文件,释放资源
            }

        }
    还有一种情况,有一些类,通过一些数据创建,创建之后在绝大多数的情况下都是只读的,但偶尔会需要改变其内部数据。为了代码的可靠性,我们可以把类写成只读的。但是如何修改其内部的数据呢?也可以通过swap:
        class Z
        
{
            
public Z(int a, float b) {
                m_a
=a;
                m_b
=b;
            }

            
public void swap(Z val) {...}
            
public int a get{return m_a;}}
            
public float b get{return m_b;}}
            
private int m_a;
            
private float m_b;
        }


        Z z
=new Z(34.5);
        z.swap(
new Z(5, 5.4)); //z的值已修改
    这样便可避免对Z的实例的随意修改。但是,这种修改方式会造成性能损失,特别是数据成员存在非O(1)复制的情况下(如有字符串、数组等),只有在修改偶尔发生的情况下才能使用。
    有些类,构造函数需要一些数据初始化对象,并且会创建的过程中会验证其有效性,和执行一些计算。也就是构造函数存在一定的数据逻辑。如果需要修改对象的某 些值,会牵涉到相应的复杂数据逻辑。通常都是把这些逻辑独立在private成员函数中,由构造函数和数据修改操作共享。这样的做法往往不能带来强异常保 证,在构造函数里的数据验证往往会抛出异常。因此,如果使用swap,便可以消除这类问题,并且使代码简化:
        class A
        
{
            
public A(int a, string b, Rectangle c) {
                ... 
//数据逻辑、计算等
            }

            
public int a {
                
setthis.swap(new A(value, m_b, m_c));}
            }

            
public string b {
                
setthis.swap(new A(m_a, value, m_c));}
            }

            
public Rectangle c {
                
setthis.swap(new A(m_a, m_b, value));}
            }

            ...
       }
    当然,也可以在类外直接进行这样的数据设置:
        A a=new A(2"zzz", Rectangle(1,110,10));
        a.swap(
new A(3, "zzz", Rectangle(1,1, 10,10)));
    这种用法可以用于某些只保存对构造函数参数的计算结果,而不需要保存这些参数的类(m_a,m_b,m_c都不需要了),只是使用上过于琐碎。
    所有这些与对象状态设置有关的swap用法,都集中表现了一个特性,即使得我们可以将对象的初始化代码集中在构造函数中,数据和资源清理的代码集中在 Dispose()中。这种做法可以大大提高代码的可维护性。如果一个软件项目中,每个类都实现swap和复制构造函数(除非该类不允许复制),并尽可能 集中数据逻辑代码,那么会使得代码质量有答复的提高。
    在上一篇《C++的营养——RAII》中,我提到守卫一个结构复杂的类:在代码中修改一个对象,然后再回复原来的状态。如果单纯手工地保存对象数据,通常 很困难(有时甚至是不可能的),而且也难以维持异常安全性(强异常保证)。但是如果使用了swap,那么将会易如反掌:
        void ScopeObject(MyCls obj) {
            
using(MyCls t_=new MyCls(obj))
            
{
                ... 
//操作obj,改变其状态或数据
                obj.swap(t_); //恢复原来的状态
            }

        }
    当然,也可以直接使用t_执行操作,这就不需要执行swap。在一般情况下两者是等价的。但是,在某些特殊情况下,比如类持有特殊资源,或者obj是并发 中的共享对象的时候,两种方法有可能不等价。swap方案使用上更全面些。总的来说相差不多,放在这里仅供参考。
    作为更进一步的发展,可以构造一个ISwapable泛型接口:
        interface ISwapable<T>
        
{
            
void swap(T v);
        }
    对于需要实现swap手法的类,实现这个接口:
        class B : ISwapable<B>
        
{
            
public B() {...}
            
public void swap(B v) {...}
            ...
        }
    这将会带来一个好处,通过泛型算法实现某些特定的操作:
        class utility
        
{
            
public static void reset<T>(T obj)
                
where T : ISwapable
                
where T : new()
            
{
                obj.swap(
new T());
            }

        }
    这样便无须为每一个类编写reset成员函数,只需这一个泛型算法即可:
        X x;
        Y y;
        utility.reset(x);
        utility.reset(y);
        ...
    swap手法可能在存在其他诸多应用,在编码的时候可以不断地发掘。只需要抓住一个原则:swap可以无抛掷,简洁地修改一个对象的值。swap所带来的 一个问题主要是性能方面。swap通常伴随着临时对象的构造,多数情况下,这种构造不会引发更多的性能损失,但在某些数据修改的应用中,会比直接的数据修 改损失更多的性能。如何取舍,需要根据具体情况分析和权衡。总的来说,swap手法所带来的好处是显而易见的,特别是强异常保证,往往是至关重要的。而诸 如简化代码等的作用,则无需多言,一用便知。
    或许swap手法非常基础,非常细小,而且很多人不用swap也过来了。但是,聚沙成塔,每一处细小的优化,积累起来则是巨大的进步。还是刘皇叔说得好:“勿以善小而不为,勿以恶小而为之”。 
展开阅读全文

C++营养——RAII

02-16

C++的营养rn莫华枫rn 动物都会摄取食物,吸收其中的营养,用于自身生长和活动。然而,并非食物中所有的物质都能为动物所吸收。那些无法消化的物质,通过消化道的另一头(某些动物消化道只有一头)排出体外。不过,一种动物无法消化的排泄物,是另一种动物(生物)的食物,后者可以从中摄取所需的营养。rn 一门编程语言,对于程序员而言,如同食物那样,包含着所需的养分。当然也包含着无法消化的东西。不同的是,随着程序员不断成长,会逐步消化过去无法消化的那些东西。rn C++可以看作一种成分复杂的食物,对于多数程序员而言,是无法完全消化的。正因为如此,很多程序员认为C++太难以消化,不应该去吃它。但是,C++的营养不可谓不丰富,就此舍弃,而不加利用,则是莫大的罪过。好在食物可以通过加工,变得易于吸收,比如说发酵。鉴于程序员们的消化能力的差异,也为了让C ++的营养能够造福他人,我就暂且扮演一回酵母菌,把C++的某些营养单独提取出来,并加以分解,让那些消化能力不太强的程序员也能享受它的美味。:)rn (为了让这些营养便于消化,我将会用C#做一些案例。选择C#的原因很简单,因为我熟悉。:))rnRAIIrn RAII,好古怪的营养啊!它的全称应该是“Resource Acquire Is Initial”。这是C++创始人Bjarne Stroustrup发明的词汇,比较令人费解。说起来,RAII的含义倒也不算复杂。用白话说就是:在类的构造函数中分配资源,在析构函数中释放资源。这样,当一个对象创建的时候,构造函数会自动地被调用;而当这个对象被释放的时候,析构函数也会被自动调用。于是乎,一个对象的生命期结束后将会不再占用资源,资源的使用是安全可靠的。rn 下面便是在C++中实现RAII的典型代码:rn class filern rn public:rn file(string const& name) rn m_fileHandle=open_file(name.cstr());rn rn ~file() rn close_file(m_fileHandle);rn rn ...rn private:rn handle m_fileHandle;rn rn 很典型的“在构造函数里获取,在析构函数里释放”。如果我写下代码: rn void fun1() ...rn file myfile("my.txt");rn ... //操作文件rn //此处销毁对象,调用析构函数,释放资源rn 当函数结束时,局部对象myfile的生命周期也结束了,析构函数便会被调用,资源会得到释放。而且,如果函数中的代码抛出异常,那么析构函数也会被调用,资源同样会得到释放。所以,在RAII下,不仅仅资源安全,也是异常安全的。rn 但是,在如下的代码中,资源不是安全的,尽管我们实现了RAII:rn void fun2() ...rn file pfile=new file("my.txt");rn ... //操作文件rn rn 因为我们在堆上创建了一个对象(通过new),但是却没有释放它。我们必须运用delete操作符显式地加以释放:rn void fun3() ...rn file pfile=new file("my.txt");rn ... //操作文件rn delete pfile;rn rn 否则,非但对象中的资源得不到释放,连对象本身的内存也得不到回收。(将来,C++的标准中将会引入GC(垃圾收集),但正如下面分析的那样,GC依然无法确保资源的安全)。rn 现在,在fun3(),资源是安全的,但却不是异常安全的。因为一旦函数中抛出异常,那么delete pfile;这句代码将没有机会被执行。C++领域的诸位大牛们告诫我们:如果想要在没有GC的情况下确保资源安全和异常安全,那么请使用智能指针:rn void fun4() ...rn shared_ptr spfile(new file("my.txt"));rn ... //操作文件rn //此处,spfile结束生命周期的时候,会释放(delete)对象rn 那么,智能指针又是怎么做到的呢?下面的代码告诉你其中的把戏(关于智能指针的更进一步的内容,请参考std::auto_ptr,boost或tr1的智能指针):rn templatern class smart_ptrrn ...rn public:rn smart_ptr(T* p):m_ptr(p) ...rn ~smart_ptr() ... delete m_ptr; rn ...rn private:rn T* m_ptr;rn rn 没错,还是RAII。也就是说,智能指针通过RAII来确保内存资源的安全,也间接地使得对象上的RAII得到实施。不过,这里的RAII并不是十分严格:对象(所占的内存也是资源)的创建(资源获取)是在构造函数之外进行的。广义上,我们也把它划归RAII范畴。但是,Matthew Wilson在《Imperfect C++》一书中,将其独立出来,称其为RRID(Resource Release Is Destruction)。RRID的实施需要在类的开发者和使用者之间建立契约,采用相同的方法获取和释放资源。比如,如果在shared_ptr构造时使用malloc(),便会出现问题,因为shared_ptr是通过delete释放对象的。rn 对于内置了GC的语言,资源管理相对简单。不过,事情并非总是这样。下面的C#代码摘自MSDN Library的C#编程指南,我略微改造了一下:rn static void CodeWithoutCleanup()rn ...rn System.IO.FileStream file = null;rn System.IO.FileInfo fileInfo = new System.IO.FileInfo("C:\file.txt");rn file = fileInfo.OpenWrite();rn file.WriteByte(0xF);rn rn 那么资源会不会泄漏呢?这取决于对象的实现。如果通过OpenWrite()获得的FileStream对象,在析构函数中执行了文件的释放操作,那么资源最终不会泄露。因为GC最终在执行GC操作的时候,会调用Finalize()函数(C#类的析构函数会隐式地转换成Finalize()函数的重载)。这是由于C#使用了引用语义(严格地讲,是对引用类型使用引用语义),一个对象实际上不是对象本身,而是对象的引用。如同C++中的那样,引用在离开作用域时,是不会释放对象的。否则,便无法将一个对象直接传递到函数之外。在这种情况下,如果没有显式地调用Close()之类的操作,资源将不会得到立刻释放。但是像文件、锁、数据库链接之类属于重要或稀缺的资源,如果等到GC执行回收,会造成资源不足。更有甚者,会造成代码执行上的问题。我曾经遇到过这样一件事:我执行了一个sql操作,获得一个结果集,然后执行下一个sql,结果无法执行。这是因为我使用的SQL Server 2000不允许在一个数据连接上同时打开两个结果集(很多数据库引擎都是这样)。第一个结果集用完后没有立刻释放,而GC操作则尚未启动,于是便造成在一个未关闭结果集的数据连接上无法执行新的sql的问题。rn 所以,只要涉及了内存以外的资源,应当尽快释放。(当然,如果内存能够尽快释放,就更好了)。对于上述CodeWithoutCleanup()函数,应当在最后调用file对象上的Close()函数,以便释放文件:rn static void CodeWithoutCleanup()rn ...rn System.IO.FileStream file = null;rn System.IO.FileInfo fileInfo = new System.IO.FileInfo("C:\file.txt");rn file = fileInfo.OpenWrite();rn file.WriteByte(0xF);rn file.Close();rn rn 现在,这个函数是严格资源安全的,但却不是严格异常安全的。如果在文件的操作中抛出异常,Close()成员将得不到调用。此时,文件也将无法及时关闭,直到GC完成。为此,需要对异常作出处理:rn static void CodeWithCleanup()rn ...rn System.IO.FileStream file = null;rn System.IO.FileInfo fileInfo = null;rn tryrn ...rn fileInfo = new System.IO.FileInfo("C:\file.txt");rn file = fileInfo.OpenWrite();rn file.WriteByte(0xF);rn rn catch(System.Exception e)rn ...rn System.Console.WriteLine(e.Message);rn rn finallyrn ...rn if (file != null)rn ...rn file.Close();rn rn rn rn try-catch-finally是处理这种情况的标准语句。但是,相比前面的C++代码fun1()和fun4()繁琐很多。这都是没有RAII的后果啊。下面,我们就来看看,如何在C#整出RAII来。rn 一个有效的RAII应当包含两个部分:构造/析构函数的资源获取/释放和确定性的析构函数调用。前者在C#中不成问题,C#有构造函数和析构函数。不过, C#的构造函数和析构函数是不能用于RAII的,原因一会儿会看到。正确的做法是让一个类实现IDisposable接口,在IDisposable:: Dispose()函数中释放资源:rn class RAIIFile : IDisposablern ...rn public RAIIFile(string fn) ...rn System.IO.FileInfo fileInfo = new System.IO.FileInfo(fn);rn file = fileInfo.OpenWrite();rn rnrn public void Dispose() ...rn file.Close();rn rnrn private System.IO.FileStream file = null;rn rn 下一步,需要确保文件在退出作用域,或发生异常时被确定性地释放。这项工作需要通过C#的using语句实现:rn static void CodeWithRAII()rn ...rn using(RAIIFile file=new RAIIFile("C:\file.txt"))rn ...rn ... //操作文件rn //文件释放rn rn 论坛

没有更多推荐了,返回首页