赋值:值还是引用?

date
Feb 7, 2024
tags
slug
assignment-value-or-reference
status
Published
summary
探讨了几种编程语言对于赋值的行为。
type
Post
前段时间学习 Rust,对于赋值这方面自己突然疑惑起来,到底传递的是值,还是传递的是引用,还是其他的什么?由此才发现自己虽然学过几门编程语言,但并没有去思考那种虽然暂时不会影响你写代码但是日后必定会狠狠绊你一脚的问题
所以在此总结各个编程语言中是如何处理赋值的,以便提醒自己日后学习编程语言时去更进一步思考。

Java

初学 Java 时,如果有 C++ 经验可能认为“对于 Java 内置原始类型是传值,对于对象 object 是传引用”。
但在 《Java 核心技术 I》中指出,Java 只有一种传递方式,按值传递。如果是传引用的话无法解释为什么下面的 swap 函数无法正常工作:
然后再加上上面 test 的调用更让人感到困惑。
我们知道所有数据在计算机中以二进制来存储,当 java 在创建原始类型时,比如 int,会分配 32 个连续二进制位,用以存储 int 型数值,然后修改内部表格将变量名指向分配地址位的第一位,而 Java 对于对象,64 位电脑会固定分配 64 位用以存储对象的首地址(java 会根据对象各个字段加上 Java 对于对象的开销分配二进制位数,比如分配了 96 位,假设地址从 1000 ~ 1096 ,那么首地址就是 1000),所以根据下图,p 旁边的方框延伸箭头指向真正的对象,因为方框存储的是地址,就像指针一样。
notion image
而 java 在处理像 left = right 时,由于是传值,会将 right 的 bits 全复制到 left 里(如果是对象的话就是复制地址)。
那么再来看上面的 swap ,a 存储的是 p 的地址,而 b 存储的是 q 的地址,而交换赋值只影响 a 和 b ,并不会对原始 p, q 造成影响
 

C

明白了 Java ,再来看 C ,C 非常简单,和 Java 一样都是 call by value,是指针就复制指针的值也就是指向的地址,可以用来模拟 call by reference。

Python

这里我们以 CPython 为例,首先需要注意的是所有的 Python 数据类型都是 PyObject (C 底层表示结构)类型的扩展,比如 PyVarObject 就是 PyObject 添加了 ob_size 字段。所以其实不管在 Python 里面是什么类型,都是‘对象’。
float 最简单的 Python 对象为例,有一个字段用以表示值,额外两个元数据字段:
  • ob_refcnt 表示对象引用数量(根据引用计数垃圾清理
  • ob_type 表示指向对象类型的指针
  • ob_val 存储值,是 C double 类型
那么,如何理解 Python 中的变量赋值行为呢,这里采用 《FluentPython》 中的比喻,变量作为命名标签附在对像上,可随时通过标签指代对象。(其实还是复制的是地址,和 Java 一样)
 
notion image
而增加一个标签,也就增加了引用计数。这个设计在使用 Python 中的可变类型比如 list 也极容易产生 bug,同时处理复制时也要尤其注意是深复制还是浅复制。
通过标签的方式来理解,那么下面的行为都说得通了:
 
 

参考:
 

© Aron Yang 2024