오버로딩

오버로딩 (Overloading)

become_older라는, 나이를 얼마나 늘릴지 숫자를 받는 메서드를 정의한다고 해 볼게요.

class Person
  getter :age

  def initialize(@name : String, @age : Int = 0)
  end

  def become_older
    @age += 1
  end

  def become_older(years)
    @age += years
  end
end

john = Person.new "John"
john.age # => 0

john.become_older
john.age # => 1

john.become_older 5
john.age # => 6

즉 같은 이름에 매개변수 개수가 다른 메서드를 여러 개 둘 수 있고, 그것들이 각각 별개의 메서드로 취급돼요. 이걸 메서드 오버로딩(method overloading) 이라고 해요.

출처: Crystal 공식 문서

본문

메서드는 여러 기준으로 오버로드돼요.

  • 매개변수의 개수
  • 매개변수에 적용된 타입 제한
  • 필수 named 매개변수의 이름
  • 메서드가 블록을 받는지 여부

예를 들어 네 가지 서로 다른 become_older 메서드를 정의할 수 있어요.

class Person
  @age = 0

  # Increases age by one
  def become_older
    @age += 1
  end

  # Increases age by the given number of years
  def become_older(years : Int32)
    @age += years
  end

  # Increases age by the given number of years, as a String
  def become_older(years : String)
    @age += years.to_i
  end

  # Yields the current age of this person and increases
  # its age by the value returned by the block
  def become_older(&)
    @age += yield @age
  end
end

person = Person.new "John"

person.become_older
person.age # => 1

person.become_older 5
person.age # => 6

person.become_older "12"
person.age # => 18

person.become_older do |current_age|
  current_age < 20 ? 10 : 30
end
person.age # => 28

블록을 받는 메서드의 경우, 컴파일러는 yield 표현식이 있기 때문에 그 사실을 알아차려요. 더 명확하게 나타내고 싶다면 마지막에 더미 &block 매개변수를 추가할 수 있어요.

class Person
  @age = 0

  def become_older(&block)
    @age += yield @age
  end
end

생성된 문서에서는 더미 &block 메서드가, 직접 쓰든 안 쓰든 항상 나타나요.

매개변수 개수가 같다면, 컴파일러는 제한이 덜 엄격한 메서드를 뒤로 보내도록 정렬하려고 해요.

class Person
  @age = 0

  # First, this method is defined
  def become_older(age)
    @age += age
  end

  # Since "String" is more restrictive than no restriction
  # at all, the compiler puts this method before the previous
  # one when considering which overload matches.
  def become_older(age : String)
    @age += age.to_i
  end
end

person = Person.new "John"

# Invokes the first definition
person.become_older 20

# Invokes the second definition
person.become_older "12"

하지만 항상 전체 순서(total ordering)가 있는 건 아니라서 컴파일러가 순서를 항상 알아내지는 못해요. 그래서 제한이 덜 엄격한 메서드를 뒤에 두는 편이 항상 더 좋아요.

주의사항 (Caveats)

오버로드 순서가 의도한 대로 되지 않는 알려진 컴파일러 버그들이 있어요. 안타깝게도 이 버그를 고치면, 이 특정하고 의도하지 않은 오버로드 순서에 의존하는 기존 코드가 깨질 수 있어요. 기존 코드를 깨뜨리지 않으려고 노력하기 때문에 고치기 쉽지 않아요.

미리보기 플래그

일부 버그 수정은 이미 컴파일러 플래그 -Dpreview_overload_order(Crystal 1.6.0에서 도입)로 사용할 수 있어요.

새 Crystal 코드를 작성할 때는 이 플래그 사용을 고려해 보세요.

이 플래그는 언젠가 기본으로 켜질 것으로 예상돼요. 그 시점에는 모든 Crystal 코드가 올바른 오버로드 순서를 쓰게 될 것이고, 여전히 잘못된 순서에 의존하는 코드는 전환 기간 동안 옵트아웃 기능 플래그를 쓸 수 있어요.

알려진 버그

  • 매개변수가 없는 오버로드가 기본값 있는 오버로드를 덮어써요 (#10231)

    def bar(x = true)
    end
    
    def bar
    end
    
    bar 1 # Error: wrong number of arguments for 'bar' (given 1, expected 0)
    

    이 문제는 -Dpreview_overload_order로 해결돼요.

  • 오버로드 순서가 타입 제한에 쓰인 타입의 정의 순서에 의존해요 (#7579, #4897)

    class Foo
    end
    
    def foo(a : Bar)
    1
    end
    
    def foo(a : Foo)
    true
    end
    
    class Bar < Foo
    end
    
    foo(Bar.new) # => true # This should be 1
    

    해결 방법으로, Bar의 선언을 def foo보다 앞으로 옮길 수 있어요.

더 알아보기

  • blocks and procs: 블록을 받는 메서드의 오버로드
  • 타입 제한: 오버로드 판별에 쓰이는 메커니즘