[译]Ruby中的解构赋值

原文:

http://po-ru.com/diary/rubys-magic-underscore/

http://po-ru.com/diary/destructuring-assignment-in-ruby/


下划线的妙用

我今天发现,在把下划线作为变量名时,Ruby在对待上会和其他名称稍有不同.

为了方便下面的讨论,我们假定存在一个这样的哈希值变量:

people = {
  "Alice" => ["green", "alice@example.com"],
  "Bob"   => ["brown", "bob@example.com"]
}

我们想要忽略掉眼睛的颜色,把每个键值对转换成一个只包含名字和邮箱的数组.我们可能会这样做:

people.map { |name, fields| [name, fields.last] }

但是这样写的话,fields.last是什么,不是很清楚.Ruby中有一个强大的语法叫做解构赋值,我们可以使用一个小括号进入到结构的内部,来让赋值语句更清晰:

people.map { |name, (eye_color, email)| [name, email] }

这样写是好多了,但是没有使用过的变量eye_color仍然是个干扰.它并没有帮助我们理解代码的意图.

在不少语言中(Haskell, Go, Clojure, 还有很多很多), 通常都是使用一个下划线来表示一个未使用的值.这也是被Ruby的风格指南所推荐的: 1, 2.

这样的话,我们的代码可以改写为:

people.map { |name, (_, email)| [name, email] }

这次很满意了.但如果我们想忽略多个元素呢?

people = {
  "Alice" => ["green", 34, "alice@example.com"],
  "Bob"   => ["brown", 27, "bob@example.com"]
}

没问题.重复使用下划线即可:

people.map { |name, (_, _, email)| [name, email] }

但你不能使用其他变量名来代替,在Ruby 1.9中.只能使用下划线 _:

people.map { |name, (x, x, email)| [name, email] }
# SyntaxError: (eval):2: duplicated argument name

原因可以在Ruby的解析器中找到(shadowing_lvar_gen),当且仅当变量名为一个下划线时,所有变量名称是否重复的检查都会跳过.

在Ruby 1.8中, 多个下划线变量名也可以使用,但并不是上面的原因:而是Ruby 1.8根本不检查块参数中重复的参数名.

同时,无论是1.8还是1.9,都允许在解构赋值语句中使用重复变量名:

a, a, b = 1, 2, 3
a, a, b = [1, 2, 3]

这两种写法都不会报错(虽然我并不推荐这样做).

解构赋值

正如前面所提到的,Ruby支持解构赋值.你可以把一个数组的值同时分配给同个变量:

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

还可以在块参数中使用:

[[1, 2, 3]].map { |a, b, c| "..." }

多层解构

你可以用小括号括住等号左边适当的多个变量来对应右侧数组的结构:

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

a # => 1
b # => 2
c # => [3, 4]

你还可以继续向下层延伸:

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

a # => 1
b # => 2
c # => 3
d # => 4

splat运算符:赋值为多个元素

等号右侧多余的值通常会被丢弃:

a, b, c = [1, 2, 3, 4, 5]

a # => 1
b # => 2
c # => 3

如果在变量前面加上一个星号(splat运算符),这个变量就会收集起所有未分配的元素:

a, b, *c = [1, 2, 3, 4, 5]

a # => 1
b # => 2
c # => [3, 4, 5]

还可以放在数组开头:

*a, b, c = [1, 2, 3, 4, 5]

a # => [1, 2, 3]
b # => 4
c # => 5

在中间也可以:

a, *b, c = [1, 2, 3, 4, 5]

a # => 1
b # => [2, 3, 4]
c # => 5

但你只能使用它一次:

a, *b, *c = [1, 2, 3, 4, 5]

# stdin:81: syntax error, unexpected tSTAR
# a, *b, *c = [1, 2, 3, 4, 5]
#         ^

更准确点说,是每一层结构上只能使用一次splat运算符:

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

a # => [1, 2]
b # => 3
c # => [4, 5]

忽略元素

你可以重复的使用下划线,用来表示那些你不关心的元素:

a, _, b, _, c = [1, 2, 3, 4, 5]

a # => 1
b # => 3
c # => 5

译者注:Python中类似的语法:

>>> (_, a) = (1, 2)
>>> a
2

perl中类似的语法:

(undef, $a) = (1, 2);
print $a;
2

如果想要忽略那些连续的多个元素,使用一个星号:

a, *, b = [1, 2, 3, 4, 5]

a # => 1
b # => 5

你可以在每一层结构中使用一个星号,但同一层中不能使用多个:

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

a # => 1
b # => 6
c # => 7

我并不是推荐你到处去使用解构赋值,但的确有需要用到它的地方.如果能让你的代码更加清晰,那就用它,需要注意的是:如果你需要保持与Ruby 1.8的兼容性,上面的大部分代码都不能正常工作.

译者注:JavaScript中的解构语法

早在2006年,当时ES4规范正在制定中,mozilla的SpiderMonkeyRhino提前实现了解构赋值的语法,对应的JavaScript版本是JavaScript1.7,对应的Firefox版本是Firefox2.0,同时引入的语法还有数组推导式,let表达式,迭代器和生成器.如今,ES4中的这些标准也都被写入了ES6规范的草案中,这些原本只能在Firefox扩展开发中使用的增强特性,也可以使用在web开发中了.下面的例子都可以在Firefox中试验.

交换两个变量的值:

[a, b] = [b, a]

接受返回的多个值:

function f() { return [1, 2] }
var a, b;
[a, b] = f();

接受返回的多个值,部分值被忽略:

你可能会想,用下划线行不行,

function f() { return [1, 2, 3] }
var [a, _, b] = f();

行是行.但是JavaScript中的下划线没有一点特殊性.和$一样就是个普通的变量.假如你用到了Underscore.js,下划线已经被占用了.undefined呢?如果在ES5的引擎中,undefined不能被修改,不会有什么问题,但老点的浏览器就不行了.

这样也是没问题的,

function f() { return [1, 2, 3] }
var [a, b, b] = f();

幸运的是,JavaScript给了我们更方便的写法,直接留空.

function f() { return [1, 2, 3] }
var [a, , b] = f();

多层解构也是可以的:

[a,,[b,,[c]]] = [1,2,[3,4,[5]]]

除了数组解构,还有对象解构:

var {key:a,value:b} = {key:1,value:23}

以及多层对象解构:

var {key:a,value:{foo:b}} = {key:1,value:{foo:2}}