首页 > 开发 > 综合 > 正文

Top Ten Traps in C# for C++ Programmers中文版(转)

2024-07-21 02:22:20
字体:
来源:转载
供稿:网友


【译序:c#入门文章。请注意:所有程序调试环境为microsoft visual studio.net 7.0 beta2和 microsoft .net framework sdk beta2。限于译者时间和能力,文中倘有讹误,当以英文原版为准】

在最近发表于《msdn magazine》(2001年7月刊)上的一篇文章里,我讲了“从c++转移到c#,你应该了解些什么?”。在那篇文章里,我说过c#和c++的语法很相似,转移过程中的困难并非来自语言自身,而是对受管制的.net环境的适应和对庞大的.net框架的理解。

我已经编辑了一个c++和c#语法不同点的列表(可在我的web站点上找到这个列表。在站点上,点击books可以浏览《programming c#》,也可以点击faq看看)。正如你所意料的,很多语法上的改变是小而琐细的。有一些改变对于粗心的c++程序员来说甚至是隐蔽的陷阱,本文将集中阐述十个危险的陷阱。

陷阱一.非确定性终结和c#析构器

理所当然,对于大多数c++程序员来说,c#中最大的不同是垃圾收集。这就意味你不必再担心内存泄漏以及确保删除指针对象的问题。当然,你也就失去了精确控制销毁对象时机的能力。实际上,c#中并没有显式的析构器。

如果你在处理一个未受管制的资源,当你用完时,你需要显式地释放那些资源。可通过提供一个finalize方法(称为终结器)隐式控制资源,当对象被销毁时,它将被垃圾收集器调用。

终结器只应该释放对象携带的未受管制的资源,而且也不应该引用别的对象。注意:如果你只有一些受管制的对象引用那你用不着也不应该实现finalize方法—它仅在需处理未受管制的资源时使用。因为使用终结器要付出代价,所以,你只应该在需要的方法上实现(也就是说,在使用代价昂贵的、未受管制的资源的方法上实现)。

永远不要直接调用finalize方法(除了在你自己类的finalize里调用基类的finalize方法外【译注:此处说法似乎有误,参见下面译注!】),垃圾收集器会帮你调用它。

c#的析构器在句法上酷似c++的析构器,但它们本质不同。c#析构器仅仅是声明finalize方法并链锁到其基类的一个捷径【译注:这句话的意思是,当一个对象被销毁时,从最派生层次的最底层到最顶层,析构器将依次被调用,请参见后面给出的完整例子】。因此,以下写法:

~myclass()

{

//do work here

}

和如下写法具有同样效果:

myclass.finalize()

{

// do work here

base.finalize();//

}

【译注:上面这段代码显然是错误的,首先应该写为:

class myclass

{

void finalize()

{

// do work here

base.finalize();//这样也不可以!编译器会告诉你不能直接调用基类的finalize方法,它将从析构函数中自动调用。关于原因,请参见本小节后面的例子和陷阱二的有关译注!

}

}

下面给出一个完整的例子:

using system;

class rytestparcls

{

~rytestparcls()

{

console.writeline("rytestparcls's destructor");

}

}

class rytestchldcls: rytestparcls

{

~rytestchldcls()

{

console.writeline("rytestchldcls's destructor");

}

}

public class rytestdstrcapp

{

public static void main()

{

rytestchldcls rtcc = new rytestchldcls();

rtcc = null;

gc.collect();//强制垃圾收集

gc.waitforpendingfinalizers();//挂起当前线程,直至处理终结器队列的线程清空该队列

console.writeline("gc completed!");

}

}

以上程序输出结果为:

rytestchldcls's destructor

rytestparcls's destructor

gc completed!

注意:在clr中,是通过重载system.object的虚方法finalize()来实现虚方法的,在c#中,不允许重载该方法或直接调用它,如下写法是错误的:

class rytestfinalclass

{

override protected void finalize() {}//错误!不可重载system.object方法。

}

同样,如下写法也是错误的:

class rytestfinalclass

{

public void selffinalize() //注意!这个名字是自己取的,不是finalize

{

this.finalize()//错误!不能直接调用finalize()

base.finalize()//错误!不能直接调用基类finalize()

}

}

class rytestfinalclass

{

protected void finalize() //注意!这个名字和上面不一样,同时,它也不是override的,这是可以的,这样,你就隐藏了基类的finalize。

{

this.finalize()//自己调自己,当然可以,但这是个递归调用你想要的吗?j

base.finalize()//错误!不能直接调用基类finalize()

}

}

对这个主题的完整理解请参照陷阱二。】

陷阱二.finalize和dispose

显式调用终结器是非法的,finalize方法应该由垃圾收集器调用。如果是处理有限的、未受管制的资源(比如文件句柄),你或许想尽可能快地关闭和释放它,那你应该实现idisposable接口。这个接口有一个dispose方法,由它执行清除动作。类的客户负责显式调用该dispose方法。dispose方法允许类的客户说“不要等finalize了,现在就干吧!”。

如果提供了dispose方法,你应该禁止垃圾收集器调用对象的finalize方法—既然要显式进行清除了。为了做到这一点,应该调用静态方法gc.suppressfinalize,并传入对象的this指针,你的finalize方法就能够调用dispose方法。

你可能会这么写:

public void dispose()

{

// 执行清除动作

// 告诉垃圾收集器不要调用finalize

gc.suppressfinalize(this);

}

public override void finalize()

{

dispose();

base.finalize();

}

【译注:以上这段代码是有问题的,请参照我在陷阱一中给的例子。微软站点上有一篇很不错的文章(gozer the destructor),说法和这儿基本一致,但其代码示例在microsoft visual studio.net 7.0 beta2和 microsoft .net framework sdk beta2都过不了,由于手头没有beta1比对,所以,现在还不能确定是文章的笔误,还是因为beta1和beta2的不同而导致,还是我没有准确地理解这个问题。比如下面这个例子(来自gozer the destructor)在beta2环境下无法通过:

class x

{

public x(int n)

{

this.n = n;

}

~x()

{

system.console.writeline("~x() {0}", n);

}

public void dispose()

{

finalize();//此行代码在beta2环境中出错!编译器提示,不能调用finalize,可考虑调用idisposable.dispose(如可用)

system.gc.suppressfinalize(this);

}

private int n;

};

class main

{

static void f()

{

x x1 = new x(1);

x x2 = new x(2);

x1.dispose();

}

static void main()

{

f();

system.gc.collect();

system.gc.waitforpendingfinalizers();

}

};

而该文声称会有如下输出:

~x() 1

~x() 2

why?】

对于某些对象来说,你可能宁愿让你的客户调用close方法(例如,对于文件对象来说,close比dispose更妥贴)。那你可以通过创建一个private的dispose方法和一个public的close方法,并且在close里调用dispose。

因为你并不能肯定客户将调用dispose,并且终结器是不确定的(你无法控制什么时候运行gc),c#提供了using语句以确保尽可能早地调用dispose。这个语句用于声明你正在使用什么对象,并且用花括号为这些对象创建一个作用域。当到达“}”j时,对象的dispose方法将被自动调用:

using system.drawing;

class tester

{

public static void main()

{

using (font thefont = new font("arial", 10.0f))

{

// 使用thefont

} // 编译器为thefont调用dispose

font anotherfont = new font("courier",12.0f);

using (anotherfont)

{

// 使用 anotherfont

} // 编译器为anotherfont调用dispose

}

}

在上例的第一部份,thefont对象在using语句内创建。当using语句的作用域结束,thefont对象的dispose方法被调用。例子第二部份,在using语句外创建了一个anotherfont对象,当你决定使用anotherfont对象时,可将其放在using语句内,当到达using语句的作用域尾部时,对象的dispose方法同样被调用。

using 语句还可保护你处理未曾意料的异常,不管控制是如何离开using语句的,dispose都会被调用,就好像那儿有个隐式的try-catch-finally程序块。

陷阱三.c#区分值类型和引用类型

和c++一样,c#是一个强类型语言。并且象c++一样,c#把类型划分为两类:语言提供的固有(内建)类型和程序员定义的用户自定义类型【译注:即所谓的udt】。

除了区分固有类型和用户自定义类型外,c#还区分值类型和引用类型。就象c++里的变量一样,值类型在栈上保存值(除了嵌在对象中的值类型)。引用类型变量本身位于栈上,但它们所指向的对象则位于堆上,这很象c++里的指针【译注:这其实更象c++里的引用j】。当被传递给方法时,值类型是传值(做了一个拷贝)而引用类型则按引用高效传递。

类和接口创建引用类型【译注:这个说法有点含糊,不能直接创建接口类型的对象,也并不是每一种类类型都是可以的,但可以将它们派生类的实例的引用赋给它们(说到“类类型”,不由得想起关于“型别”一词的风风雨雨j)】,但要谨记(参见陷阱五):和所有固有类型一样,结构也是值类型。

【译注:可参见陷阱五的例子】

陷阱四.警惕隐式装箱

装箱和拆箱是使值类型(如整型等)能够象引用类型一样被处理的过程。值被装箱进一个对象,随后的拆箱则是将其还原为值类型。c#里的每一种类型包括固有类型都是从object派生下来并可以被隐式转换为object。对一个值进行装箱相当于创建一个对象,并将该值拷贝入该对象。

装箱是隐式进行的,因此,当需要一个引用类型而你提供的是值类型时,该值将会被隐式装箱。装箱带来了一些执行负担,因此,要尽可能地避免装箱,特别是在一个大的集合里。

如果要把被装箱的对象转换回值类型,必须将其显式拆箱。拆箱动作分为两步:首先检查对象实例以确保它是一个将被转换的值类型的装箱对象,如果是,则将值从该实例拷贝入目标值类型变量。若想成功拆箱,被拆箱的对象必须是目标值类型的装箱对象引用。

using system;

public class unboxingtest

{

public static void main()

{

int i = 123;

//装箱

object o = i;

// 拆箱 (必须显式进行)

int j = (int) o;

console.writeline("j: {0}", j);

}

}

如果被拆箱的对象为null或是一个不同于目标类型的装箱对象引用,那将抛出一个invalidcastexception异常。【译注:此处说法有误,如果正被拆箱的对象为null,将抛出一个system.nullreferenceexception而不是system.invalidcastexcepiton】

【译注:关于这个问题,我在另一篇译文(a comparative overview of c#中文版(上篇))里有更精彩的描述j】

陷阱五.c#中结构是大不相同的

c++中的结构几乎和类差不多。在c++中,唯一的区别是结构【译注:指成员】缺省来说具有public访问(而不是private)级别并且继承缺省也是public(同样,不是private)的。有些c++程序员把结构当成只有数据成员的对象,但这并不是语言本身支持的约定,而且这种做法也是很多oo设计者所不鼓励的。

在c#中,结构是一个简单的用户自定义类型,一个非常不同于类的轻量级替代品。尽管结构支持属性、方法、字段和操作符,但结构并不支持继承或析构器之类的东西。

更重要的是,类是引用类型,而结构是值类型(参见陷阱三)。因此,结构对表现不需要引用语义的对象就非常有用。在数组中使用结构,在内存上会更有效率些,但若在集合里,就不是那么有效率了—集合需要引用类型,因此,若在集合中使用结构,它就必须被装箱(参见陷阱四),而装箱和拆箱需要额外的负担,因此,在大的集合里,类可能会更有效。

【译注:下面是一个完整的例子,它同时还演示了隐式类型转换,请观察一下程序及其运行结果j

using system;

class rytestcls

{

public rytestcls(int aint)

{

this.intfield = aint;

}

public static implicit operator rytestcls(ryteststt rts)

{

return new rytestcls(rts.intfield);

}

private int intfield;

public int intproperty

{

get

{

return this.intfield;

}

set

{

this.intfield = value;

}

}

}

struct ryteststt

{

public ryteststt(int aint)

{

this.intfield = aint;

}

public int intfield;

}

class ryclsstttestapp

{

public static void processcls(rytestcls rtc)

{

rtc.intproperty = 100;

}

public static void processstt(ryteststt rts)

{

rts.intfield = 100;

}

public static void main()

{

rytestcls rtc = new rytestcls(0);

rtc.intproperty = 200;

processcls(rtc);

console.writeline("rtc.intproperty = {0}", rtc.intproperty);

ryteststt rts = new ryteststt(0);

rts.intfield = 200;

processstt(rts);

console.writeline("rts.intfield = {0}", rts.intfield);

ryteststt rts2= new ryteststt(0);

rts2.intfield = 200;

processcls(rts2);

console.writeline("rts2.intfield = {0}", rts2.intfield);

}

}

以上程序运行结果为:

rtc.intproperty = 100

rtc.intfield = 200

rts2.intfield = 200



发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表