李建忠


C#/.NET, C++/CLI 培训/咨询
随笔 - 13, 评论 - 266, 引用 - 26

导航

关于

标签

每月存档

最新留言

广告

《Effective C#》 翻译笔记(1)使用属性,避免将数据成员直接暴露给外界


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 on 2005-05-08 13:53:00 by lijianzhong  评论(28) 阅读(9237)

C#中Finalize方法的问题

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 on 2005-01-13 19:55:00 by lijianzhong  评论(30) 阅读(10180)

关于“值类型的Finalize不会被调用?”

 

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 on 2005-01-13 14:40:00 by lijianzhong  评论(27) 阅读(7051)

Powered by: Joycode.MVC引擎 0.5.2.0