본문 바로가기
카테고리 없음

Enumerator 파헤치기4

by 경자꿈사 2025. 2. 7.

이번 글에서는 이전 글에서 살펴 본 Enumerator 클래스의 with_index와 with_object 메서드를 MyEnumerator 클래스에 추가해 볼 건데 먼저 with_index 메서드부터 작성해 보자.
아래 코드를 보면 with_index 메서드도 each 메서드와 동일하게 send 메서드를 사용하여 @object가 참조하는 객체에게 처리를 위임하고 있다.
다만, with_index 메서드를 호출할 때 받은 블록을 send 메서드에 그대로 전달하지 않고 with_index 메서드 안에서 새로 만든 블록을 전달한다.
그리고 새로 만든 블록 안에서는 with_index 메서드를 호출할 때 받은 블록을 실행하는데, 이때 자신이 받은 값(send 메서드를 통해 실제 호출되는 메서드가 블록에 전달하는 값)과 외부 변수를 사용한 인덱스를 블록에 함께 넘겨준다.

class MyEnumerator
  def initialize(object, method, *method_args)
    @object = object
    @method = method
    @method_args = method_args
  end
  
  def each(&block)
    @object.send(@method, *@method_args, &block)
  end
  
  def with_index(&block)
    i = -1
    @object.send(@method, *@method_args) do |e|
      i += 1
      block.call(e, i)
    end
  end
end

with_index 메서드가 추가된 MyEnumerator 코드를 my_enumerator.rb 파일에 저장한 후 다음 그림처럼 irb를 실행하여 테스트해 보자.
배열에 대해 each와 map 메서드 모두 인덱스가 블록에 잘 전달되는 것을 볼 수 있다.

아래 그림을 보면 해시 역시 each와 map 메서드 모두 인덱스가 블록에 잘 전달되는 것을 볼 수 있다.

그런데 each_with_index나 each_with_object 메서드처럼 블록 파라미터로 값을 두 개 전달해 주는 메서드의 경우에는 Enumerator의 with_index 메서드가 어떻게 동작할까?
아래 그림의 실행 결과를 보면 블록의 첫 번째 파라미터로 배열을 받는데, 그 배열은 ["a", "b"] 배열에 대해 호출된 each_with_index 메서드가 블록에 전달하는 두 개의 값(요소와 인덱스)이 배열로 묶인 것이다.
그리고 두 번째 파라미터로 실제 with_index 메서드 안에서 전달하는 인덱스가 들어온다.

그렇다면 동일한 테스트에 대해 MyEnumerator 클래스의 with_index 메서드는 어떻게 동작하는지 확인해 보자.
아래 그림을 보면 Enumerator 클래스로 테스트했을 때와 실행 결과가 다른데, 아무래도 ["a", "b"] 배열의 each_with_index 메서드가 블록에 전달해 준 인덱스가 누락된 것 같다.

아래 with_index 메서드의 코드를 보면 그 이유를 알 수 있는데, with_index 메서드 안에서 작성한 블록을 보면 파라미터가 e 하나만 있다.
그래서 ["a", "b"] 배열의 each_with_index 메서드가 블록에 넘겨준 두 개의 값 중 하나(요소)만 받은 것이다.

class MyEnumerator
  ...생략

  def with_index(&block)
    i = -1
    @object.send(@method, *@method_args) do |e|
      i += 1
      block.call(e, i)
    end
  end
end

아래 그림처럼 메서드의 파라미터 이름 앞에 *을 붙여 선언하면 메서드 호출 시 가변 인수를 전달할 수 있다는 걸 알고 있다.
블록도 메서드와 마찬가지로 동일한 방식으로 파라미터를 선언하여 가변 인수를 받을 수 있다.

아래 코드처럼 블록 파라미터 e 앞에 *을 붙여 가변 인수를 받을 수 있게 변경하자.

class MyEnumerator
  ...생략
  
  def with_index(&block)
    i = -1
    @object.send(@method, *@method_args) do |*e|
      i += 1
      block.call(e, i)
    end
  end
end

변경한 코드를 my_enumerator.rb 파일에 저장한 후 다시 테스트를 해 보면 배열의 each_with_index 메서드가 블록에 넘겨주는 인덱스가 누락되지 않고 잘 전달되는 것을 볼 수 있다.

그런데 아래 그림처럼 each 메서드에 대해 다시 테스트를 해보면 기존에 테스트했을 때와 달리 요소 하나가 배열에 담긴 채로 넘어오는 걸 볼 수 있다.

each 메서드처럼 요소 하나만 블록으로 넘어오는 경우에도 가변 인수로 처리되어 요소 하나가 배열에 담기고 그 배열을 그대로 블록에 넘기기 때문이다.
그렇다면 파라미터 e가 참조하는 배열의 크기가 1보다 크다면, 즉 값을 두 개 이상 받았으면 e를 배열 그대로 블록에 넘기고, 그렇지 않으면 배열 e의 첫 번째 요소를 넘기도록 코드를 수정해 보자.

class MyEnumerator
  ...생략
  
  def with_index(&block)
    i = -1
    @object.send(@method, *@method_args) do |*e|
      i += 1
      if e.size > 1
        block.call(e, i)
      else
        block.call(e[0], i)
      end
    end
  end
end

변경한 코드를 my_enumerator.rb 파일에 저장하고 irb를 실행하여 배열의 each와 each_with_index 메서드에 대해 MyEnumerator 클래스의 with_index 메서드를 다시 테스트해 보자.
아래 그림을 보면 두 메서드 모두 Enumerator 클래스와 동일하게 잘 동작하는 걸 볼 수 있다.

이제 MyEnumerator 클래스에 with_object 메서드를 추가해 보자. 
아래 코드를 보면 with_index 메서드의 코드와 별반 다르지 않은데, 인덱스 대신 인수로 받은 객체를 블록에 넘겨주고 메서드의 최종 결괏값으로 그 객체를 그대로 반환하고 있다.

class MyEnumerator
  ...생략
  
  def with_object(obj, &block)
    @object.send(@method, *@method_args) do |*e|
      if e.size > 1
        block.call(e, obj)
      else
        block.call(e[0], obj)
      end
    end
    obj
  end
end

추가한 코드를 my_enumerator.rb 파일에 저장한 후 irb를 실행하여 MyEnumerator 클래스의 with_object 메서드를 배열의 each와 with_index 메서드에 대해 테스트해 보자.

아래 Enumerator 클래스로 테스트를 진행한 것과 동일한 결과가 나오는 것을 볼 수 있다.

 

See you again~~