The method_missing
method
is a key part of Ruby’s method lookup algorithm (see Method Lookup) and provides a powerful way to catch and
handle arbitrary invocations on an object. The const_missing
method of Module
performs a
similar function for the constant lookup algorithm and allows us to
compute or lazily initialize constants on the fly. The examples that
follow demonstrate both of these methods.
Example 8-3 defines a Unicode
module
that appears to define a constant (a UTF-8 encoded string) for every Unicode
codepoint from U+0000
to U+10FFFF
. The only practical way to support
this many constants is to use the const_missing
method. The code makes the
assumption that if a constant is referenced once, it is likely to be
used again, so the const_missing
method calls Module.const_set
to
define a real constant to refer to each value it computes.
Example 8-3. Unicode codepoint constants with const_missing
# This module provides constants that define the UTF-8 strings for # all Unicode codepoints. It uses const_missing to define them lazily. # Examples: # copyright = Unicode::U00A9 # euro = Unicode::U20AC # infinity = Unicode::U221E module Unicode # This method allows us to define Unicode codepoint constants lazily. def self.const_missing(name) # Undefined constant passed as a symbol # Check that the constant name is of the right form. # Capital U followed by a hex number between 0000 and 10FFFF. if name.to_s =~ /^U([0-9a-fA-F]{4,5}|10[0-9a-fA-F]{4})$/ # $1 is the matched hexadecimal number. Convert to an integer. codepoint = $1.to_i(16) # Convert the number to a UTF-8 string with the magic of Array.pack. utf8 = [codepoint].pack("U") # Make the UTF-8 string immutable. utf8.freeze # Define a real constant for faster lookup next time, and return # the UTF-8 text for this time. const_set(name, utf8) else # Raise an error for constants of the wrong form. raise NameError, "Uninitialized constant: Unicode::#{name}" end end end
Earlier in this chapter, we demonstrated an extension to the
Hash
class using method_missing
. Now, in Example 8-4, we demonstrate the use of method_missing
to delegate arbitrary calls
on one object to another object. In this example, we do this in order
to output tracing messages for the object.
Example 8-4 defines an Object.trace
instance method and a TracedObject
class. The trace
method returns an instance of TracedObject
that uses method_missing
to catch invocations, trace them, and delegate them to
the object being traced. You might use it like this:
a = [1,2,3].trace("a") a.reverse puts a[2] puts a.fetch(3)
This produces the following tracing output:
Invoking: a.reverse() at trace1.rb:66 Returning: [3, 2, 1] from a.reverse to trace1.rb:66 Invoking: a.fetch(3) at trace1.rb:67 Raising: IndexError:index 3 out of array from a.fetch
Notice that in addition to demonstrating method_missing
, Example 8-4
also demonstrates Module.instance_methods
, Module.undef_method
, and Kernel.caller
.
Example 8-4. Tracing method invocations with method_missing
# Call the trace method of any object to obtain a new object that # behaves just like the original, but which traces all method calls # on that object. If tracing more than one object, specify a name to # appear in the output. By default, messages will be sent to STDERR, # but you can specify any stream (or any object that accepts strings # as arguments to <<). class Object def trace(name="", stream=STDERR) # Return a TracedObject that traces and delegates everything else to us. TracedObject.new(self, name, stream) end end # This class uses method_missing to trace method invocations and # then delegate them to some other object. It deletes most of its own # instance methods so that they don't get in the way of method_missing. # Note that only methods invoked through the TracedObject will be traced. # If the delegate object calls methods on itself, those invocations # will not be traced. class TracedObject # Undefine all of our noncritical public instance methods. # Note the use of Module.instance_methods and Module.undef_method. instance_methods.each do |m| m = m.to_sym # Ruby 1.8 returns strings, instead of symbols next if m == :object_id || m == :__id__ || m == :__send__ undef_method m end # Initialize this TracedObject instance. def initialize(o, name, stream) @o = o # The object we delegate to @n = name # The object name to appear in tracing messages @trace = stream # Where those tracing messages are sent end # This is the key method of TracedObject. It is invoked for just # about any method invocation on a TracedObject. def method_missing(*args, &block) m = args.shift # First arg is the name of the method begin # Trace the invocation of the method. arglist = args.map {|a| a.inspect}.join(', ') @trace << "Invoking: #{@n}.#{m}(#{arglist}) at #{caller[0]} " # Invoke the method on our delegate object and get the return value. r = @o.send m, *args, &block # Trace a normal return of the method. @trace << "Returning: #{r.inspect} from #{@n}.#{m} to #{caller[0]} " # Return whatever value the delegate object returned. r rescue Exception => e # Trace an abnormal return from the method. @trace << "Raising: #{e.class}:#{e} from #{@n}.#{m} " # And re-raise whatever exception the delegate object raised. raise end end # Return the object we delegate to. def __delegate @o end end
In Example 8-2, we saw a global method synchronized
, which accepts an object and
executes a block under the protection of the Mutex
associated with that object. Most of
the example consisted of the implementation of the Object.mutex
method. The synchronized
method was trivial:
def synchronized(o) o.mutex.synchronize { yield } end
Example 8-5 modifies this method so
that, when invoked without a block, it returns a SynchronizedObject
wrapper around the
object. SynchronizedObject
is a
delegating wrapper class based on method_missing
. It is much like the TracedObject
class of Example 8-4, but it is written as a subclass of
Ruby 1.9’s BasicObject
, so
there is no need to explicitly delete the instance methods of
Object. Note that the code in this example does not stand alone; it
requires the Object.mutex
method
defined earlier.
Example 8-5. Synchronizing methods with method_missing
def synchronized(o) if block_given? o.mutex.synchronize { yield } else SynchronizedObject.new(o) end end # A delegating wrapper class using method_missing for thread safety # Instead of extending Object and deleting our methods we just extend # BasicObject, which is defined in Ruby 1.9. BasicObject does not # inherit from Object or Kernel, so the methods of a BasicObject cannot # invoke any top-level methods: they are just not there. class SynchronizedObject < BasicObject def initialize(o); @delegate = o; end def __delegate; @delegate; end def method_missing(*args, &block) @delegate.mutex.synchronize { @delegate.send *args, &block } end end
3.137.212.124