`
rubynroll
  • 浏览: 204567 次
  • 性别: Icon_minigender_1
  • 来自: Wgt
社区版块
存档分类
最新评论

Ruby'陷阱'之: '||=' 的真正展开式

阅读更多
前一段时间,我在这里http://rubynroll.iteye.com/blog/192547展示了一个空格带来的'陷阱', 今天又见到另一个'陷阱'(http://dablog.rubypal.com/2008/3/25/a-short-circuit-edge-case by David).

之所以为陷阱加引号, 是因为大部分情况下我们都没有机会掉进去

大多数Ruby教科书在解释 "a ||= b" 这个复合操作时,都说她等效于: "a = a || b", 实际上真的如此么?

让我们在irb里面来看看:

h = Hash.new(1)  # 生成一个新的Hash,缺省值为1
=> {}
h[:x]
=> 1    # h[:x]未定义,所以返回1
h[:x] ||= 2
=> 1
h
=> {}   # 啊? '陷阱'?
h[:x] = h[:x] || 2
=> 1
h
=> {:x=>1}  # 啊!


David提到,Matz在RubyConf 2007上的解释是"a ||= b"真实的等价应该是: "a or a = b", 但是David后来认为"a || a = b"应该更恰当些.

果真的如此么? 不!

一个简单的例子可以反驳, 如果a未定义,则:

irb(main):001:0> a || a = 2
NameError: undefined local variable or method `a' for main:Object
        from (irb):1
        from :0


所以, 看来"a ||= b"正确的展开应该是: "a = b unless a"

对了么? 还是不对!

"a ||= b"表达式最终返回的是a, 而"a = b unless a"在a非真时返回nil.

所以, "a ||= b"的正确展开式应该为: "if a then a else a = b end"


想不到吧?

=== 补充 ===

鉴于此帖较长,且中间夹杂着不少因我的疏忽和错误造成的无谓争论,因此把最终的到的结论列在这里,免得看起来太累,或者有些人没有耐心看完而增加更多的无谓的争论。

结论如下:
1) a ||= b 不等效于: a = a || b
2) 归纳起来,准确的展开式应该是:
if defined?(a) then (a || a = b) else a = b end

特别感谢lllyq, 这个结论的credit应归于他/她。

若大家发现有更好的展开形式,欢迎讨论。
分享到:
评论
31 楼 liusong1111 2008-07-21  
终于能打汉字了,这可恶的ubuntu,只怪自己不熟吧。
关于if/else是否有and/or组装的等价式,答案是肯定的。
记得ibm developerworks有篇不错的文章,忘了是不是这个:
可爱的 Python: Python 中的函数编程
原文排版有点问题,这个盗链居然正常。

哈哈,原来rubynroll已经把lllyq的回贴合并到了主贴。
等价式还可以进一步简化为:
defined?(a) && a || a = b 


defined?是个关键字,其神奇效果类似javascript的typeof:
http://redhanded.hobix.com/inspect/methodCheckDefined.html
记得贴过一次这个链接,呵呵~



30 楼 rubynroll 2008-07-21  
to blackanger:

请看清楚标题,以及仔细阅读所有帖子再发表意见吧,否则就会和我犯下同样的错误。

这里讨论的是a ||= b的generic展开式,这个展开式不应该因a,b为特殊情况而有所区别,即语义的严格定义。

我想此贴的结论已经很明确了,如果对归纳的最终展开式还有什么异议,欢迎发表高见。

另外,对别人的知识妄下论断不会对让你显得更权威有什么帮助;同样,嵌入签名图片里的“资深会员”字样也不会。
29 楼 liusong1111 2008-07-21  
blackanger 写道

真不知道你说了写什么,把Hash搞明白再来吧。

before catching what they are talking about, why did you demand others " 把Hash搞明白再来吧"?
plz read CAREFULLY before speak like this.

28 楼 blackanger 2008-07-21  
>> h = Hash.new(1)
=> {}
>> h[:x]
=> 1
>> h[:x] ||= 2
=> 1
>> h
=> {}

陷进 ?
h[:x] 是未定义的,
a ||= b 相当于  a = b ||  a
这里因为你给Hash加了默认值,所以当然会输出1了。 看看下面的代码:
>> h = Hash.new
=> {}
>> h[:x] ||= 2
=> 2


>> h[:x] = h[:x] || 2
=> 1
>> h
=> {:x=>1}
>>
你上面这相当于给hash赋值了key-value对。


真不知道你说了写什么,把Hash搞明白再来吧。
27 楼 entaoyang 2008-07-21  
所以, "a ||= b"的正确展开式应该为: "if a then a else a = b end"


想不到吧?

楼主还是学学基本知识吧, 这是想当然的东西, 还想不到?
26 楼 liusong1111 2008-07-10  
这Spec简单了点,对讨论的问题没有帮助:
http://github.com/rubyspec/rubyspec/tree/master/1.8/language/or_spec.rb
25 楼 whygod 2008-07-10  
说实话,看完觉得有点没事找事来说的感觉。
参考一些开源的rails代码,经常可以看到
@page = param[:page]
@page ||= 1
这样的代码,就是处理分页参数的。如果没有传page,就把@page设为1.
本身意思就很明瞭。
不过也有收获,代码还是写得简单清晰一点好。楼主分析辛苦了。
24 楼 geszJava 2008-07-07  
晕。
百度谷歌上都可以找到专门讲述ruby陷阱的资料。
ruby自己的文档也有专门讲述trap的部分。不是我杜撰啊。

从来没有看到单独使用ruby的项目,可能是我孤陋寡闻,除去开源库,我真的没发现。可能ruby的优势不在此,ror无疑是ruby最擅长的地方。
但随着groovy,grails发展和兴起,ruby或者ror的优势已经淡化。

毕竟c族群语言在当代具有先天性的亲和力。

ruby是否有前途?不知道,但是复杂而不严谨的语言驾驭起来有难度,使用起来有顾忌,团队整合上有困难。语言不是越复杂越好,而是越简单越好。

曲高和寡,先人早有断言。ruby是文言,而大众只需要大白话。用ruby无疑可以写出锦绣文章,但大部分人却看不懂也改不动。与其做老学究,不如现实一些。
23 楼 rubynroll 2008-07-04  
我之所以在这里“咬文嚼字”,是为了追求对语言的精确理解。

对于象ruby这种特性丰富的语言,没有象JavaScript那样有一个定义严谨的规范来作参考,只能靠“咬文嚼字”了。

说“离开ror,ruby啥都不是“这个严重不同意. 单独来看ruby本身就是一门非常优秀的脚本语言,就“陷阱”而言并不比C++多,语言的表达能力并不在Perl之下而可读性非常之高,唯一缺憾是速度较慢,但对于脚本语言来说这个问题并不显著。

我开始用ruby的时候,根本就不知道什么是rails.
22 楼 seemoon 2008-07-04  
geszJava 写道
回复不见了?
ruby按我说就是不断句的之乎者也,陷阱多多,没多少前途。最终还是死在这个陷阱上。
ruby的兴起完全是因为ror,离开ror,ruby啥都不是。
还是看好groovy,同为动态语言,却严谨的多。不必咬文嚼字玩文字游戏。


希望楼上能把“陷阱多多”一一列举而出,好有益于用ruby干活的劳苦大众。
21 楼 geszJava 2008-07-04  
回复不见了?
ruby按我说就是不断句的之乎者也,陷阱多多,没多少前途。最终还是死在这个陷阱上。
ruby的兴起完全是因为ror,离开ror,ruby啥都不是。
还是看好groovy,同为动态语言,却严谨的多。不必咬文嚼字玩文字游戏。
20 楼 seemoon 2008-06-30  
引用

但是David后来认为"a || a = b"应该更恰当些.


楼主是咬字头咬过头了,在一个极端的情况下(即a没有定义)写了一段代码然后造成了一些混乱,你又怎知david没有个潜台词是a has defined呢?

不过举的那个hash有默认值的||=这个问题是应该提起注意。
19 楼 lllyq 2008-06-30  
:) 没问题,或者
a = defined?(a) ? (a || a = b) : b
18 楼 rubynroll 2008-06-30  
lllyq 写道
rubynroll 写道
lllyq 写道
你可以试试看新开irb,执行
if a then a else a = 2 end
这个效果跟新开irb,执行
a || a = 2 效果是一样的,ruby 版本是1.8.6

效果不一样, 至少在版本是1.8.6. 1.8.7上是不一样的, 后者会raise undefined error.

根据你的回复我估计你还是没去试,显然,无论在1.8.6, 1.8.7,后者都会raise undefined error. 前者你试过了么?试过后你就知道为什么我会回第一个帖了\

我说的更清楚一点就是你测试下来两者行为不一样,那是因为你的测试方法不对,你没有每次都新开irb来测试,所以测试方法不对你看到的结果就不对,得出的结论也就不对了


哈哈我真是搞糊涂了,我试的是a ||= 2和a || a = 2, 比较的太多了,连自己都弄糊涂了, 看来此贴应该归新手贴才对:(


至于结论, 你认为 a ||= b最终展开为 if defined?(a) then (a || a = b) else a = b end 还有问题么?
17 楼 lllyq 2008-06-30  
rubynroll 写道
lllyq 写道
你可以试试看新开irb,执行
if a then a else a = 2 end
这个效果跟新开irb,执行
a || a = 2 效果是一样的,ruby 版本是1.8.6

效果不一样, 至少在版本是1.8.6. 1.8.7上是不一样的, 后者会raise undefined error.

根据你的回复我估计你还是没去试,显然,无论在1.8.6, 1.8.7,后者都会raise undefined error. 前者你试过了么?试过后你就知道为什么我会回第一个帖了\

我说的更清楚一点就是你测试下来两者行为不一样,那是因为你的测试方法不对,你没有每次都新开irb来测试,所以测试方法不对你看到的结果就不对,得出的结论也就不对了
16 楼 rubynroll 2008-06-30  
lllyq 写道
你可以试试看新开irb,执行
if a then a else a = 2 end
这个效果跟新开irb,执行
a || a = 2 效果是一样的,ruby 版本是1.8.6

效果不一样, 至少在版本是1.8.6. 1.8.7上是不一样的, 后者会raise undefined error.

lllyq 写道

非要用一个准确的展开式下面这个应该可以
if defined?(a) then (a || a = b) else a = nil end

这就是我所想要的讨论的,一个准确的展开式.

但是这个式子还是有问题, 应该是这样才对:
if defined?(a) then (a || a = b) else a = b end

所以结论是不是这样:
1) a ||= b 不等效于: a = a || b
2) 在a已经定义过时, a ||= b等效于a || a = b
3) 在a未定义时, a ||= b等效于a = b
所以归纳起来,准确的展开式应该是:
if defined?(a) then (a || a = b) else a = b end


意见??





15 楼 03131302 2008-06-29  
rubynroll 写道
lllyq 写道
这个h[:x]行为有什么特别么?跟预期的行为一致啊
h[:x] ||= 2 并没有对h[:x]赋值,相当于h[:x] || h[:x] = 2,而h[:x] = nil是有一个赋值动作


我举的那个例子就是为了反驳
caryl 写道

同意楼上,感觉应该相当于
a = nil
a || a = b


caryl说的"楼上"就是
lllyq 写道
a || a = b 就是 a || a = b,本来就没考虑defined?的问题,跟 if a then a else a = b end 一样的,如果你执行下来不一样那估计是你连续执行的问题,前一句相当于做了define



另外, h[:x] ||= 2 相当于h[:x] || h[:x] = 2在这里之所以成立,是因为h这个Hash是已经定义过了. 对于一个通用的 a ||= b, 并不能简单地展开成 a || a = b, 因为这里的a如果未定义就会是个例外.

注意, 这里探讨的是"完全等同"的展开式, 因为如果存在特例, 除非语言规范明白地告诉你"这是个特例", 否则'陷阱'就有可能在那里等着你.

14 楼 caryl 2008-06-29  
好,也用代码说事:
先执行一边楼主的代码
h = Hash.new(1)  # 生成一个新的Hash,缺省值为1  
=> {}  
h[:x]  
=> 1    # h[:x]未定义,所以返回1  
h[:x] ||= 2  
=> 1  
h  
=> {}     
h[:x] || h[:x] = 2  
=> 1  
h  
=> {}  #恩,还是{}

可见针对楼主hash的代码,david观点没错。
楼主应该知道,hash的[]是个方法,用个简单的类模拟一下
class A
  def []
    defined?(@a) ? @a : 1
  end

  def []=(a)
    @a = a
  end
end


试一下:
a = A.new
a[]                  # => 1 undefined.
a[] ||= 2          # => 1 undefined.
a[] || a[] = 2    # => 1 还是一样的

#当然如果没有[]方法中 defined? 的保护,直接执行
a || a = 2
#肯定是要有错误的,因为a未定义


当然,我前面说的相当于
a = nil #定义
a || a = b

是不能满足上边A的例子的(同样对楼主hash的例子有问题),但是如果根据上边的理解,david说的 a ||= b 相当于 a || a = b没什么问题。
所以我说支持 lllyq 的观点,可能是 ||= 因为随后就是赋值操作,没有raise undefined错误?这可能真得看了源码才知道了。
13 楼 lllyq 2008-06-28  
Quake Wang 写道
lllyq 写道
你可以试试看新开irb,执行
if a then a else a = 2 end
这个效果跟新开irb,执行
a || a = 2 效果是一样的,ruby 版本是1.8.6

非要用一个准确的展开式下面这个应该可以
if defined?(a) then (a || a = b) else a = nil end

rubynroll的意思是
你新开irb 执行a ||= 2和
新开irb执行a || a = 2的结果是不一样的,所以这2个公式不是等价的

这样说来我前面的公式也不行...


这个我没有否定,我一开始只是说不用搞这么复杂去涉及define的问题,而且他的结论展开式为 if a then a else a = 2 end 是不对的,看我第一个回帖的分析,以及上一个回帖的说明
12 楼 QuakeWang 2008-06-28  
lllyq 写道
你可以试试看新开irb,执行
if a then a else a = 2 end
这个效果跟新开irb,执行
a || a = 2 效果是一样的,ruby 版本是1.8.6

非要用一个准确的展开式下面这个应该可以
if defined?(a) then (a || a = b) else a = nil end

rubynroll的意思是
你新开irb 执行a ||= 2和
新开irb执行a || a = 2的结果是不一样的,所以这2个公式不是等价的

这样说来我前面的公式也不行...
Global site tag (gtag.js) - Google Analytics