Just for fun - can I write a custom NilClass for a specific use?
EDIT | Or another question, on the same object subject. Can I write my own class definition that would cause the following all to work?
o = WeirdObject.new
puts "Object o evaluates as true in boolean expressions" if o # Line never output
puts "Object o is nil?" if o.nil? # Line is output
I can do the nil?
thing easy, since that's just a method. But no idea how to make it evaluate as a non-true value in a boolean expression (including just the most basic expression "o").
Original question follows...
More out of curiosity as to how much fun I can have with Ruby (1.9.2) really...
When a user is not logged in, I'd like to be able to detect it with unless @user
or unless @user.nil?
etc, but at the same time, I'd like to be able to call the methods of User
on this object and have them return the same NilClass
(ala Objective-C), since in most places this would remove the need for boilerplate if @user
code, while still acting like nil
in boolean expressions.
I see that you can't subclass NilClass
, since it doesn't have a #new
method. You can add methods directly to the instance, but it's a singleton, so this would cause issues if the only place I wanted to observe this behaviour was with non-logged-in users.
Is it possible?
What I mean, is something like this:
class CallableNil < NilClass
def method_missing(meth, *args, &block)
self
end
end
user = CallableNil.new # or .singleton, or something
puts "Got here" unless user # Outputs
puts "And here" unless user.nil? # also outputs
puts user.username # no-op, returns nil
This is basically how Objective-C handles nil and it's useful in some circumstances.
I guess I could not subclass NilClass and just implement the methods that would cause it to be false in boolean expressions?
EDIT | Haha, you really break Ruby pretty badly if you redefine the top-level NilClass
... it stops responding to any method call at all.
>> class NilClass
>> def method_missing(meth, *args, &block)
>> self
>> end
&g开发者_运维百科t;> end
>>
?> nil.foo
>>
?>
?> puts "here"
>> quit
>> # WTF? Close dammit!
If your primary requirement is to be able to call methods on a nil
value without raising an exception (or having to check that it's defined), ActiveSupport
adds Object#try
which gives you the ability to do this:
instance = Class.method_that_returns_nil
unless instance
puts "Yes, we have no instance."
end
instance.try(:arbitrary_method) # => nil
instance.try(:arbitrary_method, "argument") # => nil
As you've seen, screwing around with NilClass
has some interesting side-effects.
Another potential solution (not thread safe):
def null!
@@null_line = caller.first
end
class NilClass
def method_missing(name, *args, &block)
if caller.first == @@null_line
nil
else
super
end
end
end
null!; nil.a.b.c.d.e # returns nil
nil.a # throws an error
Generally, it's not recommended to change the nil
object in this way -- fail-fast. I'd recommend to use the egonil or the andand gem. For some more reading/links on this topic [and for fun ;)], checkout this blog post .
I was looking for a very similar thing today and the general consensus is that it's "impossible" to turn "nil" into "null" without breaking everything. So, this is how you do impossible:
class ::BasicObject
def null!() self end
def nil!() self end
end
class ::NilClass
NULL_BEGIN = __LINE__
def __mobj__caller()
caller.find do |frame|
(file, line) = frame.split(":")
file != __FILE__ || !(NULL_BEGIN..NULL_END).cover?(line.to_i)
end
end
def null?()
@@null ||= nil
@@null && @@null == __mobj__caller
end
def null!()
@@null = __mobj__caller
self
end
def nil!
@@null = nil
self
end
def method_missing(name, *args, &block)
if null?
self
else
nil!
self
end
end
NULL_END = __LINE__
end
(Forgive the formatting if it's screwed up, I'm doing this on an iPhone)
This adds a "null!" method to both nil and all objects in general. For normal objects, it's a no-op, and things work as normal (including throwing exceptions when methods are missing). For nil, however, it won't throw exceptions, no matter how many methods are in the chain, and it will still evaluate exactly as nil (since it's still just a normal nil). The new behavior is scoped only to the single line from which "null!" was called. Afterwards nil returns to it's normal behavior.
if obj.null!.foo.bar.baz
puts "normal execution if this is valid for obj"
end
obj = nil
if obj.null!.foo.bar.baz
puts "this will not print, and no exception"
end
if obj.foo.bar.baz
puts "this will throw an exception as usual"
end
It achieves this by walking up the call tree and marking the first file/line that isn't it's own code, and using that as a flag to indicate that it is now in "null" mode. As long as the file/line don't change, it will continue to act like a "null". As soon as it sees that the interpreter has moved on, it resets itself back to normal behavior.
The caveats for this approach are:
Your statement must all fit on a single line.
Your statement probably must have an actual file and line number (I've not tested it in IRB, but I suspect it will not work).. Without these it can't tell that it's still executing on the same line, so it will probably revert to normal right away.
The new behavior will continue to affect other nils until the end of the line. You can force it to act like normal again by calling "nil!" later in the line, though. For example:
obj.null!.foo.bar.nil! || normal.nil.behavior
Anything that successful method calls do in their own bodies that call non existant methods against nil or call "null!" will probably reset the behavior since those take place at other line numbers, but, in theory, that shouldn't be a problem unless you are doing very weird things.
If you'd like this kind of "fun" stuff, I have a gem called "mobj" on gemcutter with a bunch of this wacky hackery, or pull the source from here:
https://github.com/gnovos/mobj
I have another gem that could do this in a different way, if you want a more robust solution (and don't mind wrapping things in blocks):
require "ctx"
class NilClass
ctx :null do
def missing_method(*) self end
end
end
And then, anywhere that you want this behavior;
obj = nil
...
ctx :null do
if obj.foo.bar
puts "inside ctx block, acts like 'null!' from above, but extends to everything up and down the stack that inside of the ctx scope"
end
end
if obj.foo.bar
puts "outside ctx block, should be back to normal again"
end
I haven't tested this last one, but it will probably work. If this interests you, you can find ctx here:
https://github.com/gnovos/ctx
Cheers!
精彩评论