UnboundMethod

UnboundMethod

Ruby는 "객체화된 메서드(objectified method)"의 두 가지 형태를 지원해요. ClassMethod는 특정 객체와 연결된 메서드, 즉 그 객체에 바인딩(bound)된 메서드 객체를 나타내죠. 객체에 바인딩된 메서드 객체는 Object#method으로 만들 수 있어요.

Ruby는 또한 특정 객체와 연결되지 않은 메서드 객체, 즉 언바운드(unbound) 메서드도 지원해요. 이것은 Module#instance_method을 호출하거나, 바인딩된 메서드 객체에서 unbind를 호출해 만들 수 있어요. 두 경우 모두 결과는 UnboundMethod 객체예요.

언바운드 메서드는 어떤 객체에 바인딩한 뒤에만 호출할 수 있어요. 그 객체는 반드시 메서드의 원래 클래스의 kind_of?여야 해요.

class Square
  def area
    @side * @side
  end
  def initialize(side)
    @side = side
  end
end

area_un = Square.instance_method(:area)

s = Square.new(12)
area = area_un.bind(s)
area.call   #=> 144

언바운드 메서드는 객체화된 시점의 메서드에 대한 참조예요. 그래서 이후에 기본 클래스가 바뀌어도 언바운드 메서드에는 영향이 없어요.

class Test
  def test
    :original
  end
end
um = Test.instance_method(:test)
class Test
  def test
    :modified
  end
end
t = Test.new
t.test            #=> :modified
um.bind(t).call   #=> :original

출처: Ruby 3.3 API

본문

각 메서드를 하나씩 살펴볼게요.

==(other_meth) → true or false

두 언바운드 메서드 객체는 같은 메서드 정의를 가리킬 때 같다고 판정해요.

Array.instance_method(:each_slice) == Enumerable.instance_method(:each_slice)
#=> true

Array.instance_method(:sum) == Enumerable.instance_method(:sum)
#=> false, Array redefines the method for efficiency

arity → integer

메서드가 받는 인자 수를 나타내는 값을 돌려줘요. 고정된 개수의 인자를 받는 메서드는 0 이상의 정수를 돌려주고, 가변 인자를 받는 Ruby 메서드는 -n-1을 돌려줘요. 여기서 n은 필수 인자의 수예요. 키워드 인자는 별도의 추가 인자 하나로 간주되는데, 키워드 인자 중 필수인 것이 있으면 그 인자도 필수로 취급돼요. C로 작성된 메서드가 가변 인자를 받으면 -1을 돌려줘요.

class C
  def one;    end
  def two(a); end
  def three(*a);  end
  def four(a, b); end
  def five(a, b, *c);    end
  def six(a, b, *c, &d); end
  def seven(a, b, x:0); end
  def eight(x:, y:); end
  def nine(x:, y:, **z); end
  def ten(*a, x:, y:); end
end
c = C.new
c.method(:one).arity     #=> 0
c.method(:two).arity     #=> 1
c.method(:three).arity   #=> -1
c.method(:four).arity    #=> 2
c.method(:five).arity    #=> -3
c.method(:six).arity     #=> -3
c.method(:seven).arity   #=> -3
c.method(:eight).arity   #=> 1
c.method(:nine).arity    #=> 1
c.method(:ten).arity     #=> -2

"cat".method(:size).arity      #=> 0
"cat".method(:replace).arity   #=> 1
"cat".method(:squeeze).arity   #=> -1
"cat".method(:count).arity     #=> -1

bind(obj) → method

umethobj에 바인딩해요. umeth를 얻은 클래스가 Klass라면, obj.kind_of?(Klass)true여야 해요.

class A
  def test
    puts "In test, class = #{self.class}"
  end
end
class B < A
end
class C < B
end

um = B.instance_method(:test)
bm = um.bind(C.new)
bm.call
bm = um.bind(B.new)
bm.call
bm = um.bind(A.new)
bm.call

결과는 이렇게 나와요:

In test, class = C
In test, class = B
prog.rb:16:in `bind': bind argument must be an instance of B (TypeError)
 from prog.rb:16

bind_call(recv, args, ...) → obj

umethrecv에 바인딩한 다음, 지정된 인자들로 메서드를 호출해요. 이는 umeth.bind(recv).call(args, ...)과 의미적으로 같아요.

clone → new_method

이 메서드의 클론을 돌려줘요.

class A
  def foo
    return "bar"
  end
end

m = A.new.method(:foo)
m.call # => "bar"
n = m.clone.call # => "bar"

eql?(other_meth) → true or false

두 언바운드 메서드 객체는 같은 메서드 정의를 가리킬 때 같다고 판정해요.

Array.instance_method(:each_slice) == Enumerable.instance_method(:each_slice)
#=> true

Array.instance_method(:sum) == Enumerable.instance_method(:sum)
#=> false, Array redefines the method for efficiency

hash → integer

메서드 객체에 대응하는 해시 값을 돌려줘요. Object#hash도 함께 참고하세요.

inspect → string

기본 메서드의 사람이 읽기 좋은 설명을 돌려줘요.

"cat".method(:count).inspect   #=> "#<Method: String#count(*)>"
(1..3).method(:map).inspect    #=> "#<Method: Range(Enumerable)#map()>"

후자의 경우 메서드 설명은 원래 메서드의 "owner"( Range에 포함된 Enumerable 모듈)를 포함해요. inspect는 가능할 때 메서드 인자 이름(호출 순서)과 소스 위치도 제공해요.

require 'net/http'
Net::HTTP.method(:get).inspect
#=> "#<Method: Net::HTTP.get(uri_or_host, path=..., port=...) <skip>/lib/ruby/2.7.0/net/http.rb:457>"

인자 정의에서 ...는 그 인자가 선택적(기본값이 있음)이라는 뜻이에요. C로 정의된 메서드(언어 코어와 확장)에서는 위치와 인자 이름을 뽑아낼 수 없고, *(임의 개수 인자)나 _(위치 인자 몇 개) 형태의 일반 정보만 제공돼요.

"cat".method(:count).inspect   #=> "#<Method: String#count(*)>"
"cat".method(:+).inspect       #=> "#<Method: String#+(_)>"

name → symbol

메서드의 이름을 돌려줘요.

original_name → symbol

메서드의 원래 이름을 돌려줘요.

class C
  def foo; end
  alias bar foo
end
C.instance_method(:bar).original_name # => :foo

owner → class_or_module

이 메서드가 정의된 클래스나 모듈을 돌려줘요. 다시 말해,

meth.owner.instance_methods(false).include?(meth.name) # => true

메서드가 제거/정의되지 않음/재정의되기 전까지는 위 조건이 성립해요(메서드가 private면 instance_methods 대신 private_instance_methods를 사용). Method#receiver도 함께 참고하세요.

(1..3).method(:map).owner #=> Enumerable

parameters → array

이 메서드의 파라미터 정보를 돌려줘요.

def foo(bar); end
method(:foo).parameters #=> [[:req, :bar]]

def foo(bar, baz, bat, &blk); end
method(:foo).parameters #=> [[:req, :bar], [:req, :baz], [:req, :bat], [:block, :blk]]

def foo(bar, *args); end
method(:foo).parameters #=> [[:req, :bar], [:rest, :args]]

def foo(bar, baz, *args, &blk); end
method(:foo).parameters #=> [[:req, :bar], [:req, :baz], [:rest, :args], [:block, :blk]]

source_location → [String, Integer]

이 메서드를 포함하는 Ruby 소스 파일명과 줄 번호를 돌려줘요. 메서드가 Ruby로 정의되지 않았다면(즉 네이티브라면) nil을 돌려줘요.

super_method → method

super를 사용할 때 호출될 슈퍼클래스의 Method를 돌려줘요. 슈퍼클래스에 그 메서드가 없으면 nil을 돌려줘요.

to_s → string

기본 메서드의 사람이 읽기 좋은 설명을 돌려줘요.

"cat".method(:count).inspect   #=> "#<Method: String#count(*)>"
(1..3).method(:map).inspect    #=> "#<Method: Range(Enumerable)#map()>"

후자의 경우 메서드 설명은 원래 메서드의 "owner"( Range에 포함된 Enumerable 모듈)를 포함해요. to_s는 가능할 때 메서드 인자 이름(호출 순서)과 소스 위치도 제공해요.

require 'net/http'
Net::HTTP.method(:get).inspect
#=> "#<Method: Net::HTTP.get(uri_or_host, path=..., port=...) <skip>/lib/ruby/2.7.0/net/http.rb:457>"

인자 정의에서 ...는 그 인자가 선택적(기본값이 있음)이라는 뜻이에요. C로 정의된 메서드(언어 코어와 확장)에서는 위치와 인자 이름을 뽑아낼 수 없고, *(임의 개수 인자)나 _(위치 인자 몇 개) 형태의 일반 정보만 제공돼요.

"cat".method(:count).inspect   #=> "#<Method: String#count(*)>"
"cat".method(:+).inspect       #=> "#<Method: String#+(_)>"