回到Ruby系列文章


Ruby变量

关于Ruby变量的一些基本特性

  • Ruby中的变量的类型:局部变量、全局变量、实例变量、类变量。当然,还有方法的参数、代码块的参数也认为是变量
    • 显然,参数是局部限定在方法内部的,即局部变量
    • 但代码块中的参数变量则不一定:如果执行代码块的时候,其内所引用的变量还不存在,则临时创建一个局部于该代码块的变量,如果引用的变量已经存在,则继承变量的作用域属性
  • *变量命名惯例 *
    • 局部变量、方法参数、方法的名称都使用小写字母开头
      • 有时候变量、方法以下划线开头_name也是局部的意思,它表示这是私有的东西,不应该暴露给外界
    • 全局变量以$开头,例如$var
    • 实例变量以@开头,例如@name
    • 类变量以@@开头,例如@@class_var
    • 类名称、模块名称、常量名称都以大写字母开头
    • 方法名称可以以?、!、=字符结尾,例如equals?
      • ?字符结尾的方法,表示返回的是一个布尔值,用于测试true/false
      • !字符结尾的方法,表示警告提醒,这类方法一般表示原处修改(destructive)对象,要小心使用。一般都会提供成对的带有!结尾和不带!结尾(non-destructive)的方法供选择。例如uniq()uniq!(),前者修改的是拷贝后的对象,后者在原有对象上修改
      • =结尾的方法表示赋值行为,例如有一个方法名为test=(),那么test=(6)等价于test = 6。正如数组元素赋值arr[1] = 3,实际上是调用了[]=方法,等价于arr[1]=(3)arr[1]= 3 。所以,对于面向对象来说,它表现的是setter类方法
  • 变量/表达式在字符串中的内插方式是使用#开头。在Ruby中,#前缀可以看作是一种对某对象的引用、调用之义。例如:
    • 内插全局变量#$var
    • 内插实例变量#@var
    • 内插类变量#@@var
    • 但是对于普通的不带前缀符号的局部变量或表达式,为了避免歧义,通常在#后加上{}

Ruby变量的创建和赋值时间点

下面的语句会创建一个局部变量a(前提是之前并不存在这个a变量),它将指向内存中数值对象3(即保存了数值对象3的地址)。

1
a=3

1
2
>> a.nil?   #=> 引用未赋值过的局部变量,将抛出变量未定义错误
>> $a.nil? #=> true,引用未赋值过的全局变量,将默认初始化为nil

此外,赋值操作并非是执行了赋值语句才赋值,Ruby中的赋值行为是在解析器遇到赋值语句时就赋值的。例如,在一个条件判断语句中做赋值操作,如果条件判断为假,则也会赋值,只不过这时候赋值的是初始值nil:

1
2
>> x=3 if false  # 条件false,但也创建了变量赋值为nil
>> p x #=> nil

变量的赋值语句

简单的,直接一个等号赋值:

1
a=3

还可以多变量同时赋值:

1
a, b = 1, 2

这在Ruby中实际上是隐式创建了数组[1,2],就像Python中逗号分隔的元素将隐式创建一个tuple一样。

还可以:

1
a, (b, c) = 1, [2, 3]

不仅如此,Ruby中也能进行数组打包、解包。实际上,上面的多变量同时赋值语句就是一个数组打包、解包的示例。

1
a=1, 2     # 隐式创建(打包)了一个数组对象

更多关于打包、解包相关的内容,参见:splat操作符:参数打包解包

『丢弃』不要的赋值

可以直接使用一个下划线来『丢弃』值。例如:

1
2
a,_=3,4
_=5

其实并没有真的丢弃,它是将值赋值给了一个特殊的变量名”_”而已。但这是作为一种特殊符号来暗示这个值是不使用的。

splat操作符:参数打包解包

Ruby中可以使用一个星号*和两个星号**完成一些打包、解包操作,它们称为splat操作符:

  • 一个星号:以数组为依据进行打包解包(参考文章)
  • 两个星号:以hash为依据进行打包解包(参考文章)

两个星号的splat场景很少见,如有必要可参考上面列出的参考文章。

当splat操作符后面跟的是数组,则进行数组解包操作:解包成元素列表。这个解包效果在调用函数并传参时比较能体现出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
def f(a,b,c,d)
p "1.#{a}"
p "2.#{b}"
p "3.#{c}"
p "4.#{d}"
end

arr=%w(aa bb cc dd)
f(*arr)

# 赋值解包:将数组解包成元素列表再赋值给arr1变量
# 这过程中会创建一个新的数组保存解包后但要赋值的各元素
arr1=*[1,2,3] #=> arr1=[1,2,3]

上面的*arr中,splat操作符后面跟的是一个数组,所以它做了解包操作,将数组解包成了4个元素,并依次赋值给参数a、b、c、d。

当splat操作符后面跟的是一个或多个元素,则进行数组打包操作:创建一个新数组保存这些元素。这个在函数定义中比较常见,很偶尔的在赋值的时候也能见到。

1
2
3
4
5
6
7
def foo(a,b,*args)
p a
p b
p args #=> 打包成[3,4,5]赋值给参数args
end

foo(1,2,3,4,5)

下面的打包、解包示例比较经典:

1
2
3
4
a,*x=1,2,3    #=> a=1,x=[2,3]

a, (b, *c), *d = 1, [2, 3, 4], 5, 6
#=> a=1,b=2,c=[3, 4],d=[5, 6]

按引用赋值

Ruby中赋值和函数的参数传递是按引用赋值的

在Ruby中,变量仅仅只是一个名称,它保存的是对象的地址,可认为变量是一个指针,指向赋值给它的数据对象,或者说是引用那个对象。

例如:

1
a=3

表示创建一个数值对象3,并将这个对象的地址保存在变量a中,通过a能找到这个数值对象3。

于是,将一个变量赋值给另一个变量,实际上是拷贝了地址。

1
2
3
4
a="hello"    #=> "hello"
b=a #=> "hello"
a.__id__ #=> 70368647078020
b.__id__ #=> 70368647078020

所以,如果变量指向的是一个容器类型,那么这样赋值后,修改一个变量会影响另一个变量的值。

1
2
3
4
a="hello"    #=> "hello"
b=a #=> "hello"
a[0]="x" #=> "x"
b #=> "xello"

但因为有些数据类型是不可变类型,即使赋值是按引用赋值使得双方都指向同一个对象,但这时修改一方因为无法修改这个对象,只能创建一个新对象并指向这个新对象,于是两者现在指向不同对象。这就像是”按值传递”而非”按引用传递”的假象

1
2
3
4
5
6
7
8
9
a=200   # 数值是不可变对象
b=a # 两者指向同一个数值对象
a=20 # 数值不可变,所以只能创建新数值对象20,并让a指向它
# 于是a指向20,但b仍然指向200

a="hello" # 字符串是可变对象
b=a # 两者指向同一个字符串对象
a[0]="x" # 同时也会修改b的值,因为字符串可变,不会创建新对象
# 但字符是不可变对象,所以只能新创建字符对象x

如果不想要这种按引用赋值的效果,那么对于某些类型的对象,可以使用它们特殊的方法来生成内容完全一致的新对象。也可以使用所有对象都通用的dup()或clone()方法直接拷贝对象。

例如,对于字符串,可以直接通过字符串已有的方法返回一个内容一致的字符串新对象。

1
2
3
4
a="hello"
b=a[0..-1]
a.__id__ #=> 70368645629620
b.__id__ #=> 70368645712680

使用dup()或clone(),dup()只是简单拷贝对象,clone()是完全拷贝一个对象,包括对象的各种状态。不管如何,dup和clone都能拷贝基本内容完全相同的对象。

1
2
3
4
5
6
7
a="hello"
b=a.dup
c=a.clone

a.__id__ #=> 70368646490760
b.__id__ #=> 70368646550620
c.__id__ #=> 70368646546940