为什么在条件赋值中Ruby的展开语法不能用于数组转换?

6
尽管星号(*)结构通常被称为星号操作符,但很明显,与否定(!)操作符等其他一元操作符相比,它是一种不同的存在。
星号在赋值(=)时可以正常使用(即未包含在括号中),但在条件赋值(||=)中使用时会产生错误。例如:
a = *(1..3)
#=> [1, 2, 3]

b ||= *(1..3)
SyntaxError: (irb):65: syntax error, unexpected *

我不是在寻找做同一件事情的替代方法,而是在寻找一个对Ruby内部机制有更好理解的人来解释为什么第一个案例中使用扩展符构造可以工作,而第二个案例中却不能。


1
我猜这只是 Ruby 解析器的一个小缺陷。 - Aleksei Matiushkin
1
我最好的猜测是, a = *(1..3) 实际上被解析为 a =* (1..3) ,因此 =* 有点像一个运算符,而 ||=* 则会被处理成一个不同的不存在的运算符。 - amoebe
@amoebe 确实!显式的 a =* a || (1..3) 可以工作。 - Petr Skocik
1
关于splat的有趣之处是,*a = (1..3) 将会改变 a 的值为 [1,2,3] 即使它在左边。然而 *a ||= 1..3 仍然不起作用。 - fylooi
@PSkocik 但请记住,当变量未定义时,||=确实可以避免名称错误和其他有趣的事情。其他有趣的事情 - amoebe
实现似乎介于https://github.com/ruby/ruby/blob/trunk/parse.y和https://github.com/ruby/ruby/blob/trunk/node.c之间。如果有人能解释一下逻辑,那就太棒了。 - fylooi
2个回答

8
以下是我对于splat实际目标的理解。这适用于Ruby 2.2 MRI/KRI/YARV。
Ruby的splat可以在赋值过程中将一个对象解构为数组。
当a为falsey时,以下示例都提供了相同的结果:
a = *(1..3)
a = * (1..3)
a =* (1..3)
a = *1..3
a = * 1..3
a = * a || (1..3)
a = * [1, 2, 3]
=> [1, 2, 3]
在赋值过程中进行解构,就好像你写了这样的代码:
a = [1, 2, 3]

(注意:展开操作符调用了#to_a。这意味着当您展开一个数组时,它不会发生变化。这也意味着如果您希望,可以为自己的任何类定义自己的解构方式。)
但是以下语句会失败:
*(1..3)
* 1..3
* [1,2,3]
false || *(1..3)
x = x ? x : *(1..3) 
=> SyntaxError

这些语句失败是因为在展开发生时没有进行任何赋值操作。

您的问题就是这个特殊情况:

b ||= *(1..3)

Ruby将其扩展为:
b = b || *(1..3)

这个语句无法通过,因为恰好在splat(展开)发生时没有任何赋值操作。
如果您需要在自己的代码中解决这个问题,可以使用一个临时变量,例如:
b ||= (x=*(1..3))

值得一提的是:当展开运算符出现在表达式左侧时,它有完全不同的用法。这种展开运算符在并行赋值中作为低优先级的贪婪收集器使用。
例子:
*a, b = [1, 2, 3]  #=> a is [1, 2], b is 3
a, *b = [1, 2, 3]  #=> a is 1, b is [2, 3]

所以这个可以解析:

*a = (1..3)  #=> a is (1..3)

它将a设置为右侧结果的所有内容,即范围。

在极少数情况下,星号可以被理解为解构器或收集器,这时解构器具有优先权。

此行:

x = * y = (1..3)

评估结果为:
x = *(y = (1..3)) 

不要这样写:

x = (*y = (1..3)) 

非常棒!回答太赞了。 - digitalextremist
@fylooi 我为你添加了更多细节。 - joelparkerhenderson
@joelparkerhenderson:那么,我理解得正确,splat的这种行为与赋值运算符紧密相关(可能是在语言的更低层次上)? - Drenmi
刚想起另一个不太能解释的 splat 用法 - [*input] - 如果 input 是一个数组,返回 input,否则返回 [input]。有趣的是,这个用法不需要赋值操作。 - fylooi
1
@fylooi 关于不带数组分配的splat,它仅仅调用#to_a方法,而Array定义了该方法返回self。因此,在不进行分配的情况下对数组进行splat操作并没有任何作用。(另请参见Evgney的评论,关于带有分配的splat,例如隐式分配) - joelparkerhenderson
显示剩余5条评论

0
通过在表达式中使用*(1..3)进行展开,您可以获得1, 2, 3,当它被赋值时,它的行为类似于批量赋值,但是Ruby似乎不支持条件赋值。即a=1,2,3只是Ruby的语法糖。请显式地使用一个数组:
a ||= [*1..3] #=> [1, 2, 3]

实际上,这里你只使用了splat功能的一部分 - 它是自动转换为数组 :) 所以你可以简单地这样做:

a ||= (1..3).to_a #=> [1, 2, 3]

我认为他们没有添加大量条件赋值来保持代码易于理解。如果你写 a,b ||= 1,2,不清楚 Ruby 是否应该只分配一个参数,如果另一个参数不是 nil,还是保持不变…… - Eugene Petrov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接