理解值与引用

面向对象分析和设计需要区分对象的值语义与引用语义。我的一块钱和你的一块钱相等,这是值语义;20岁的我和30岁的我是同一个人,这是引用语义。值对象包括2大特征:内容和运算,比如:3这个整数在计算机内部用二进制11表示,可以参与+,-,*,/等运算;引用对象包括3大特征:标识、状态和行为,比如:person对象拥有不变的标识,并可通过行为改变状态。值对象的同一性建立在内容的基础上,而引用对象的同一性建立在标识的基础上。

struct和class是OOP语言为分别表达值语义和引用语义所提供的语法机制。值对象只能被动地参与运算,引用对象拥有主动的行为。C#中struct可以实现接口,可以定义方法;但从语义角度,对于struct DateTime对象todaytoday.AddDays(1)的语义是today1参与AddDays运算得出一个新的日期值。

由于值对象的同一性建立在内容的基础上,而引用对象的同一性建立在标识的基础上。值对象从语义上是不可变的,而引用对象从语义上是不可拷贝的。但在实际实现中由于性能考虑常常会用引用语法来实现值语义,比如:C#的string是值语义,但却是引用语法;类似的,flyweight模式被共享的小对象也是引用语法表达值语义。由于值语义的不可变性要求,用引用语法模拟值语义常常会加上不可变的限制,比如:C#的string对象在参与+运算本身并不改变,而是产生一个新string对象作为结果。这种通过不可变引用语法来模拟值语义的手段在不支持struct的Java中更加常用。而在C++中,虽然提供了struct和class,但二者在语法上都是值语法(可拷贝);所以,在C++中常常会为class加上不可拷贝的限制明确引用语义,强制使用指针或引用的方式使用对象。

在使用上,值对象通常用于两种场合:1.表示引用对象的某一属性,比如:颜色RGB,字体类型,出生日期,地址;2.对象间传递的消息,即作为方法调用的参数。下面通过一个例子进一步体会值与引用的区别:

//C#
class Person{
    public DateTime GetBirthday() { 
         return m_birthday;
    }
    private DateTime m_birthday;
}

值类型的m_birthday对象是为了描述引用类型Person某一方面属性的。如果DateTime是class,那么GetBirthday只需要返回引用;而DateTime如果是struct,GetBirthday就是值拷贝。从性能角度,返回引用优于值拷贝;但从设计角度,返回引用使得外部可以直接修改Person对象的内部状态,破坏了封装。如果是Java,由于不支持struct,所以只能在引用语法的基础上模拟值语义,比如:返回一份clone。显然,由于C#对struct的直接支持,一旦明确了m_birthday的值语义那么语法的支持直接而简洁。

确定领域对象是值语义还是引用语义是分析设计阶段的事情,其原则是基于值对象和引用对象的特征。而对于struct和class的选择是实现阶段的事情。假如分析设计确定领域对象是引用语义,那么语法选择只有class,不可能去选择struct;而如果领域对象是值语义,比如DateTimestring,那么语法选择可以是struct也可以是class,struct是默认的选择,即使由于性能原因选择了class,还是需要像string一样注意设计细节去表达值语义。在明确领域对象的语义后,我们提倡首选默认选项,即用struct表示值,用class表示引用。默认不需要理由,不用默认才需要理由。

posted on 2010-07-24 15:48  Todd Wei  阅读(2911)  评论(26编辑  收藏  举报