【我的OOP学习笔记】值与引用(2)语义类型

值与引用

值语义的对象是独立的,语义的对象却是允许共享的。由于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类。

传输对象的焦点在于“有什么”,值对象的焦点在于“是什么”,而引用对象的焦点在于"是哪个"。

合成是基于值语义的包含,聚合是基于引用语义的包含。

为了达到抽象的目的,实现级别的信息需要隐藏,靠的是访问控制;设计级别的信息需要过滤,靠的抽象建模。

时间: 2024-08-05 16:54:04

【我的OOP学习笔记】值与引用(2)语义类型的相关文章

C# in Depth Third Edition 学习笔记-- 值类型和引用

I. C#中值类型和引用类型 1. 类class 引用类型,结构struct值类型 2. 数组是引用类型,即使元素是值类型,int[]是引用类型 3. 枚举是值类型enum 4. 委托类型delegate是引用类型 5. 接口类型interface是引用类型,但可以由值类型实现. II. 值的表达式:表达式“2+3”的值就是5:而对于引用类型的表达式,它的值是一个引用,而不是该引用所指代的对象,如String.Empty的值不是一个空字符串,而是对空字符串的一个引用. III. 变量的值在它声明

swift学习笔记(七)自动引用计数

与Object-c一样,swift使用自动引用计数来跟踪并管理应用使用的内存.当实例不再被使用时,及retainCount=0时,会自动释放是理所占用的内存空间. 注:引用计数仅适用于类的实例,因为struct和enumeration属于值类型,也就不牵涉引用,所以其存储和管理方式并不是引用计数. 当一个实例被初始化时,系统会自动分配一定的内存空间,用于管理属性和方法.当实例对象不再被使用时,其内存空间被收回. swift中的引用类型分为三种,即Strong强引用,weak弱引用和无主引用unw

C++学习笔记29,引用变量(1)

引用变量在创建的时候就必须初始化.无法创建一个未被初始化的引用. #include <iostream> using namespace std; int main() { int x=10; int y=20; int &r1; } 编译结果: 如果引用未被初始化,编译将报错. 修改引用: 引用总是指向初始化的那个变量,也就是说,引用一旦被创建并初始化之后就无法改变.这一规则有点让人迷惑.. 如果声明了一个引用的同时使用一个变量赋值了,那么这个引用就会一直指向这个变量. 在此后使用变

iOS: 学习笔记, 值与引用类型(译自: https://developer.apple.com/swift/blog/ Aug 15, 2014 Value and Reference Type

值和引用类型 Value and Reference Types 在Swift中,有两种数据类型. 一是"值类型"(value type), 它是每一个实例都保存有各自的数据,通常定义为struct, enum或tuple. 二是"引用类型"(reference types),它是多实例共享一份数据,这种类型通常定义为class. 在本文中,我们将展示值类型和引用类型各自的优点以及如何在二者之间选择. 它们有什么区别? 最基本的区别是 "值类型"

iOS: 学习笔记, 值与引用类型(译自: https://developer.apple.com/swift/blog/ Aug 15, 2014 Value and Reference Types)

值和引用类型 Value and Reference Types 在Swift中,有两种数据类型. 一是"值类型"(value type), 它是每一个实例都保存有各自的数据,通常定义为struct, enum或tuple. 二是"引用类型"(reference types),它是多实例共享一份数据,这种类型通常定义为class. 在本文中,我们将展示值类型和引用类型各自的优点以及如何在二者之间选择. 它们有什么区别? 最基本的区别是 "值类型"

《iOS应用逆向工程》学习笔记(四)iOS程序类型

越狱iOS中最常见的程序有Application, Dynamic Library和Daemon三类. 1.Application 除了传统意义上的App外,越狱iOS平台上还有两种App形式的存在:WeeApp(依附于NotificationCenter的App)和PreferenceBundle(依附于Settings的App),常见于Cydia平台. 普通App的bundle中存放的是可执行程序和所需资源,而framework的bundle中存放的是动态链接库. 主要关注App中的三个部分

IOS开发学习笔记-(2)键盘控制,键盘类型设置,alert 对话框

一.关闭键盘,放弃第一响应者,处理思路有两种 ① 使用文本框的 Did End on Exit 绑定事件 ② UIControl on Touch 事件 都去操作 sender 的  resignFirstResponder #import <UIKit/UIKit.h> @interface ViewController : UIViewController @property (weak, nonatomic) IBOutlet UITextField *txtUserName; @pro

C++学习笔记:不用sizeof判断int类型占用几个字节

#include <stdio.h> #include <string.h> char *change(int val, int base, char *retbuf) { static const char *str = "0123456789ABCDEF"; char *p; char buf[15]; p = buf+14; *p = 0; do { *--p = str[val % base]; } while( val /= base ); strcp

Dubbo -- 系统学习 笔记 -- 示例 -- 泛化引用

Dubbo -- 系统学习 笔记 -- 目录 示例 想完整的运行起来,请参见:快速启动,这里只列出各种场景的配置方式 泛化引用 泛接口调用方式主要用于客户端没有API接口及模型类元的情况,参数及返回值中的所有POJO均用Map表示,通常用于框架集成,比如:实现一个通用的服务测试框架,可通过GenericService调用所有服务实现. <dubbo:reference id="barService" interface="com.foo.BarService"