Ruby类似于nil的对象

如何在ruby中创建一个Object,它将在类似于nil的逻辑表达式中被评估为false?

我的目的是在其他对象上启用嵌套调用,其中链的一半位置通常nil ,但允许所有调用继续 – 返回我的类似nil的对象而不是nil本身。 该对象将返回自己以响应任何收到的消息,它不知道如何处理,我预计我将需要实现一些覆盖方法,如nil?

例如:

 fizz.buzz.foo.bar 

如果fizzbuzz属性不可用,我会返回我的类似nil的对象,它会接受调用一直到bar返回自己。 最终,上述陈述应评估为假。

编辑:

根据以下所有优秀答案,我提出了以下建议:

 class NilClass attr_accessor :forgiving def method_missing(name, *args, &block) return self if @forgiving super end def forgive @forgiving = true yield if block_given? @forgiving = false end end 

这允许一些卑鄙的技巧,如:

 nil.forgiving { hash = {} value = hash[:key].i.dont.care.that.you.dont.exist if value.nil? # great, we found out without checking all its parents too else # got the value without checking its parents, yaldi end } 

显然你可以在一些函数调用/ class / module / where里透明地包装这个块。

对于如何处理问题的一堆想法和代码示例,这是一个很长的答案。

try

Rails有一个try方法 ,让你像那样编程。 这就是它的实现方式:

 class Object def try(*args, &b) __send__(*a, &b) end end class NilClass # NilClass is the class of the nil singleton object def try(*args) nil end end 

您可以像这样编程:

 fizz.try(:buzz).try(:foo).try(:bar) 

你可以想象,修改它可以有所不同,以支持更优雅的API:

 class Object def try(*args) if args.length > 0 method = args.shift # get the first method __send__(method).try(*args) # Call `try` recursively on the result method else self # No more methods in chain return result end end end # And keep NilClass same as above 

然后你可以这样做:

 fizz.try(:buzz, :foo, :bar) 

andand

并使用一种更邪恶的技术,黑客攻击你不能直接实例化NilClass子类的事实:

 class Object def andand if self self else # this branch is chosen if `self.nil? or self == false` Mock.new(self) # might want to modify if you have useful methods on false end end end class Mock < BasicObject def initialize(me) super() @me = me end def method_missing(*args) # if any method is called return the original object @me end end 

这允许您以这种方式编程:

 fizz.andand.buzz.andand.foo.andand.bar 

结合一些花哨的重写

你可以再次扩展这种技术:

 class Object def method_missing(m, *args, &blk) # `m` is the name of the method if m[0] == '_' and respond_to? m[1..-1] # if it starts with '_' and the object Mock.new(self.send(m[1..-1])) # responds to the rest wrap it. else # otherwise throw exception or use super # object specific method_missing end end end class Mock < BasicObject def initialize(me) super() @me = me end def method_missing(m, *args, &blk) if m[-1] == '_' # If method ends with '_' # If @me isn't nil call m without final '_' and return its result. # If @me is nil then return `nil`. @me.send(m[0...-1], *args, &blk) if @me else @me = @me.send(m, *args, &blk) if @me # Otherwise call method on `@me` and self # store result then return mock. end end end 

要解释发生了什么:当你调用一个强调方法触发模拟模式时, _meth的结果会自动包装在一个Mock对象中。 无论何时你在该模拟上调用一个方法,它都会检查它是否持有nil ,然后将你的方法转发给该对象(这里存储在@me变量中)。 然后模拟用函数调用的结果替换原始对象。 当你调用meth_它结束模拟模式并返回meth的实际返回值。

这允许像这样的api(我使用下划线,但你可以使用任何东西):

 fizz._buzz.foo.bum.yum.bar_ 

残酷的猴子修补方法

这真的非常讨厌,但它允许一个优雅的API,并不一定搞砸整个应用程序中的错误报告:

 class NilClass attr_accessor :complain def method_missing(*args) if @complain super else self end end end nil.complain = true 

使用这样:

 nil.complain = false fizz.buzz.foo.bar nil.complain = true 

据我所知,没有非常简单的方法可以做到这一点。 在Ruby社区中已经完成了一些工作,它们实现了您正在谈论的function; 你可能想看看:

  • 和gem
  • Rails的try方法

andand gem使用如下:

 require 'andand' ... fizz.buzz.andand.foo.andand.bar 

您可以修改NilClass类以使用method_missing()来响应任何尚未定义的方法。

 > class NilClass > def method_missing(name) > return self > end > end => nil > if nil: * puts "true" > end => nil > nil.foo.bar.baz => nil 

有一个叫做得墨忒耳法则[1]的原则,它表明你所做的不是好习惯,因为你的对象不一定非常了解其他物体的关系。

但是,我们都这样做:-)

在简单的情况下,我倾向于将属性链接委托给检查存在的方法:

 class Fizz def buzz_foo_bar self.buzz.foo.bar if buzz && buzz.foo && buzz.foo.bar end end 

所以我现在可以调用fizz.buzz_foo_bar知道我不会得到exception。

但我也得到了一段代码(在工作中,我不能抓住它直到下周)处理方法缺失并寻找下划线和测试反映关联,以查看它们是否响应链的其余部分。 这意味着我现在不必编写委托方法等等 – 只需包含method_missing补丁:

 module ActiveRecord class Base def children_names association_names=self.class.reflect_on_all_associations.find_all{|x| x.instance_variable_get("@macro")==:belongs_to} association_names.map{|x| x.instance_variable_get("@name").to_s} | association_names.map{|x| x.instance_variable_get("@name").to_s.gsub(/^#{self.class.name.underscore}_/,'')} end def reflected_children_regex Regexp.new("^(" << children_names.join('|') << ")_(.*)") end def method_missing(method_id, *args, &block) begin super rescue NoMethodError, NameError if match_data=method_id.to_s.match(reflected_children_regex) association_name=self.methods.include?(match_data[1]) ? match_data[1] : "#{self.class.name.underscore}_#{match_data[1]}" if association=send(association_name) association.send(match_data[2],*args,&block) end else raise end end end end end 

[1] http://en.wikipedia.org/wiki/Law_of_Demeter

Interesting Posts