Nil and boolean in Ruby

2020-08-12 18:20发布

问题:

Can someone explain the reasoning being this? Just spent 30 mins trying to figure out why my boolean method returned nil and found out that in Ruby:

2.2.1 :001 > nil && true
 => nil
2.2.1 :002 > nil && false
 => nil

Since nil is a falsey value, I would have expected the output of nil && true to be false. Also it seems to go against the idea that conditional operators should return a boolean value.

What is the rationale behind this?

It makes sense that the boolean operator is not commutative:

nil && false != false && nil

For others seeing this, my issue was that in rails I had a statement like:

def some_method?
  object.attr && object.attr > something
end

But when object.attr is nil, the function will be nil. Which is fine in most cases but when chaining boolean methods together, not so much. I just changed it to this instead:

def some_method?
  object.attr.present? && object.attr > something
end

I could do the same thing in vanilla Ruby with:

def some_method?
  !!object.attr && object.attr > something
end

回答1:

The statement goes through the conditions in order, will stop when a falsy result is obtained and return the value of the last evaluation performed.

In contrary to && which stops at a falsy value, || will stop at a truthy value instead.



回答2:

If first operand is falsy (nil or false), then, second operand of && will not be evaluated, as falsy && whatever will be always falsy



回答3:

Problem

You list two related problems:

  1. You are confused by a Boolean expression returning nil instead of false.
  2. You are experiencing surprising behavior when chaining methods when one of the methods may evaluate to nil, false, or some other object that doesn't respond to the next method in the chain.

Nil And False

In your examples, you are chaining methods that may sometimes be called on nil. In Ruby, nil is a falsey value (e.g. it isn't "true"), but it is not actually false, either. Consider:

nil == false
#=> false

They aren't even descended from the same class.

nil.class
#=> NilClass

false.class
#=> FalseClass

It is often best to think of nil as a special value meaning "undefined" or "not applicable". It may also be useful to think of nil as similar to the special database value of null. Nil (like null) isn't true, false, or empty, and understanding this will help you avoid many confusing exceptions.

Short-Circuit Evaluation

When you evaluate nil && false you're really evaluating two separate expressions:

(nil) && (false)

The expression nil is not true, and therefore short-circuits and never evaluates the expression false. As a result, the expression nil && false will correctly return nil rather than false. This is a source of surprise for some people, but is expected behavior for experienced Rubyists.


Solutions

In addition to short-circuit expressions or post-expression conditionals such as:

# Send :to_s unless condition evaluates to false or nil.
object.attr.to_s if object.attr

you should consider the Rails Object#try method, or the Ruby 2.3.0 safe navigation operator.

Use Object#try or Safe Navigation Operator

Object#try

If you're using Rails, the Object#try method is a common way to avoid invoking methods on nil, or other objects that don't #respond_to? the chained method. For example:

# Invoke #to_s without raising an exception. May return nil. 
object.attr.try(:to_s)

This is similar, but not exactly equivalent, to:

object.attr && object.attr.to_s

Ruby 2.3.0 introduces the safe navigation operator (&) which brings similar functionality to Ruby's core.

The & Safe Navigation Operator

Ruby 2.3.0 introduced a new safe navigation operator. This appears to be an actual operator, not a method. This operator lets you chain methods as long as the expression on the left is truthy. For example:

object.attr&.to_s

Because it chains methods, and because comparison operators like > are really methods rather than parser tokens, you can now do things like:

1&.> 0
#=> true

1&.> 2
#=> false

Whether or not you find object.attr&. > something more elegant, readable, or useful than other constructs is a matter of opinion, but it's certainly an option in many cases. YMMV.



回答4:

It's good to have an understanding of variables/values "truthiness":

First, looking at Ruby's idea of true and false:

if true
  'is true' # => "is true"
else
  'is false' # => 
end

if false
  'is true' # => 
else
  'is false' # => "is false"
end

A shorter form of that if/else uses the ternary:

true ? true : false # => true
false ? true : false # => false

Continuing with that:

nil ? true : false # => false
0 ? true : false # => true
1 ? true : false # => true

Notice that nil, when used as a conditional acts like false, whereas 0 is true. (Perl treats false and 0 as false values in tests which is a confusing point when moving to Ruby.) 1 is true, and basically, in Ruby, only nil and false are false and everything else is true.

! performs a "not", reversing the truthiness of the value:

!true ? true : false # => false
!false ? true : false # => true
!nil ? true : false # => true
!0 ? true : false # => false
!1 ? true : false # => false

And !! "nots" the value twice, which we use as a way to quickly convert something from a non-true/false value to a true/false:

!!true ? true : false # => true
!!false ? true : false # => false
!!nil ? true : false # => false
!!0 ? true : false # => true
!!1 ? true : false # => true

Knowing how these work in Ruby makes it easier to understand other people's code as you'll see ! and !! often.

All that leads back to using && and ||. Using || ("or"), only one side has to be true to return a true result:

true || true # => true
false || false # => false
true || false # => true
false || true # => true

With && ("and") both sides have to be true to get a true result:

true && true # => true
false && false # => false
true && false # => false
false && true # => false

Don't confuse || and or, and && and and. Both !! and or do similar things, and && and and, however their tests will occur at different times in an expression. That's order-of-precedence and is very important to know but is a different question. You can find plenty of information about that with simple searches.