Nils and method chaining

2019-03-26 05:01发布

问题:

I'm just breaking into the ruby world and I could use a helping hand.

Suppose b is nil.

I'd like the following code to return nil instead of a "NoMethodError: undefined method"

a.b.c("d").e

The first thing I tried was to overload NilClass's missing_method to simply return a nil. This is the behaviour I want except I don't want to be this intrusive.

I'd love it if I could do something like this

SafeNils.a.b.c("d").e

So it's like a clean way to locally overload the NilClass's behaviour.

I'd love to hear some ideas or great resources for digging in on this. I'm also quite open to other approaches as long as it's fairly clean.

Thank you very much.

回答1:

I think you can find a great solution in rails but that solution follows a different approach. Take a look at the try method. It's a clean approach.



回答2:

You can use the inline rescue:

x = a.b.c("d").e rescue nil

x will be nil if b is nil.

Or, as someone commented in that link, you can use the andand gem (see docs):

x = a.andand.b.andand.c("d").andand.e


回答3:

Check Ick's maybe:

a.b.maybe.c("d").maybe.e

or using a block:

a.b.maybe { |b| b.c("d").e }


回答4:

"try" is very clean, as lucapette said. More generally, you could also use a begin-rescue-end block too, depending on your situation.

begin
  a.b.c("d").e
rescue NoMethodError=>e
  return nil
end


回答5:

Remark in advance: b is a method, not a variable. So b 'is' not nil, it returns nil.

When 'b' is a method, why not modify b, so it returns something, what can handle nil.

See below for an example.


You may define the missing methods:

class A
  def b
    nil
  end
end
class NilClass
  def c(p);nil;end
  def e;nil;end
end
a = A.new

a.b.c("d").e

But I think, a rescue may fit your need better:

class A
  def b
    nil
  end
end
a = A.new
x = begin a.c.c("d").e 
rescue NoMethodError
  nil
end

An example, how you may define a nil-like example.

class A
  def b
    MyNil.new
  end
end

class MyNil
  def method_missing(m, *args, &block)
      if nil.respond_to?(m)
        nil.send(m)
    else
      self
    end
  end
  #Simulate nils bahaviour.
  def nil?;true;end
  def inspect;nil.inspect;end
  def to_s;nil;end
end

a = A.new
x = a.b.c("d").e 
p x
puts x
p x.nil?


回答6:

To use a safe_nils similar to that you wrote:

def safe_nils &blk
  return blk.call
rescue NoMethodError
  return nil
end

safe_nils { a.b.c("d").e }


回答7:

I've made the may_nil gem for this. https://github.com/meesern/may_nil

Like @Sony Santos's safe_nils answer it uses a block to wrap the method chain and rescues NoMethodError, but will check for nil (and so will properly raise an exception for other classes).

require `may_nil`
may_nil {a.b.c("d").e}   => nil (or the end result)
may_nil {0.b.c("d").e}   => Exception: NoMethodError (b on Fixnum)

Unlike andand or ike's maybe you don't need to pepper the method chain.

a.andand.b.andand.c("d").andand.e
==>
may_nil{ a.b.c("d").e }


回答8:

One approach is to use inline assignment to local variables:

a && (ab = a.b) && (abcd = ab.c("d")) && abcd.e

For as long a chain as you've got here it isn't very elegant, but for a shorter chain it can be useful:

def date_updated(entry)
  (updated = entry.updated) && updated.content
end


回答9:

Starting from ruby v2.3.0 there is another way built into the language, the safe navigation operator (&.) You can write: a&.b&.c&.d and you will safely get nil if one of the calls in the chain returns nil. You can read more about it here: http://mitrev.net/ruby/2015/11/13/the-operator-in-ruby/