我的OOP学习笔记值与引用语义类型
Posted 庸人、自扰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我的OOP学习笔记值与引用语义类型相关的知识,希望对你有一定的参考价值。
值与引用
值语义的对象是独立的,语义的对象却是允许共享的。由于Java不支持值类型对象,Java程序员才更需要加强这方面的意识。语法和语义并不总是一致的——语法上的值类型可能在语义上是引用类型,语法上的引用类型可能在语义上是值类型。永远不要忘记一个基本原则:语法只是手段,语义才是目的。
为了判断一个类型的语义,那么简明的‘石蕊测试法’便是一个很好的选择.在不影响程序正确性的前提下,一个对象的复件能否代替原件?如果可以则该对象的类型是值语义的,否则是引用语义的。(这种判断方法与语法无关,完全取决于对象设计者的用意。)
从命令式编程的角度看,一个值语义变量的内存地址是无关紧要的,原件和复件的唯一差别在被清除后变得完全的等价,因而值语义又称复制语义(copy semantics)。相对地,引用语义变量的内存地址至关重要,通常用指针来实现,因而引用语义又称指针语义(pointer semantics)
从函数式编程的角度看,值应当是引用透明的,即一个表达式随时可被其值所替代。比如2+3总可以被5代替,“ab”.concat(“cd”)总可用“abcd”来代替。显然,值的可替代性实质上抹杀了引用的作用。(在计算机术语中,透明transparency一词很容易引起误解,它不是指因透明而看得见,而是指透明得看不见或意识不到、不受影响)
从对象式编程的角度看,值语义与引用语义的区别在于对象标识(object identity)的重要程度。对象标识是一个对象区别于其他对象的唯一的标识。它的每个对象都具备的一个特质,反映了一个对象作为实体的独立性、可识别性和本体性,是对象的三大特性之一。OOP中对象的三大特性是状态(state)、行为(behavior)和标识(identity)。倘若一个对象的标识在程序中没有实际意义,意味着它的对象特性模糊、主体意识淡薄,更多地代表的是一种抽象的属性(attribute)而非一个具体的实体(entity),则它具有值语义。反而,则具有引用语义。值通过具体的数据来描述抽象的属性,引用通过抽象的方式来指代具体的实体。
比如字符串,Java和C#中的String类虽然是引用类型,但它们的值语义是很明显的。人们关心的是字符串的内容,而非它的内存地址。Java初学者最容易犯的一个错误就是用相等运算符(equality operator)‘==’来判断字符串的异同。用==比较的是字符串的引用,用equals方法比较的才是字符串的内容。因此,C#干脆明智地重载(overload)了String的相等运算符,避免了这类错误。虽然C++同样也重载了该运算符,但是C++中包括String在内的所有自定义类型本就是基于值语法的,它们的相等运算符自然不可能用于引用比较。C++中基于指针的char*字符串类型的比较也不能用==运算符,而应该用strcmp函数。
值与引用还有一个区别。值是不依赖内存地址的,即具有空间无关性。其实值还有时间无关性,即一个值语义的对象在其生命周期中的状态是固定的。也就是说,值语义类型一般是不可变的(immutable)。以Java中的String为例:
String s1 = “ab”; String s2 = s2;//这行的赋值是基于引用的,因此s1与s2指向同一个字符串对象。 assert(s1=s2); s2 += "cd"; assert(s1 != s2);
如果String是可变的,那么当s2的内容从“ab”变成“abcd”后,s1的内容也会发生相应的变化。这就产生别名问题(aliasing problem),通常并非我们想要的结果,因此在s2完成自增运算后,系统便让它换成另一个字符串对象,而原先的字符串对象任然保持不变。这样使我们省去了显示深克隆的过程。由此可见,不变性为引用类型贯彻值语义提供了变通的语法支持。
Java和C#中的StringBuffer是可变的字符串,角色的定位不是字符串的持有者,而是字符串的创造者,故而是可变的,并不具备值语义。C++中的string类由于是值类型的,对不可变的需求没有那么强烈。但除非特别需求,程序员还是应尽可能地保持字符串对象的不可变性。除了在赋值、按值传递、作为返回值等是凭借值类型的特点来保证字符串的复制以外,还可利用关键字const来保证常量正常性(const-corectness)。
Java和C#中的基本数据类型是值类型的,但有时需要以引用类型身份出现,这就需要一个转化过程,术语为封装(boxing),逆过程称为拆箱(unboxing)。装箱后的对象虽然是引用类型的,但仍保持值语义,因此应该是不可变的。
一个值语义的引用类型也有可能是可变的,比如Java的日期类型Date类。为了实现其值语义,需要手动进行必要的防御性复制(defensive copying)。比如在getter方法返回对象的日期属性之前、在setter方法传入的日期参数赋值之前,都应拷贝一份对象。虽然这就影响到程序性能,但程序的正确性永远是第一位的。略讽刺意味的是,当初Date类被设计成可变类型是为了减少对象的创建,但结果却事与愿违,反复的防御性复制也许会创建更多的对象。避免重复的防御性复制的一个方法就是在规范文档中进行相关说明。但这很不自然,同时不能保证客户遵守规范。
值语义对象不是真正意义的对象,接下来就是关于值的时间无关性在语义上的意义。值是静态而单纯的,而引用是动态而复杂的。从这个角度上说,不可变性加强了值语义。比如整型是最常见的值类型之一,一个整型就是一个常量,理所应当是不可变的。同理,一个值语义的对象也应该是一个常量。举个具体的例子,颜色类型Color具备着典型的值语义,在Java和C#中都是不可变的。你看得出x=Color.Red与x=1有什么本质的区别吗?无非是取值空间不一样。当x被重新赋值为Color.Green时,原来的对象Color.Red并未发生变化,只是不再被x所引用。退一步讲,即使一个值语义对象是可变的,它与引用语义对象在概念上仍是有区别的:值语义对象的改变是一种新旧更替,即新对象更替旧对象,只是凑巧重用了后者所用的内存空间;引用语义对象的改变是一种自我更新。即一个对象在保持同一性的前提下发生的状态迁移或属性改变。
如果说不可变性让语法上的引用类型倾向于值语义,那么不可复制性则让语法上的值类型具备明显的引用语义。这非常自然,值语义的特点是复件具有等效性,引用语义的特点是复件不具等效性。当然,不可复制性是一种比较极端的情形,因为一边引用语义的类型也是允许复制的,只不过不能替代原件而已。由于语法的缘故(Java没有自定义的值类型,而C#中的值类型通常都是遵循值语义的。),具有不可复制性的值类型主要出现在C++上,著名的Boost C++库提供了noncopyable类。
传输对象的焦点在于“有什么”,值对象的焦点在于“是什么”,而引用对象的焦点在于"是哪个"。
合成是基于值语义的包含,聚合是基于引用语义的包含。
为了达到抽象的目的,实现级别的信息需要隐藏,靠的是访问控制;设计级别的信息需要过滤,靠的抽象建模。
以上是关于我的OOP学习笔记值与引用语义类型的主要内容,如果未能解决你的问题,请参考以下文章