值对象与引用对象

起因

之前一直写C#,因为有GC,所以不用关心对象的复制问题,默认的浅复制就够了,也就没去深究struct、class、值、引用间的区别,前段时间写了点C++,C++需要手动管理内存,如果类包含有指针或引用成员时就要遵循rule of three,实现复制、赋值复制和析构以正确管理资源,这种生命周期管理函数写多了感觉挺枯燥的,而且我发现许多domain object没有必要实现复制,因为大多数domain object并不适合用值语义来表达,首先它们本质不是一种“值”,并且我希望传递时传递同一个对象,而不是一个副本。

C#中同时保留了值类型和引用类型,而我几乎从未写过自定义的值类型。于是我就开始思考值对象和引用对象到底有什么区别,随即写下此文分享本人的见解。

值对象(value object)

什么是值对象

值对象是标识取决于状态的对象

你现在拿出一张纸币,它就是典型的值对象,虽然每张纸币都有一个唯一的编号,但实际使用中这个编号是没有意义的,我们关心的是它们的面值。

这里纸币的编号就对应值对象的标识(比如内存地址),纸币的面值就对应值对象表示的值。

其它值对象的例子:IP地址、RGB颜色、GUID、地理坐标、日期时间。

我的理解:值对象是用来表达一个信息的对象,它的状态是静态的,且比较“透明”;我们使用值对象时,关心的是它所表达的信息,而不是这个对象本身

值对象与不可变性(immutability)

值对象应该被设计成不可变的(immutable),因为

  • 从理论上讲值对象存在的意义就在于表示一个值,它的状态决定了它的标识,如果状态修改了,那就是一个新的值,新的对象。
  • 简化编程、避免bug
    如果值对象可变,当它被共享时,一处修改可能会影响另一处,修改操作就会产生“副作用(side effect)”,如java中的java.util.Date就是可变的引用类型,使用不当就会产生问题:

    这里的问题在于我们关心的其实只是一个“日期值”而不是一个java.util.Date对象,所以在传递时应该是传递表示的日期值,而不是直接传递java.util.Date对象。注释中的FIXME标注是一种解决办法。

值对象必须要实现为不可变吗?

但是如果把一个类实现为不可变的话,意味着修改一个成员就要创建一个新的对象,如果成员非常多的话,代码写起来会比较繁琐。
有些语言支持值语义(value semantics),对象传递时是传递的它所表示的值(传值),而不是传递对象本身(传引用),这就意味着传递过程就是“复制”,比如C++默认就是值语义、C#中的struct等,因为是复制,所以值对象不会被共享,也就没有了上面提到的那个问题,这种情况下,值对象可变的话也是完全可以的,但是值对象仍可能会被“按引用”传递,所以把值对象实现为不可变是最保险的手段。

值对象的实现

  • C++
    C++默认就是值语义,如果类状态较简单,则可以不做任何特殊处理。如果比较复杂,比如成员包含指针或引用,则就要实现生命周期管理函数
  • C#
    • 如果类状态较简单,可直接用struct,因为struct正好就是值语义
    • 如果状态较复杂则用class,类的设计者应该从接口上把它设计成不可变,如果没有这样设计,则使用者最好在使用时“手动实现值语义”(见上java.util.Date示例)。
  • java
    同C#中的class

与值对象相关的概念

struct与class

不管是C++和C#中的struct关键字,还是ruby中的Struct::new都是趋向用于定义简单的复合类型,所以struct适合用来定义没有复杂行为和状态的值对象。而class更趋向用于定义具有丰富逻辑、复杂状态的对象类型,struct、class和值对象、引用对象并不是一一对应的关系,但一般而言,值对象都不会太复杂。

字符串

抛开具体的实现,字符串是一个静态的字符序列,它的状态决定了它的相等性,是一种值。

问题1,为什么在C#、java中字符串都是引用类型呢?

“引用类型”是具体语言/平台实现中的概念,“值对象”,“引用对象”是语言无关的,引用类型的对象也可能是值对象,只是引用类型的对象不具有原生的值语义

将字符串实现为”引用类型“更多是出于性能考虑。

  1. 避免复制。传引用,可以避免发生内容的拷贝。
  2. 可以实现String interning(字符串扣留)。扣留操作会检查一个全局字符串扣留池,看有没有与给定字符串内容相同的已被扣留的字符串对象,如果有则返回,没有则进行扣留。如果程序中有很多内容相同的字符串对象,这样能节约内存,这也是Flyweight模式的一个案例。
    java示例:

    C#示例:

问题2,为什么C++中的std::basic_string和Ruby中的String都是可变的?

很简单,这样用起来更方便。对于C++,可以用const实现不可变性,而Ruby是通过Symbol来表示唯一的、不可变的字符串。

C#中的值类型

C#语言中的类型分为两类“值类型”和“引用类型”,strut和enum属值类型,class属引用类型。C#编译器处理struct时让该类型继承了System.ValueType这个抽象类,而enum则继承自System.Enum,System.Enum还是继承自System.ValueObject。

值类型 和 引用类型的区别只有一个:一个是值语义(传值,复制),一个是引用语义(传引用),至于什么“一个分配于栈,一个分配于堆”,这是具体实现的问题,而且值类型不一定分配于栈,比如作为引用类型的成员(被捕捉到闭包中同属该情况)。

引用对象(reference object)

引用对象是相等性取决于它的标识的一种对象。

为什么“相等性取决于标识”?因为在使用引用对象时,我们关心的是这个对象本身,在传递过程中,需要传递同一个对象,所以需要传递对象的“引用(即标识、一般是内存地址)”,这也就是“引用语义(reference semantics)”。

我们实际写程序中,使用对象的目的在于映射问题域中的事物,基本关心的是对象本身,所以对象大多都属于引用对象。

实现

  • 在C#和java中用class定义的类型叫“引用类型”,具有引用语义,创建的对象天生就是引用对象
  • 在C/C++中通常通过分配堆内存和传递指针实现

参考

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

1 1 收藏 评论

关于作者:taney

.NET Developer 个人主页 · 我的文章 · 1 ·  

可能感兴趣的话题



直接登录
跳到底部
返回顶部