当前位置:编程学习 > C#/ASP.NET >>

C++程序员容易犯的十个C#错误

答案:我们知道, C#的语法与C++非常相似,实现从C++向C#的转变,其困难不在于语言本身,而在于熟悉.NET的可管理环境和对.NET框架的理解。

尽管C#与C++在语法上的变化是很小的,几乎不会对我们有什么影响,但有些变化却足以使一些粗心的C++编程人员时刻铭记在心。在本篇文章中我们将讨论C++编程人员最容易犯的十个错误。

错误1: 析构函数上的差异
几乎可以完全肯定地说,对于大多数C++编程人员而言,C#与C++最大的不同之处就在于垃圾收集。这也意味着编程人员再也无需担心内存泄露和确保删除所有没有用的指针。但我们再也无法精确地控制杀死无用的对象这个过程和时机。事实上,在C#中没有明确的Destructor。

如果使用非托管资源,在不使用这些资源后,必须明确地释放它。对资源的隐性控制是由Finalize方法(在C#语言中,析构函数将被编译器转换为Finalize方法)提供的,当对象被销毁时,它就会被垃圾收集程序调用收回对象所占用的非托管资源。

Finalize方法应该只释放被销毁对象占用的非托管资源,而不应牵涉到其他托管对象。如果在程序中只使用了托管资源,那就无需也不应当为类编写Finalize方法,只有在非托管资源的处理中才会用到Finalize方法。直接调用一个对象的Finalize方法是绝对不允许的(除非是在派生类的Finalize中调用基类的Finalize。),垃圾收集程序会自动地调用Finalize。

从语法上看,C#中的Destructor与C++非常相似,但其实它们是完全不同的。C#中的Destructor只是定义Finalize方法的捷径。因此,下面的二段代码是有区别的:

~MyClass()
{
// 需要完成的任务
}

public override void Finalize()
{
// 需要完成的任务

base.Finalize(); // 这应该是本方法中的最后一步
}

第二段代码中要显示调用基类的Finalize方法是因为,编译器不会像析构函数那样帮我们自动调用基类的析构函数。

错误2:Finalize?还是Dispose?
从上面的论述中我们已经很清楚,显性地调用Finalize是不允许的,它只能被垃圾收集程序调用。如果希望尽快地释放一些不再使用的数量有限的非托管资源(如文件句柄,数据库连接,网络连接等),则应该使用IDisposable接口,这一接口有个Dispose方法,它能够帮你完成这个任务。Dispose是无需等待Finalize被调用而能够释放非托管资源的唯一方法。

如果已经使用了Dispose方法,则应当阻止垃圾收集程序再对相应的对象执行Finalize方法。为此,需要调用静态方法GC.SuppressFinalize,并将相应对象的指针传递给它作为参数,垃圾收集程序就不会在回收内存前对该对象调用Finalize方法。据此,我们能够得到如下的代码:

public void Dispose()
{
// 完成清理操作

// 通知GC不要再调用Finalize方法
System.GC.SuppressFinalize(this);
}

public override void Finalize()
{
Dispose();
base.Finalize();
}

对于有些对象,可能调用Close方法就更合适(例如,对于文件对象调用Close就比Dispose更合适),可以通过创建一个private属性的Dispose方法和public属性的Close方法,并让Close调用Dispose来实现对某些对象调用Close方法。

由于不能确定类的客户代码一定会调用Dispose,而且finalizer的执行时机也是不确定的(我们无法控制GC会在何时运行),C#提供了一个Using语句来保证Dispose方法会在尽可能早的时间被类的客户代码调用。一般的方法是定义使用哪个对象,然后用括号为这些对象指定一个活动的范围,当遇到最内层的括号时,Dispose方法就会被自动调用,对该对象进行处理。

using System.Drawing;

class Tester
{
public static void Main()
{
using (Font theFont = new Font("Arial", 10.0f))
{
//使用theFont对象
}// 编译器将调用Dispose处理theFont对象

Font anotherFont = new Font("Courier",12.0f);

using (anotherFont)
{
// 使用anotherFont对象
}// 编译器将调用Dispose处理anotherFont对象
}
}

在本例的第一部分中,Font对象是在Using语句中创建的。当Using语句结束时,系统就会调用Dispose,对Font对象进行处理。在本例的第二部分,Font对象是在Using语句外部创建的,在决定使用它时,再将它放在Using语句内,当Using语句结束时,系统就会调用Dispose。

Using语句还能防止其他意外的发生,保证系统一定会调用Dispose。

错误3:C#中的值型变量和引用型变量是有区别的
与C++一样,C#也是一种强类型编程语言。C#中的数据类型被分为了二大类:C#语言本身所固有的数据类型和用户自定义数据类型,这一点也与C++相似。

此外,C#语言还把变量分为值类型和引用类型。除非是被包含在一个引用类型对象中,值类型变量的值保留在栈中,这一点与C++中的变量非常相似。引用类型的变量也是保存在栈中,它的值是堆中对应对象的地址,与C++中的指针非常地相似。值类型变量的值被直接传递给方法,引用型变量在被作为参数传递给方法时,传递的是索引。

类和接口可以创建引用类变量,但需要指出的是,结构数据类型是C#的一种内置数据类型,同时也是一种值型的数据类型。

错误4:注意隐性的数据类型转换
Boxing和unboxing是使值型数据类型被当作索引型数据类型使用的二个过程。值型变量可以被包装进一个堆对象中,然后再被解包回值型变量。包括内置数据类型在内的所有C#中的数据类型都可以被隐性地转化为一个对象。包装一个值型变量就会生成一个对象的实例,然后将变量拷贝到实例中。

Boxing是隐性的,如果在需要索引型数据类型的地方使用了值型数据类型的变量,值型变量就会由编译器隐性地转化为索引型数据类型的变量。Boxing会影响代码执行的性能,因此应当尽量避免,尤其是在数据量较大的时候。

如果要将一个打包的对象转换回原来的值型变量,必须显性地对它进行解包。解包需要二个步骤:首先对对象实例进行检查,确保它们是由值型的变量被包装成的;第二步将实例中的值拷贝到值型变量中。为了确保解包成功,被解包的对象必须是通过打包一个值型变量的值生成的对象。

using System;

public class UnboxingTest
{
public static void Main()
{
int i = 123;

//打包
object o = i;

// 解包(必须是显性的)
int j = (int) o;
Console.WriteLine("j: {0}", j);
}
}

如果被解包的对象是无效的,或是一个不同数据类型的对象,就会产生InvalidCastException异常。

错误5:结构与类是有区别的
C++中的结构与类差不多,唯一的区别是,在缺省状态下,结构的访问权限是public,其继承权限也是public。一些C++编程人员将结构作为数据对象,但这只是一个约定而非是必须这样的。

在C#中,结构只是一个用户自定义的数据类型,并不能取代类。尽管结构也支持属性、方法、域和操作符,但不支持继承和Destructor。

更重要的是,类是一种索引型数据类型,结构是值型数据类型。因此,结构在表达无需索引操作的对象方面更有用。结构在数组操作方面的效率更高,而在集合的操作方面则效率较低。集合需要索引,结构必须打包才适合在集合的操作中使用,类在较大规模的集合操作中的效率更高。

错误6:虚方法必须被明确地覆盖
在C#语言中,编程人员在覆盖一个虚方法时必须显性地使用override关健字。假设一个Window类是由A公司编写的,ListBox和RadioButton类是由B公司的和编程人员在购买的A公司编写的Window类的基础上编写的,B公司的编程人员对包括Window类未来的变化情况在内的设计知之甚少。

如果B公司的一位编程人员要在ListBox上添加一个Sort方法:

public class ListBox : Window
{
public virtual void Sort() {}
}

在A公司发布新版的Window类之前,这不会有任何问题。如果A公司的编程人员也在Window类中添加了一个Sort方法。

public class Window
{
public virtual void Sort() {}
}

在C++中,Windows类中的Sort方法将成为ListBox类中Sort方法的基础方法,在希望调用Windows类中的Sort方法时,ListBox类中的Sort方法就会被调用。在C#中,虚拟函数总是被认为是虚拟调度的根。也就是说,一旦C#发现一个虚拟的方法,就不会再在虚拟链中查找其他虚拟方法。如果ListBox再次被编译,编译器就会生成一个警告信息:

"\class1.cs(54,24): warning CS0114: 'ListBox.Sort()' hides
inherited member 'Window.Sort()'.

要使当前的成员覆盖原来的方法,就需要添加new关健字。

要消除警告信息,编程人员必须搞清楚他想干什么。可以在ListBox类中的Sort方法前添加new,表明它确实需要覆盖Window中的虚方法:

public class ListBox : Window
{
public new virtual void Sort(){}
}

这样就可以清除警告信息。如果编程人员不希望覆盖掉Window中的方法,而是需要重写该方法,就必须使用override关健字来显性地表明其意图:

public class ListBox : Window
{
public override void Sort(){}
}

错误7:类成员变量的初始化
C#中的初始化与C++中不同。假设有一个带有private性质的成员变量age的Person类,Employee是由继承Person类而生成的,它有一个private性质的salaryLevel成员变量。在C++中,我们可以在Employee的构造器的初始化部分初始化salaryLevel,如下面的代码所示:

Employ

上一个:从Internet上抓取指定URL的源码的方案(C#)
下一个:C#中使用Oracle 存储过程笔记

CopyRight © 2012 站长网 编程知识问答 www.zzzyk.com All Rights Reserved
部份技术文章来自网络,