由于a ||= 1
等价于a || a = 1
,因此可以说这是同步糖:
if a.nil?
a = 1
end
同样,假设session
是一个类似哈希的对象,如下所示:
def increment_session_counter
session[:counter] ||= 0
session[:counter] += 1
end
相当于:
def increment_session_counter
if session[:counter].nil?
session[:counter] = 0
end
session[:counter] += 1
end
这是否意味着隐式if
语句每次都会在increment_session_counter
的原始定义中执行?由于session[:counter]
很可能只是第一次(即<<1% 的时间)才nil
,我觉得以下代码更好,因为隐式if
不会每次都触发:
def increment_session_counter
session[:counter] += 1
rescue NoMethodError
session[:counter] = 1
end
从这个意义上说,这段代码更好吗?
话虽如此,我不知道Rescue
是如何在 ruby 中实现的,以及如果是这样,它是否真的与可以带来的微小优化相关。
session[:counter] += 1
做三件事:
- 获取值 (
Hash#[]
) - 递增值 (
Integer#+
) - 存储递增的值 (
Hash#[]=
)
这非常方便,但它的简洁性也使其不灵活。
如果将步骤分开,则提供默认值要容易得多:
def increment_session_counter
session[:counter] = session.fetch(:counter, 0) + 1
end
捕获错误是一个非常聪明的想法,但它也比使用||=
更难阅读。但更容易的是,在创建哈希时设置初始值:
@session = {:counter => 0}
def increment_session_counter
@session[:counter] += 1
end
当事先不知道密钥时,这不起作用:
def increment_user_counter(username)
@session[username] ||= 0
@session[username] += 1
end
但是,在这种情况下,您假设计数器的值只有零一次就会陷入危险。事实上,由于许多分布都遵循幂律,因此 1 可能是最常见的计数。
因此,如果您事先知道可能的值,最好在初始化程序或类时将它们设置为零,并跳过默认值检查的需要。如果您事先不知道所有可能的值,您可能会发现经常需要||=
语句。在很少的情况下,假设检查nil
便宜得多,使用异常是最佳解决方案。
我使用以下代码尝试了一些基准测试。这是一个 rails 应用程序。 Rails v.5.1.1/Ruby v2.4.1
最初的目的只是计算某些控制器的任何show
操作中的访问次数,如下所示:
include SessionCount
...
def show
...
@counter = increment_session_counter
...
然后,我可以在相关视图中显示计数器。
因此,我对要测试的默认分配的两个版本的适当代码提出了担忧:
module SessionCount
private
#Counter version A
def increment_session_counter_A
session[:counter] ||= 0
session[:counter] += 1
end
#Counter version B
def increment_session_counter_B
session[:counter] += 1
rescue
session[:counter] = 1
end
end
为了测试两个版本的默认分配,我更改了控制器代码,如下所示:
include SessionCount
...
def show
...
t0 = Time.now
1000000.times do
session[:counter] = 0; #initialization for normalization purpose
increment_session_counter_A
end
t1 = Time.now
puts "Elapsed time: #{((end_time - beginning_time)*1000).round} ms"
@counter = increment_session_counter_A
...
备注:在该代码中,初始化是为了强制执行"快乐路径"(其中值不是 nil)。在实际场景中,对于给定用户,这只会在第一次发生。
以下是结果:
我使用版本 A(||=
运算符)的平均时间为 3100 毫秒。
我在版本 B (rescue
) 中平均得到 2000 毫秒。
但有趣的部分现在开始了。
在前面的代码中,代码按照"快乐路径">执行,其中没有发生异常。
因此,我将为规范化目的所做的初始化更改如下,以强制执行"异常路径">:
1000000.times do
session[:counter] = nil; #initialization for normalization purpose
increment_session_counter_A
end
结果如下:
我使用版本 A(||=
运算符)平均得到 3500 毫秒。
我使用版本 B 的平均时间约为 60 000 毫秒(rescue
)。是的,我只尝试了几次。.
所以在这里我可以得出结论,正如斯皮克曼所说,异常处理确实非常昂贵。
但我认为在很多情况下,第一次初始化很少发生(比如一开始没有帖子的博客)。
在这种情况下,没有理由每次都测试nil
,使用异常处理可能会很有趣。
我错了吗?
我不想挑剔一些女士。在这里,我只想知道这个成语作为一种设计模式是否有意义。我看到了这两个版本的差异,例如while... end
和do ... while
之间的差异,因为意图并不相同。