14 工作空间和变量赋值

14.1 工作空间

R把在命令行定义的变量都保存到工作空间中, 在退出R时可以选择是否保存工作空间。 这也是R与其他如C、Java这样的语言的区别之一。

ls()命令可以查看工作空间中的内容。

随着多次在命令行使用R, 工作空间的变量越来越多, 使得重名的可能性越来越大, 而且工作空间中变量太多也让大家不容易查看其内容。 在命令行定义的变量称为“全局变量”, 在编程实际中, 全局变量是需要慎用的。

可以用rm()函数删除工作空间中的变量,格式如

要避免工作空间杂乱, 最好的办法还是所有的运算都写到自定义函数中。 自定义函数中定义的变量都是临时的, 不会保存到工作空间中。 这样,仅需要时才把变量值在命令行定义, 这样的变量一般是读入的数据或自定义的函数 (自定义函数也保存在工作空间中)。

可以定义如下的sandbox()函数:

运行sandbox()函数,将出现如下的browser命令行:

沙盘:接连的空行回车可以退出。
Called from: sandbox()
Browse[1]> 

提示符变成了“Browser[n]”,其中n代表层次序号。 在这样的browser命令行中随意定义变量, 定义的变量不会保存到工作空间中。 用“Q”命令可以退出这个沙盘环境, 接连回车也可以退出。

14.2 非法变量名

R的变量名要求由字母、数字、下划线、小数点组成, 开头不能是数字、下划线、小数点, 中间不能使用空格、减号、井号等特殊符号, 变量名不能与ifNA等保留字相同。

有时为了与其它App系统兼容, 需要使用不符合规则的变量名, 这只要将变量名两边用反向单撇号“`”保护, 如:

## [1] 199

如果变量名(元素名、列名等)是以字符串形式使用, 就不需要用“`”保护。如:

## score a score b 
##      85      66

14.3 变量赋值与绑定

本小节内容技术上比较复杂, 初学者可以略过。

在R中赋值本质上是把一个存储的对象与一个变量名“绑定”(bind)在一起, 比如:

并不是像C++、JAVA等语言那样, x代表某个存储位置, “x <- c(1,2,3)”代表将1到3这些值存储到x所指向的存储位置。 实际上,<-右边的c(1,2,3)是一个表达式, 其结果为一个R对象(object), 而x只是一个变量名, 并没有固定的类型、固定的存储位置, 赋值的结果是将x绑定到值为(1,2,3)的R对象上。 R对象有值,但不必有对应的变量名; 变量名必须经过绑定才有对应的值和存储位置。

这样,同一个R对象可以被两个或多个变量名绑定。 对于基本的数据类型如数值型向量, 两个指向相同对象的变量当一个变量被修改时自动制作副本。 tracemem(x)可以显示变量名x绑定的地址并在其被制作副本时显示地址变化。 如:

## <0000000018288290>
## <0000000018288290>
## tracemem[0x0000000018288290 -> 0x00000000183c5190]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous> do.call eval eval eval eval eval.parent local
## [1] 1 2 3
## [1] 1 2 0

可见y <- x并没有制作副本, 但是修改y[3]值时就对y制作了副本。

如果某个变量名所指向的对象没有被其他变量名绑定, 则修改其元素值并不需要制作副本,如:

## <00000000188C09D0>
## tracemem[0x00000000188c09d0 -> 0x000000001895b270]: eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous> do.call eval eval eval eval eval.parent local
## [1] 1 2 0

在调用函数时, 如果函数内部不修改自变量的元素值, 输入的自变量并不制作副本, 而是直接被函数使用实参绑定的对象。 如:

## <0000000018DA05A0>
## <0000000018DA05A0>

从上面的例子可以看出, 函数fx为实参, 但不修改x的元素, 不会生成x的副本, 返回的值是x指向的对象本身, 再次赋值给z, 也不制作副本, zx绑定到同一对象。

如果函数内部修改自变量的元素值, 则输入的自变量也会制作副本。 如:

## <000000001931EE70>
## tracemem[0x000000001931ee70 -> 0x00000000193d8880]: f2 eval eval withVisible withCallingHandlers handle timing_fn evaluate_call <Anonymous> evaluate in_dir block_exec call_block process_group.block process_group withCallingHandlers process_file <Anonymous> <Anonymous> do.call eval eval eval eval eval.parent local
## <00000000193D8880>

从程序输出看, 函数f2()x为实参, 并修改x的内部元素, 就制作了x的副本, 返回的结果赋给变量z, 绑定的是修改后的副本。

如果在函数中对自变量重新赋值, 这实际是重新绑定, 也不会制作输入的实参的副本。

如果修改y的元素值时还修改了其存储类型, 比如整型改为浮点型, 则会先制作y的副本, 然后制作类型改变后的副本, 然后再修改其中的元素值。

在当前的R语言中, 一个对象的引用(如绑定的变量名)个数, 只区分0个、1个或多个这三种情况。 在没有引用时, R的垃圾收集器会定期自动清除这些对象。 rm(x)只是删除绑定, 并不会马上清除x绑定的对象。 如果已经有多个引用, 即使是只有2个, 减少一个引用也还是“多个”状态, 不会变成1个。

垃圾收集器是在R程序要求分配新的对象空间时自动运行的, R函数gc()可以要求马上运行垃圾收集器, 并返回当前程序用道的存储量; lobstr包的mem_used()函数则报告当前会话内存字节数。

在上面的示例中, 用了基本类型的向量讲解是否制作副本。 考虑其它类型的复制。

如果x是一个有5个元素的列表, 则y <- x使得yx指向同一个列表对象。 但是, 列表对象的每个元素实际上也相当于一个绑定, 每个元素指向一个元素值对象。 所以如果修改yy[[3]] <- 0, 这时列表y首先被制作了副本, 但是每个元素指向的元素值对象不变, 仍与x的各个元素指向的对象相同; 然后, y[[3]]指向的元素值进行了重新绑定, 不再指向x[[3]], 而是指向新的保存了值0的对象, 但y的其它元素指向的对象仍与x公用。 列表的这种复制方法称为浅拷贝, 表格对象及各个元素绑定被复制, 但各个元素指向(保存)的对象不变。 这种做法节省空间也节省运行时间。 在R的3.1.0之前则用的深拷贝方法, 即复制列表时连各个元素保存的值也制作副本。

如果x是一个数据框, 这类似于一个列表, 每个变量相当于一个列表元素, 数据框的每一列实际绑定到一个对象上。 如果y <- x, 则修改y的某一列会对y进行浅拷贝, 然后仅该列被制作了副本并被修改, 其它未修改的列仍与x共用值对象。

但是如果修改数据框y的一行, 因为这涉及到所有列, 所以整个数据框的所有列都会制作副本。

对于字符型向量, 实际上R程序的所有字符型常量都会建立一个全局字符串池, 这样有许多重复值时可以节省空间。

用lobstr包的obj_size()函数可以求变量的存储大小, 如obj_size(x), 也可以求若干个变量的总大小, 如obj_size(x,y)。 因为各种绑定到同一对象的可能性, 所以变量的存储大小可能会比想象要少, 比如, 共用若干列的两个数据框, 字符型向量, 等等。 基本RApp的object.size()则不去检查是否有共享对象, 所以对列表等变量的存储大小估计可能会偏高。

从R 3.5.0版开始,1:n这种对象仅保存其开始值和结束值。

在自定义函数时, 自变量通常是按引用使用的, 函数内部仅使用自变量的值而不对其进行修改时不会制作副本, 但是如果函数内部修改了自变量的值, 就会制作副本, 当自变量的存储很大而且返回调用这个函数时就会造成运行速度缓慢。 在函数内应慎重修改自变量的值。

在循环中修改数据框的列, 也会造成反复的复制。

14.4 环境

前面讲的工作空间就是一个环境, 称为“全局环境”。 环境可以认为是包含了变量绑定和函数定义的一个对象, 对环境的修改都不会制作副本。 一般大家只需要用到全局环境, 在自定义函数的运行过程中会有一个局部的运行环境, 但是在函数内定义的嵌套函数可以带有其定义处的环境, 这样可以实现有历史记忆的函数, 类似于其它面向对象程序设计语言如C++等, 对象可以同时有状态(变量成员)和适用操作(方法成员)。

环境的应用将在“函数进阶”部分讲到内嵌函数、函数工厂时详细讲解。