3

When I delegate methods on instances of class A to $delegation_target as below:

$delegation_target = ""
class A
  def method_missing *args, ≺ $delegation_target.send(*args, &pr) end
  def respond_to_missing? *args; $delegation_target.respond_to?(*args) end
end

the arity of the methods on instances of A returns -1 irrespective of the arity of the methods on $delegation_target:

def $delegation_target.foo; end
A.new.method(:foo).arity # => -1

def $delegation_target.foo arg1; end
A.new.method(:foo).arity # => -1

def $delegation_target.foo arg1, arg2; end
A.new.method(:foo).arity # => -1

def $delegation_target.foo arg1, arg2, arg3; end
A.new.method(:foo).arity # => -1

Where does this -1 come from? And, is there a way to make it so that, for any possible method name m, A.new.method(m).arity returns the arity of $delegation_target.method(m) (if it is defined)?

4

2 回答 2

6

Object#method handles respond_to_missing? & method_missing in a special way. Let's dive into the Ruby C source and see what happens:

Starting at Object#method, we call mnew, which creates a new Method object for the object it's called on and the id passed. In the source for mnew we can easily see the special handling when the method is not defined, but respond_to_missing? returns true when given the id:

if (UNDEFINED_METHOD_ENTRY_P(me)) {
    ID rmiss = rb_intern("respond_to_missing?");
    VALUE sym = ID2SYM(id);

    if (obj != Qundef && !rb_method_basic_definition_p(klass, rmiss)) {
        if (RTEST(rb_funcall(obj, rmiss, 2, sym, scope ? Qfalse : Qtrue))) {
            def = ALLOC(rb_method_definition_t);
            def->type = VM_METHOD_TYPE_MISSING;
            def->original_id = id;
            // ...

The def->type = VM_METHOD_TYPE_MISSING; is important. Finding the definition for VM_METHOD_TYPE_MISSING, we see that it is a "wrapper for method_missing(id)". So essentially the method that is returned is really just method_missing, with the first argument already specified as the id of the method you were originally trying to get.

We can confirm our suspicions by verifying that the arity of method_missing is the same as what we're getting:

A.new.method(:method_missing).arity  #=> -1

As an aside, an arity of -1 means that the method can take an unlimited number of arguments.

As for whether you can have it return the "real" arity of the method being called… no, you can't. For one, Ruby makes no assumptions about what happens in your method_missing, and doesn't even know that it's simply delegating to some other method.

于 2012-12-09T17:59:32.740 回答
3

A negative arity means that there is a final *-prefixed argument. If there is one required argument followed by a *-prefixed one (so one required argument and an optional additional amount), then the arity would be represented as -2, so -n-1, where n is the number of required arguments.

-n-1 is known as the one's complement of n, and ruby even has an operator to get the number of required arguments, the ~ operator.

p ~A.new.method(:foo).arity #=> 0
于 2012-12-09T17:58:18.233 回答