开发者

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!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜