UnboundMethod 클래스
UnboundMethod 클래스
Ruby에는 "객체화된 메서드"가 두 가지 형태로 있어요. Method 클래스는 특정 객체에 바인딩된 메서드를 나타내고, UnboundMethod는 특정 객체에 바인딩되지 않은 메서드예요. Object#method로 얻는 Method vs Module#instance_method나 bind된 메서드에 unbind를 호출해 얻는 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 인스턴스 s에 bind한 뒤에야 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_slice는 Array가 Enumerable에서 가져온 정의를 그대로 쓰므로 같지만, sum은 Array가 성능을 위해 재정의했기 때문에 다르게 평가돼요.
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
umeth을 obj에 바인딩해요. 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.new와 B.new는 B의 하위/동일 타입이라 바인딩되지만, A.new는 B의 인스턴스가 아니어서 TypeError가 발생해요. 바인딩 대상은 원래 클래스 자신 또는 더 아래(하위) 타입이어야 하죠.
bind_call(recv, args, ...) → obj
umeth을 recv에 바인딩한 뒤, 지정한 인자들로 그 메서드를 바로 호출해요. 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는 다음을 포함해요.
- Ruby 소스 파일명
- 정의가 시작되는 줄 번호
- 정의가 시작되는 열 번호
- 정의가 끝나는 줄 번호
- 정의가 끝나는 열 번호
메서드가 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()>"