UnboundMethod 클래스

UnboundMethod 클래스

Ruby에는 "객체화된 메서드"가 두 가지 형태로 있어요. Method 클래스는 특정 객체에 바인딩된 메서드를 나타내고, UnboundMethod는 특정 객체에 바인딩되지 않은 메서드예요. Object#method로 얻는 Method vs Module#instance_methodbind된 메서드에 unbind를 호출해 얻는 UnboundMethod로 나뉘는 거죠.

출처: Ruby 4.0 API — UnboundMethod

본문

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

Area 메서드를 클래스에서만 떼어내 area_un을 만들고, Square 인스턴스 sbind한 뒤에야 call로 실행할 수 있어요.

또 하나 중요한 특징이 있어요. UnboundMethod객체화된 시점의 메서드 정의를 참조해요. 이후에 원래 클래스를 수정해도 언바운드 메서드에는 영향이 없어요.

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

동일한 test 클래스 메서드지만, t.test는 다시 정의된 :modified를, 예전에 객체화한 um은 원래의 :original을 가리키는 걸 볼 수 있어요.

Public Instance Methods

meth == other_meth → true or false

두 언바운드 메서드 객체가 같은 메서드 정의를 참조하면 true예요.

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

each_sliceArrayEnumerable에서 가져온 정의를 그대로 쓰므로 같지만, sumArray가 성능을 위해 재정의했기 때문에 다르게 평가돼요.

arity → integer

메서드가 받아들이는 인자 수를 나타내는 정수를 돌려줘요.

  • 고정된 개수의 인자를 받는 메서드는 음이 아닌 정수를 반환해요.
  • 가변 인자를 받는 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

여기서 -3은 "필수 인자 2개 + 나머지 가변 인자"라는 의미의 -(2+1)이고, -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

C.newB.newB의 하위/동일 타입이라 바인딩되지만, A.newB의 인스턴스가 아니어서 TypeError가 발생해요. 바인딩 대상은 원래 클래스 자신 또는 더 아래(하위) 타입이어야 하죠.

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

두 언바운드 메서드 객체가 같은 메서드 정의를 참조하면 true예요. ==와 동일한 기준으로 동작해요.

hash → integer

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

inspect → string

기저 메서드에 대한 사람이 읽을 수 있는 설명을 돌려줘요.

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

두 번째 예에서 설명에 원래 메서드의 "주인"(Range에 include된 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

메서드의 원래 이름을 돌려줘요. alias로 만들어진 메서드라면 별칭이 아니라 원래 이름이 나와요.

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를 써요).

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

Method#receiver도 함께 참고하세요.

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]]

종류 심볼로는 :req(필수), :opt(선택), :rest(가변), :key(키워드), :keyreq(필수 키워드), :block(블록) 등이 있어요.

source_location → [String, Integer, Integer, Integer, Integer]

메서드가 정의된 위치를 돌려줘요. 반환되는 Array는 다음을 포함해요.

  1. Ruby 소스 파일명
  2. 정의가 시작되는 줄 번호
  3. 정의가 시작되는 열 번호
  4. 정의가 끝나는 줄 번호
  5. 정의가 끝나는 열 번호

메서드가 Ruby에서 정의되지 않았으면(즉 네이티브라면) nil을 돌려줘요.

super_method → method

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

to_s → string

기저 메서드에 대한 사람이 읽을 수 있는 설명을 돌려줘요. inspect와 동일한 출력 기준이에요.

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