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

原创 2002年01月06日 14:48:00

Top Ten Traps in C# for C++ Programmers中文版<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

作者:Jesse Liberty

译者:荣耀

【译序:C#入门文章。请注意:所有程序调试环境为Microsoft Visual Studio.NET 7.0 Beta2和 Microsoft .NET Framework SDK Beta2。限于译者时间和能力,文中倘有讹误,当以英文原版为准】

在最近发表于《MSDN Magazine》(2001年7月刊)上的一篇文章里,我讲了“从C++转移到C#,你应该了解些什么?”。【译注:这篇文章的中文版可查阅《程序员》杂志.NET专刊。在那篇文章里,我说过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比对,所以,现在还不能确定是文章的笔误,还是因为Beta1Beta2的不同而导致。比如下面这个例子(来自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】。当被传递给方法时,值类型是传值(做了一个拷贝)而引用类型则按引用高效传递。

     类和接口创建引用类型,但要谨记(参见陷阱五):和所有固有类型一样,结构也是值类型。

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

陷阱四.警惕隐式装箱

     装箱和拆箱是使值类型(如整型等)能够象引用类型一样被处理的过程。值被装箱进一个对象,随后的拆箱则是将其还原为值类型。C#里的每一种类型包括固有类型都是从object派生下来并可以被隐式转换为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

C++11 FAQ中文版

C++11 FAQ中文版 http://www.chenlq.net/cpp11-faq-chs http://www.stroustrup.com/C++11FAQ....
  • bamboolsu
  • bamboolsu
  • 2015年03月20日 17:41
  • 1518

分组Top N问题(一) - java实现Top n算法基础

前言: 在分析MapReduce、Hive、Redis和Storm、Spark等工具实现分组Top n问题前,我们先看下java最原始实现Top的方法有哪些,为后面奠定些基础,这也是我要整理成一个系列...
  • zeb_perfect
  • zeb_perfect
  • 2016年11月25日 14:58
  • 2854

C++Primer中文版(第5版)(顶级畅销书重磅升级 全面采用最新 C++ 11标准)

C++Primer中文版(第5版)(顶级畅销书重磅升级 全面采用最新 C++ 11标准) 【美】Stanley B.Lippman( 斯坦利 李普曼)  Josee Lajoie(约瑟 ...
  • broadview2006
  • broadview2006
  • 2013年09月10日 16:05
  • 3584

C++实现数组中出现最频繁的前top k个元素

要求: 时间复杂度小于等于 nlogn. 算法解题思路: 1, 由于原始数组是杂乱无序的, 所以 统计数组中元素出现的次数时间复杂度达到了n^2, 不符合题意. 2, 在统计数组中的元素出现次数之前,...
  • u010506130
  • u010506130
  • 2016年05月10日 21:11
  • 1219

算法——TOP K问题最小堆实现

1. 问题背景在实际应用中,我们经常会遇到在一大推数据中找出最大的几个数的问题,也就是我们提到的TOP K问题。K表示需要找出数据的数量2. 解决方案TOP K问题也有多种解决方案,比如排序,最后截取...
  • CYXLZZS
  • CYXLZZS
  • 2016年05月11日 16:46
  • 2140

《Effect C++》学习------基本知识

术语我觉得C++里面比较重要的基础就是一些术语。 - 首先较为重要的术语就是声明式,它是用来告诉编译器某个东西的名称和类型,但略去细节。如下面几个例子:extern int x; //对象声明式...
  • qq_19528953
  • qq_19528953
  • 2016年08月10日 10:55
  • 1039

Top K算法详细解析--- 百度面试

问题描述: 这是在网上找到的一道百度的面试题: 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复度比较 高...
  • Aries_zz
  • Aries_zz
  • 2014年02月02日 21:20
  • 4031

为《C++ Primer》第5版中文版写推荐序

花了几个晚上把C++ Primer第5版中文版翻看了一遍,主要关注2011标准中新增的一些要素的讲解。 ------------------------------ 推荐序 书名:C++ P...
  • panaimin
  • panaimin
  • 2013年09月14日 23:35
  • 6648

OWASP Top 10十大风险 – 10个最重大的Web应用风险与攻防

先来看几个出现安全问题的例子OWASP TOP10开发为什么要知道OWASP TOP10TOP1-注入TOP1-注入的示例TOP1-注入的防范TOP1-使用ESAPI(https://github.c...
  • lifetragedy
  • lifetragedy
  • 2016年09月18日 14:45
  • 26410

Top K算法和寻找第K个最小的数

关于Top K算法和寻找第K个最小的数这种经典问题网上已经说的很详细了,不过毕竟不是自己的,这里自己总结一下,而且这两个问题又稍稍有点区别。 1.Top K算法:即寻找一列数中K个最小值或K个最大值,...
  • moses1213
  • moses1213
  • 2016年01月19日 19:34
  • 1632
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Top Ten Traps in C# for C++ Programmers中文版(上篇)
举报原因:
原因补充:

(最多只允许输入30个字)