RSS 2.0 Feed

Friday, September 23, 2005

由于Stan Lippman的突然缺席(不过各位不必太过遗憾,Lippman会在年内来中国弥补 http://blog.dreambrook.com/jzli/archive/2005/09/20/1196.aspx),北京九华Tech Ed上有关VC++ 2005的Session 只有我这一场了,战战兢兢,如履薄冰。

 

我的Session: Visual C++ 2005 : 无缝集成,无限潜力

9月25日下午14:45--15:45

课程编号:DEV 349

我将在此次讲座里谈谈如何使用VC++ 2005的几种集成技术来无缝地桥接managed world和unmanaged world。然而1个小时(还包括10分钟的Q&A)的演讲确实太短,ppt本来我是制作了很长的,但是不得不忍痛割爱了。不过如果时间允许的话,我还是希望略微谈谈“使用C++/CLI来迁移ISO-C++下的Application Framework”这样的大话题。

 

BTW,刚刚从Herb Sutter的blog http://pluralsight.com/blogs/hsutter/archive/2005/09/22/14970.aspx得知C++/CLI Standard已经接近被ECMA批准,那些认为C++/CLI昙花一现的言论不攻自破。

So, Learning C++/CLI as a New Language! 不知博客堂的各位看官有几位会有耐心来捧场?

 

 

posted @ | Feedback (29) | Filed Under [ C++/CLI ]

Friday, July 01, 2005

最近在讲授C#培训课程中谈到托管对象内存的布局(Layout)和托管对象大小的问题时,一位学员问了这样一个问题 “既然每一个对象都是有大小的,为什么C#语言没有提供sizeof操作符?”

 

当然这个问题准确的问法应该是“为什么C#语言的sizeof操作符不能应用于class上?”,因为实际上C#是提供有sizeof操作符的,只不过只能在unsafe代码中对struct类型使用。而更进一步的问题便是“如何获取托管对象的大小?”

 

 

这个问题表面看起来并不大,但是仔细思考起来却很有意思,如果真的深究下去,恐怕需要把.NET挖个底朝天。正好我在SSCLIShared Source Common Language Infrastructure)上有过一段研究心得,于是便打算写一组文章来探讨这个问题,希望能够满足那些喜欢 Under The Hood”的朋友的胃口。

 

有关“Under The Hood”的解释,可以参见孙展波先生这里的一篇bloghttp://blog.joycode.com/zhanbos/archive/2004/06/10/24208.aspx.当然也可以到这里http://blogs.msdn.com/matt_pietrek/ 寻找“Under The Hood”的创始人Matt Pietrek

 

 

在回答这些问题之前,首先要谈谈“既然每一个对象都是有大小的”这句话,因为我发现并不是每一个程序员对此都有非常清晰的理解。C++程序员一般对此有比较好的观念,但是很多C#程序员、VB.NET程序员,Java程序员对这样的观念很淡薄,甚至许多“搞了很长时间面向对象”的程序员压根就不知道对象还有所谓大小这回事,这也是我在讲授C#/VB.NET培训课程时要着力强调它的原因。

 

因为要搞清楚对象的大小,必须搞清楚对象的Layout。而只有对对象的Layout非常清楚,才能彻底理清栈、托管堆、值类型、引用类型、参数传递,虚方法调用(多态)、垃圾收集。。。。等等这些编程中的核心问题。我以前在面试C#/.NET开发人员时,就把“一个class中定义有一个int、一个byte、和一个string,那么这个class的实例对象有多大”这样的问题作为对一个.NET程序员的“终极测试题”——如果这个问题回答得令人满意,基本上再问其他问题都显得多余。我甚至偏执地认为,只有“清楚了解对象Layout的程序员”才是“真正懂面向对象的程序员”。

 

言归正传,现在开始具体谈谈如何来获取一个托管对象的大小——注意这里说的是“一个托管对象的大小”而不是“一个类型的大小”,后面我会解释为什么要这样说。在这之前,先来看看C#C++/CLI目前的sizeof操作符在值类型上的行为,它对后面探讨托管对象的sizeof多少有一点帮助。

 

 

 C#C++/CLI中的sizeof操作符

 

C#C++/CLI都提供了sizeof操作符,比如在C++/CLIC#不支持在一个含有引用类型字段的值类型上使用sizeof操作符)中,我们可以这样来计算一个值类型的大小:

 

value struct MyValue { Byte data1; int data2; String^ data3; Byte data4; }; int main() { int size=sizeof(MyValue); Console::WriteLine(size); }

 

这里的输出结果为12,单位为bytes。和ISO-C++class的结果不同。其原因为CLR对值类型的Layout做了优化调整,事实上对于上面的声明顺序,CLR在内存中调整如下:

 

value struct MyValue { String^ data3; // 4bytes 指针 int data2; // 4bytes 整数 Byte data1; //1byte 整数 Byte data4;   //1byte 整数 };

由于alignment的作用,最后两个Byte字段后面要再填充两个bytes的空间,因此结果为4+4+1+1+2=12——这种做法显然是比较优化的算法,能够最大程度地节省一个栈对象的内存开销(ISO-C++按字段声明顺序来排列Layout,结果为16)。

 

顺便提一句,从编译出的元数据来看,编译器自动在MyValue上应用了如下Attribute

[StructLayout(LayoutKind::Sequential)]   // 顺序排列Layout

 

但是CLR并没有理会这一Attribute,而是自我主张使用了如下Attribute

[StructLayout(LayoutKind:: Auto)]     // 自动排列Layout,优化方案

 

 

不过sizeof只能在值类型上使用,而不能在引用类型上使用,计算的也是这些值类型在栈上分配的“裸对象”的成本——而非box到托管堆上的多态对象。

 

 

C#C++/CLI不允许在托管类型上使用sizeof,并不代表托管对象没有大小。那么,托管对象的大小如何计算呢?

 

 

预知详情,下回分解:)

 

posted @ | Feedback (21) | Filed Under [ C++/CLI ]

Thursday, May 12, 2005

近来接到几个朋友问Visual C++ 2005 (C++/CLI) Webcast中讲的“值类型的强类型装箱实例”是什么?

讲座比较匆忙,因此对这个技术点只是点了一下,没有详细展开。这里借blog把它展开说一下。


首先来看下面的C#代码:

using System; using System.Collections; struct MyClass { public int data; } class Test { public static void Main() { MyClass myClass1 = new MyClass(); MyClass myClass2=new MyClass(); ArrayList list=new ArrayList(); list.Add(myClass1); list.Add(myClass2); Print(list); for(int i=0;i<list.Count;i++) { MyClass temp=(MyClass)list[i]; temp.data=i+1; list[i]=temp;// 注意这句话 } Print(list); } public static void Print(ArrayList list) { for(int i=0;i<list.Count;i++) { MyClass temp=(MyClass)list[i]; Console.WriteLine(temp.data); } } }

其中list[i]=temp这句话很重要,否则无法实施改变,因为temp是一个“和list中hold的boxed的值实例无关的”stack上的实例。

但是如果使用C++/CLI,我们就可以这么做:

using namespace System; using namespace System::Collections; value class MyClass { public: int data; }; void Print(ArrayList^ list); int main() { MyClass^ hMyClass1 = gcnew MyClass; MyClass^ hMyClass2 = gcnew MyClass; ArrayList^ list=gcnew ArrayList(); list->Add(hMyClass1); list->Add(hMyClass2); Print(list); for(int i=0;i<list->Count;i++) { MyClass^ temp=(MyClass^)list[i]; temp->data=i+1; // 注意这里无需再有list[i]=temp这句话,因为temp引用的就是“list中hold的boxed的值实例” } Print(list); } void Print(ArrayList^ list) { for(int i=0;i<list->Count;i++) { MyClass^ temp=(MyClass^)list[i]; Console::WriteLine(temp->data); } }

其实在C#中,我们使用MyClass temp=(MyClass)list[i]; 中间就发生了一个unbox和一个copy动作。

而在C++/CLI中,我们使用MyClass^ temp=(MyClass^)list[i]; 中间只发生了unbox,而没有发生copy动作——注意只发生unbox,而数据还位于managed heap中,但是类型却是MyClass^,这样我们就可以直接获取MyClass的data成员——这就是我所说的“值类型的强类型装箱实例”。

这在C#中是不能做到的,只能获得类型为Object的一个“弱类型”——有一种办法是采用interface来间接实现,因为interface是一个引用类型。

 

“值类型的强类型装箱实例”当然不仅仅意味着“少写代码”,实际上它带来很好的性能提升——因为unbox代价很小,与copy动作是分离的。不像box,它是代价比较高的操作,因为本身包含copy动作。


其实C++/CLI对类型的强大描述能力不仅仅体现在这一个地方,还有很多,比如interior_ptr,这将是我今后将要剖析C++/CLI的一个重点, 也是我去年撰写“漫谈C++/CLI中的几种指针和引用(1)”(http://blog.dreambrook.com/jzli/archive/2004/11/20/352.aspx)系列的规划——可惜后来由于太忙,没有集中的时间展开。展开讨论它要相当大的篇幅,因为它牵扯到对托管对象模型的深入剖析。下面我会随着自己对Shared Source CLI (Rotor)源代码的剖析,来展开这一系列的blog。

posted @ | Feedback (18) | Filed Under [ C++/CLI ]

Sunday, May 08, 2005


Item 1 - Always Use Properties Instead of Accessible Data Members.

 

最近在翻译Bill Wagner先生的《Effective C#》一书,由于自己早先也有Effective.NET写作的打算,所以对书中很多items,也有很多自己的思考。如果作为译注来添加,担心把最后的译本添得四不象,不添又甚感遗憾。正好有blog和论坛,遂考虑把翻译过程中自己的所思所想直接记录下来,供大家讨论打磨,弥补作/译者认识不足的地方,相信也许可以收到正常出版渠道不能取得之效果。blog好像本来就含有“个人出版”之意。言归正传,今天讨论第一个Item。

学习研究.NET的早期,经常碰到一些学习C#/.NET的朋友问,要属性这种华而不实的东西做什么?后来做项目时也时常接到team里的人的抱怨,

为什么不直接放一个public字段?

class Card
{
   public string Name;
}


而非要做一个private字段+public属性?
class Card
{
   private string name;

   public string Name
   {
      get { return this.name;}
      set { this.name=value;}
   }

}


我记得早期的一个项目里,team中的一个朋友甚至厌烦了写private字段+public属性,尤其是碰到一大堆臃肿的data object class的时候,索性自己写了一个小工具,来提供一个类的字段名和类型,然后自动为该类生成相应的private字段+public属性。

我在编程的时候是个彻底的实用主义者,用稍微高雅一点的话说叫“不喜欢过度的设计”。如果真的象上面那样写Card,而且在将来没有什么改变的需求,我也不喜欢后面那样把事情故意搞得复杂。但如果从component的角度来讲,总有一些class是要供外部长久地使用,也潜在地有改变的需求。这时候,提供属性就很有必要了。


这就是这个item试图要归纳的使用属性的理由:

1。可以对赋值做校验、或者额外的处理。
2。可以做线程同步。
3。可以使用虚属性、或者抽象属性。
4。可以将属性置于interface中
5。可以提供get-only或者set-only版本;甚至可以给读、写以不同的访问权限(C# 2.0支持)


个人感觉3、4条是属性最大的优点,可以填补没有“虚字段”或“抽象字段”的缺憾,在设计组件的时候非常有用,也体现了C#这样的component-oriented语言的精神内涵。

但如果没有上述理由,日后也并不太可能做大的改动,我想也大可不必非要把每个public字段都要变成属性。比如在设计一些轻型的struct,用于互操作的时候,直接使用public字段没什么不好。所以,感觉本条目Bill Wagner先生使用“Always Use Properties Instead of Accessible Data Members”显得太过强硬。

 

其实,这里的讨论也表明阅读Effective C#一书时需要注意的地方,即Effective原则并不是防之四海皆准的。不同的项目(组件化、复用程度较高的项目? 还是“一次编写、n年都run”的项目),不同的角色(类库/组件开发人员? 还是应用程序开发人员?),有着不同的Effective准则。 事实上,书中很多items都是从类库/组件开发人员的角度来考虑的。

posted @ | Feedback (28) | Filed Under [ C#/.NET ]

Tuesday, April 19, 2005

下面几个月我会在MSDN中国的WebCast讲授一个系列的C++/CLI,也就是VC++ 2005的语言内核。下面是这个系列的介绍。其中(1)和(2)分别安排在4月20日和4月28日。欢迎对C++/CLI感兴趣的朋友来这里http://www.microsoft.com/china/msdn/default.aspx捧场:)

 

直接免费注册即可,对于时间不凑巧的朋友,MSDN中国在讲座之后提供有视频下载服务。

 

(1)VC++ 2005 (C++/CLI):基础概览
VC++ 2005(又称C++/CLI)是微软为广大C++程序员量身定做的,面向.NET平台的一门系统级编程语言。如何认识VC++ 2005?它为我们带来了什么?怎样才能学好VC++ 2005?本课程将对其做一概括性的介绍,并就这些热点问题做详细的探讨,帮助观众认识这一全新的编程语言。


(2)VC++ 2005 (C++/CLI):类型系统

类型系统是一门编程语言的“立身之本”,VC++ 2005由于对ISO-C++和CLI实现了集成而使得其类型系统在突显强大的同时,也凭添了许多复杂。本课程将对VC++ 2005包含的两大类型系统:托管类型系统和本地类型系统,及其可能的混合体进行全面的探讨,为您建立一个清晰的类型图景。


(3)VC++ 2005 (C++/CLI):类型成员

作为支持面向组件程序设计的编程平台,CLI和ISO-C++有着迥异的设计思路,其中一个表现就在类型成员的设计上。本课程将向大家介绍CLI托管类型系统中的各种成员(除析构函数),包括字段、方法、构造函数、操作符、属性、事件以及应用在它们之上的各种修饰,并就它们和ISO-C++本地类型系统中的类型成员做横向的比较。


(4)VC++ 2005 (C++/CLI):确定性资源清理

确定性资源清理是C++/CLI中提出的一个新的语言特性,它利用C++便捷的语法,简化了.NET应用程序开发时对非托管资源的处理,而这在其他.NET语言中需要繁杂的Dispose模式才能实现。本课程将对C++/CLI中确定性资源清理所涉及到的语法构造、运行机理等进行深入的剖析。

(5)VC++ 2005 (C++/CLI):指针与对象模型

指针是C++语言的精髓,也是C++语言的难点,由于CLI平台的托管特性,C++/CLI中出现了各种指针的变体,可谓难上加难。为什么C++/CLI的指针类型如此复杂?因为C++/CLI背后的对象模型非常复杂。C++/CLI中的指针类型完整映射了ISO-C++本地世界和CLI托管世界所包含的所有对象模型。本课程将从本地对象模型和托管对象模型入手,步步深入,探讨C++/CLI中的各种指针。

 

(6)VC++ 2005 (C++/CLI):元数据与动态编程

如果问CLI和ISO-C++最大的区别是什么?答案一定是元数据。元数据是CLI组件平台的灵魂,它在构建整个CLI组件平台中居功甚伟。在夯实CLI各种组件基础设施的同时,元数据也赋予了CLI强大的动态编程能力。本课程将从元数据入手,探讨C++/CLI中的动态编程。


(7)VC++ 2005 (C++/CLI):泛型编程

泛型编程在C++领域中早已深入人心,它赋予了类型参数式多态的能力,这种能力在ISO-C++中以编译时的模板实例化为依托。而CLI借自己强大的元数据系统,选择了运行时的模板实例化来支持泛型编程。C++/CLI在保留ISO-C++“编译时泛型编程”的同时,也增添了对CLI“运行时泛型编程”的支持。本课程将着重介绍C++/CLI中的“运行时泛型编程”,并将它们和“编译时泛型编程”做横向的对比。

 

(8)VC++ 2005 (C++/CLI):与ISO-C++的集成

在选择支持CLI的问题上,C++/CLI大胆地选择了“集成”而非“替换”的策略。同时支持ISO-C++和CLI两种编程方式并不复杂,但如何将二者在对象模型的层次上集成在一起则是一个非常复杂的问题。本课程将以ISO-C++本地对象模型和托管对象模型为纲,介绍C++/CLI中的集成技术。


(9)VC++ 2005 (C++/CLI):非托管互操作

代码重用是任何一个编程平台、语言都不可忽视的问题,C++/CLI同样也不例外。实际上C++/CLI不仅支持模块级(DLL动态链接库)、和组件级(COM组件)的重用,同时也支持源代码级(IJW,It Just Works技术)的重用。本课程将介绍这些互操作技术。

 

BTW,本周六在徐家汇有IT俱乐部的4月线下活动(http://www.chinaitclub.org),主题是Windows 移动开发平台,嘉宾为同济大学的何宗键先生,何先生是微软 Windows 嵌入式开发认证讲师,在嵌入式/移动开发领域有很好的造诣。对移动开发感兴趣的朋友,不可错过。

 

posted @ | Feedback (29) | Filed Under [ C++/CLI ]

Wednesday, March 16, 2005

IT俱乐部新春首期活动:高级Windows调试

3月20日(周日),上海·徐家汇·美罗大厦

主讲人为来自Intel的有9年WINDOWS平台工作经验的资深软件工程师张仁魁(Raymond Zhang)先生。张先生将以著名的蓝屏死机问题(BSOD)为线索,层层剖析Windows内核,畅谈高级Windows调试技术,对于喜欢底层体验的程序员来说,不可错过。

除了2个小时的技术主题外,我们还设置了1个小时的软件开发及软件人的成长主题交流论坛,供大家畅所欲言。

 

已经注册的用户不必再注册,我们会根据用户的情况进行筛选。对俱乐部感兴趣的新用户可以到这里http://www.zhucheng.biz/club/club.aspx了解情况并注册。


BTW, 年前1月23日的我的Effecitive.NET活动相关图片和ppt可参见这里http://www.zhucheng.biz/club/record.aspx

由于近来忙于融资和公司搬迁的事情,网站也在进行全面改版,所以在外部显得有些工作停滞,希望各位朋友理解。不过大家很快将会看到IT俱乐部的发展,同时也欢迎各界朋友为IT俱乐部献计献策,共商合作。比如筹建IT酒吧、IT书屋、IT KTV等......

 

 

posted @ | Feedback (11) | Filed Under [ 程序人生 ]

Wednesday, February 23, 2005

明天我在微软MSDN上有一个C++/CLI方面的技术讲座Webcast:

讲座主题:VC++ 2005:确定性资源清理

活动日期:2005年2月24日 14:30--16:00

http://www.microsoft.com/china/msdn/events/webcasts/Webcast/webcast_Feb05.aspx

欢迎各位对C++/CLI(VC++ 2005)感兴趣的朋友捧场。

Web授课是个好东西,只要你有一台电脑就可以很方便地参加,消除了很多地域限制。今后祝成科技的咨询/培训组也会在网上推出一些精品的系列网络课程,其中正在制作的就有我的C# 2.0和VC++ 2005课程,希望能满足各地朋友的需要。

 

无独有偶,Herb Sutter在Channel9上也有两段video:

Herb Sutter - The future of Visual C++, Part I (http://channel9.msdn.com/showpost.aspx?postid=39280

Herb Sutter - The future of Visual C++, Part II(http://channel9.msdn.com/ShowPost.aspx?PostID=39463


非常值得一看。

 

 

posted @ | Feedback (12) | Filed Under [ C++/CLI ]

Sunday, January 23, 2005

Effective .NET演讲圆满结束,拖着疲惫的身体回到家,非常感谢大家的支持。

唯一的遗憾是时间安排太紧,一些item匆匆过了,而没有深入的展开,还有一些item由于考虑到讲座的覆盖度,没有列出。这些主题将会在我将来的书稿中得到完整的体现。

我会把Effective C++/CLI努力写好,也希望大家能贡献自己的智慧,一起来完善这本书。

posted @ | Feedback (28) | Filed Under [ 程序人生 ]

Wednesday, January 19, 2005

ninputer在关于“值类型的Finalize不会被调用”中(http://blog.joycode.com/lijianzhong/archive/2005/01/13/42991.aspx#FeedBack)评论到“VB对Finalize管的可松呢,可以直接重写、直接调用、允许不调用父类的Finalize,或者多次调用父类的Finalize等等…… 完全不像C#”。

其实C#的Finalize方法看起来只是比VB的好一点,但仍然有非常隐蔽的问题。问题如下。

首先来看如下的代码:

using System;

public class Grandpapa
{
     ~Grandpapa(){ Console.WriteLine("Grandpapa.~Grandpapa");}
}

public class Parent:Grandpapa
{
     ~Parent(){ Console.WriteLine("Parent.~Parent");}
}

public class Son:Parent
{
     ~Son(){ Console.WriteLine("Son.~Son");}
}

public class App
{
     public static void Main()
     {
         Son s=new Son();
 
         GC.Collect();
         GC.WaitForPendingFinalizers();
     }
}

这段代码的运行结果毫无疑问是:

Son.~Son
Parent.~Parent
Grandpapa.~Grandpapa

这没什么问题。但是如果将Parent类重新定义如下,会出现什么情况呢?

public class Parent:Grandpapa
{
     protected void Finalize(){ Console.WriteLine("Parent.Finalize");}
}

运行结果变成了:

Son.~Son
Parent.Finalize

情况已经有些不妙了,我在Parent中定义了一个“普通”的Finalize方法,竟然被它的子类Son的析构器给调用了?

当然Finalize方法在C#中并不是一个“普通”的方法,析构器编译后就是一个有上述签名的Finalize方法。但C#编译器并没有禁止我们定义“普通”的Finalize,

C#规范也没有指出定义这样的Finalize方法就是在定义一个析构器——实际上也不是,只是上述代码的表现如此——甚至还有这样一句诱人犯错的话:The compiler behaves as if this method(Finalize), and overrides of it, do not exist at all。分析IL代码可以看出,Parent中定义的“普通”的Finalize方法实际上“欺骗”了它的子类。它的子类只关心其父类是否定义了Finalize(当然签名要为上述形式)方法,它并不关心那个Finalize方法是否具有“析构器”语义。

如果上述代码的行为通过理性分析还算可以接受的话,那么下面代码的运行结果就令人眩晕了,将Parent类重新定义如下(在上面的基础上添加了一个virtual关键字):

public class Parent:Grandpapa
{
     protected virtual void Finalize(){ Console.WriteLine("Parent.Finalize");}
}

编译后运行结果如下:

Grandpapa.~Grandpapa

这一次从IL代码的角度也解释不清了,我怀疑CLR对于析构器的判断是否还有另外的附加条件,但无论如何C#编译器呈现的行为是诡异的,因为这种结果放到哪里都是难以自圆其说的。我曾经为此挖掘了sscli源代码很长时间,但是就是找不到原因。

这一方面是C#编译器的一个bug,另一方面也是CLR的一个bug。这个bug从.NET Framework的1.0版(VS.NET 2002),到1.1版(VS.NET 2003),以及Alpha版本的Longhorn操作系统中自带的1.2版都存在。后来我写信给C#的产品经理Eric Gunnerson(http://blogs.msdn.com/ericgu/)告诉他们这个bug。Eric Gunnerson随后回信告诉我他们会修复这个bug。

我现在使用Visual C# Express 2005编译器编译(version 8.00.41013)上述代码,后面两种修改版都会得到一个warning:

warning CS0465: Introducing a 'Finalize' method can interfere with destructor invocation. Did you intend to declare a destructor?

但是如果不理会这样的警告,得到的exe文件执行行为仍然是非常奇怪。也就是说CLR中的bug仍然没有fix。我个人认为对于C#编译器来说,warning是不够的,应该彻底禁止定义这样的Finalize方法。

 

实际上在我的Effective .NET (in C#)一书的draft里也有这样一个条款:

# 不要在一个类中有定义任何Finalize方法的念头,因为那样会对你的“析构器链”造成潜在的严重的伤害。

 

 

 

posted @ | Feedback (30) | Filed Under [ C#/.NET ]

Friday, January 14, 2005

本月23号(下周日)我在美罗大厦有一个topic:高效.NET应用程序设计原则。欢迎在上海附近,对.NET开发感兴趣的博客堂的朋友过来聊聊。

点击这里注册:http://www.zhucheng.biz/club/club.aspx

另外,本周日(16号)还有黄雪斌先生的“防范黑客攻击”,很有意思的话题。

BTW,注册的时候,建议你在地址部分加一个[博客堂],我会嘱咐给予特殊照顾:)

 

 

posted @ | Feedback (19) | Filed Under [ 程序人生 ]

Thursday, January 13, 2005

 

ninputer在这里(http://blog.joycode.com/ninputer/archive/2005/01/12/42866.aspx)有一篇blog提出了一个问题“值类型的Finalize不会被调用?”

我曾经对Rotor,也就是sscli(Shared Source Common Language Infrastructure),有过一些粗略的探索——不过现在由于比较忙,慢慢也半途而废了:)

这个问题可以从sscli里得到解释——sscli和目前运行在我们机器上的CLR实现差别主要在效率和扩展层面,因此研究它有助于理解CLR的行为。所有有关底层运作的代码都在目录sscli\clr\src\vm下。结合sscli的源码,下面我来聊聊这个话题。


首先给出一个结论:这是因为CLR对值类型进行了专门的设计,让它不可能进入Freachable Queue 里面。


下面根据sscli源码来对上述结论进行解释:

1。有关CLR类型一个最关键的类就是MethodTable。它的第一个字段m_wFlags(一个DWORD)的第21位 bit用来标示这个类是否有Finalizer。

MethodTable有一个方法为HasFinalizer就做此用:

MethodTable::HasFinalizer()   
{
     return (m_wFlags & enum_flag_HasFinalizer);
}
其中enum_flag_HasFinalizer =   0x100000,

GC在判断一个类型的实例对象是否需要放到Freachable Queue中,就是采用MethodTable::HasFinalizer()方法来判断。

 

2。最关键的是EEClass::BuildMethodTable,这个方法负责建立类型的方法表,它会被ClassLoader::LoadTypeHandleFromToken调用,ClassLoader::LoadTypeHandleFromToken又被ClassLoader::LoadTypeHandle和Module::BuildClassForModule调用。

用通俗的语言来解释就是“每一个类型被load到内存中的时候,它都会建立和该类型相关的方法表”,而我们在CLR中的所有对象都有自己的类型。

 

3。下面就是看EEClass::BuildMethodTable如何设置MethodTable::m_wFlags。

EEClass::BuildMethodTable中和“值类型的Finalize”这个主题相关的动作有以下几个调用(为简便起见我没有在这里写方法的参数):

EEClass::BuildMethodTable
{
     ...
     CheckForValueType
     ...

     CheckForEnumType

     ...
     GetMethodTable()->MaybeSetHasFinalizer

     ...
}


4。来看CheckForValueType和CheckForEnumType分别做了什么。

HRESULT EEClass::CheckForValueType(bmtErrorInfo* bmtError)
{
    if(...) //查看类型元数据
            SetValueClass();
}

HRESULT EEClass::CheckForEnumType(bmtErrorInfo* bmtError)
{
   if(...) //查看类型元数据
        SetValueClass();
}

再来看SetValueClass做了什么:

inline void EEClass::SetValueClass()
{
    m_VMFlags |= VMFLAG_VALUETYPE;
}

就是设置EEClass::m_VMFlags的第24位bit来表示这个类为“值类型”。

其中VMFLAG_VALUETYPE   = 0x00800000,

 

5。最后来看MaybeSetHasFinalizer做了什么(我简化了其中很多代码,只展示和本问题相关的代码逻辑)。

void MethodTable::MaybeSetHasFinalizer()
{

    if ( !IsValueClass())
    {

        if(...)
        {
            m_wFlags |= enum_flag_HasFinalizer;
        }

    }
}

这段代码的意思是只要IsValueClass()为true,那么MethodTable::m_wFlags的第21位 bit就不会被置1。

那么MethodTable::IsValueClass()做了什么呢?

inline BOOL MethodTable::IsValueClass()
{
    return GetClass()->IsValueClass();
}

inline DWORD EEClass::IsValueClass()
{
    return (m_VMFlags & VMFLAG_VALUETYPE);
}

判断EEClass::m_VMFlags的第24位bit看看其是否为“值类型”。


至此,整个来龙去脉已经非常清晰——CLR的设计者通过MethodTable::m_wFlags的第21位bit来控制一个类型是否有Finalizer,同时通过EEClass::m_VMFlags的第24位bit来控制一个类型是否为值类型。最后在调用EEClass::BuildMethodTable的时候,判断如果一个类型为值类型,那么就让它不可能具有Finalizer语义。

 

Ninputer 在随后的回复中还提了一个问题“如果值类型用了非托管资源怎么释放呢?”。

我的回答是:不要这么做,值类型当初就是为象integer这样的轻量级类型而设计的,持有非托管资源的类型天生就是一个“重量级类型”。当然你可以使struct实现IDisposable,但是那是不完整的Dispose模式。

实际上在我的Effective .NET (in C#)一书的draft里就有这样一个条款:

# 如果使用非托管资源,请把它封装在class而不是struct里面。

 

 

posted @ | Feedback (27) | Filed Under [ C#/.NET ]

Monday, January 10, 2005

C++/CLI中新推出的自动确定性资源回收(Automatic deterministic destruction)被视为一个优秀的设计。是使用所谓C++/CLI这个“新瓶”来装Bjarne Stroustrup提出的RAII这个“旧酒”。

这的确不错,相对而言,这个比C#中的using 关键字(dispose模式),以及Java中的hard-coded的dispose方法都要好许多。这个特性是由C++/CLI中栈对象(局部对象)来提供的,局部对象本身没错,RAII也是局部对象应有之义。

但问题在于C++/CLI中栈对象的可用性由于许多原因会大打折扣,使用起来已经远远不如ISO-C++中那样流畅。下面列出了损伤其可用性的几大硬伤:

#1。C++/CLI的栈对象并非真的位于栈中

只要类型是ref class,C++/CLI中的栈对象就仍位于托管堆中。仍然使用newobj IL指令来分配。如果R没有定义析构器(~R)(注意:C++/CLI中的析构器和C#中的析构器完全两回事),那么下面两行代码实际上将生成完全一样的IL代码:

R r;
R h=gcnew R;


好像记得Herb Sutter曾经说过他们将来可能会在真正的方法栈中分配r ——说实话恐怕只有C++背景的人敢这么“胡思乱想”:) 他们现在只是想在语法层面让程序员"感觉"就像r是从栈中分配的一样。又一个syntax sugar:)

当然为了对称和语义的完美,有时候还需要在r上应用%——虽然背后仍是什么也没做:)

 

#2。C++/CLI编译器默认情况下不会自动产生拷贝构造函数和拷贝赋值操作符

这一点非常令人烦恼,几乎让人“望栈对象而却步”。更糟糕的是BCL中的所有类型都没有提供拷贝构造函数和拷贝赋值操作符——因为恐怕只有C++/CLI会用到他们。

话说回来,即使C++/CLI会自动产生拷贝构造函数和拷贝赋值操作符,那么继承自BCL的类型还是会很麻烦。


#3。如果函数要被其他CLI语言调用,那么就不能将其参数设计为栈对象

a. static void add(R r){...}

编译出来有一个modopt元数据,所以可以被其他语言调用,但是如果被其他语言调用,比如C#,那么其他语言将是以传值的方式传递引用,而C++/CLI将是传递对象拷贝(要调用拷贝构造器),所以语义混乱,完全不可以这样做。


b. static void add(R% r){...}

由于编译出来都有一个modreq元数据,所以不能被其他CLI语言调用。

 

#4。如果函数要被其他CLI语言调用,那么也不能将其返回值设计为栈对象

a. static R add(){...}

b. static R% add(){...}

两者编译出来都有一个modreq元数据,所以都不能被其他CLI语言调用。

 

#5。使用BCL时,如果要传递栈对象,总要使用“莫名其妙”的%操作符

比如:

String s("abc");
ArrayList list;
list.Add(%s);

实在很不好,还是使用追踪引用比较好:

String^ s="abc";
ArrayList^ list=gcnew ArrayList();
list->Add(s);


总结一下

#1和#5对栈对象的可用性影响不算大,毕竟从语义层面来理解,还是行得通的。
但是,#2、#3、#4的影响就很大。#3和#4使得我们必须放弃使用栈对象来进行互操作。而#2会让编写C++/CLI代码非常的不方便——除非你以后不想使用栈对象。

现在的问题是,是否C++/CLI中的栈对象只是为了获得自动确定性资源回收而存在?值得这样做吗?

 

 

 

posted @ | Feedback (34) | Filed Under [ C++/CLI ]

Monday, December 27, 2004

承蒙开心就好的邀请,很高兴入住博客堂。作为微软技术的知名社区,博客堂是一个做技术blog的好地方。我在博客堂的blog将集中在C++/CLI和C#两个方面。我在http://blog.dreambrook.com/jzli/上维护有一个更全面更私人化的个人blog。博客堂的blog则集中在.NET相关的技术方面。

简单介绍一下我的情况:在.NET刚刚推出之时,我曾为MSDN(中国)写过一个《C#锐利体验》专栏。从此糊里糊涂进入技术写译界(业余),也和.NET结下了不解之缘。前后翻译了《Microsoft .NET框架程序设计(修订版)》和《Microsoft .NET框架程序设计——Visual Basic .NET 语言描述》。

目前我在上海祝成信息科技有限公司担任软件架构师,并讲授C#/.NET软件开发课程。业余时间写作《C#面向组件程序设计》(清华大学出版社,2005年出版)。同时和Stan Lippman先生合作撰写《C++/CLI全景体验》专栏。关于我的更多信息,可访问我的个人网站:http://www.lijianzhong.com/

希望以后和各位多多交流。

posted @ | Feedback (72) | Filed Under [ 程序人生 ]