[31] 시퀀스에 사용할 수 있는 조합 가능한 API를 작성하라

다음 본문은 도서 이펙티브 C# (빌 와그너)에서 나오는 주제를 다룹니다.

 

 

함수를 작성할 때 함수 속에서 foreach, for, while등의 반복문을 사용하여

컬렉션을 다루는 경우는 흔히 있다.

 

이 경우 각각의 함수마다 컬렉션의 일부를 필터링하거나 내용을 수정하거나 하는 등의 작업 후,

이를 다시 반환하는 식으로 코드를 작성할 것이다.

 

하지만 대부분의 경우 이러한 작업들은 단일 작업이 아니라 여러 작업을 거쳐야 하며,

단계를 거칠 때마다 중간 결과를 저장하기 위해서 추가적으로 메모리 공간이 필요하기도 하며,

매번 전체 컬렉션을 순회하기 때문에 큰 비용이 발생하게 된다.

 

그렇다면 이러한 함수들을 하나의 루프 내에서 수행되도록 병합해버린다면 어떨까?

특정한 상황에서만 사용할 수 있는 거대한 함수가 만들어지며,

이러한 함수는 재사용 가능성이 없어 범용성이 떨어지게 된다.

 

이럴 때 이터레이터 메서드를 만들어 볼 수 있다.

 

1. 이터레이터 메서드

단일의 시퀀스 IEnumerable<T>로 표현되는 입력을 취하고,

결과로도 단일의 시퀀스 IEnumerable<T>를 반환하는 메서드를 말한다.

 

이러한 메서드를 작성할 때 yield return을 사용하면 메서드 내에서 시퀀스 내의 개별 요소를 저장할 필요가 없다.

출력 결과가 필요한 시점에 출력 시퀀스로 결과를 내보내기 때문.

 

여러 이터레이터 메서드를 조합하면 이터레이터의 특징인 지연 수행 모델 덕분에

전체 시퀀스를 한 번만 순회하면서 조합된 메서드 세트를 수행하니 런타임 효율이 개선된다.

 

1-1) 일반적인 메서드

void Start()
{
    List<int> myList = new List<int>() {1,2,3,4};
    Unique(myList);
}

public static void Unique(IEnumerable<int> nums)
{
    var uniqueVals = new HashSet<int>();

    foreach (var num in nums)
    {
        if(!uniqueVals.Contains(num))
        {
            uniqueVals.Add(num);
            Console.WriteLine(num);
        }
    }
}

 

1-2) 이터레이터 메서드로 변환.

 - 반환형이 IEnumerable<int> 면서 yield return을 통해 각 순회를 반환하도록 작성한다.

 - 하지만 오히려 이터레이터를 쓰고나니 복잡해진 느낌이 들 것이다.

void Start()
{
    List<int> myList = new List<int>() {1,2,3,4};

    foreach (var item in Unique2(myList))
        Console.WriteLine(item);
}

public static IEnumerable<int> Unique2(IEnumerable<int> nums)
{
    var uniqueVals = new HashSet<int>();

    foreach (var num in nums)
    {
        if (!uniqueVals.Contains(num))
        {
            uniqueVals.Add(num);
            Console.WriteLine($"순회 중.. 이번값은 {num}");
            yield return num;
        }
    }
}

 - 출력결과를 보면 이터레이터 메서드의 특이점을 볼 수 있다.

   미리 순회하고 값을 넘긴 것이 아니라 순회를 도는 시점에서야 각 단계가 수행되고 있다!

 

 

2. 이터레이터 메서드 조합

 다음과 같이 제곱을 만드는 이터레이터 메서드를 만들고, 기존 출력 메서드와 결합해보자. 

void Start()
{
    List<int> myList = new List<int>() {1,2,3,4};
    foreach (var item in Square(Unique(myList)))
        Debug.Log(item);
}

public static IEnumerable<T> Unique<T>(IEnumerable<T> nums)
{
    var uniqueVals = new HashSet<T>();

    foreach (var num in nums)
    {
        if (!uniqueVals.Contains(num))
        {
            uniqueVals.Add(num);
            yield return num;
        }
    }
}

public static IEnumerable<int> Square(IEnumerable<int> nums)
{
    foreach (var num in nums)
        yield return num * num;
}

 

3. 복수의 입력시퀀스를 사용한 이터레이터 메서드

 일반적으로는 한개의 입력시퀀스로 한개의 출력 시퀀스를 작성하지만

 복수의 IEnumerable로부터 각각 개별 요소를 가져와서 출력할 수도 있다.

 다음은 두 개의 시퀀스를 연결시킨 string을 반환하는 이터레이터 메서드.  

public static IEnumerable<string> Zip(IEnumerable<string> first, IEnumerable<string> second)
{
    using(var firstSequence = first.GetEnumerator())
    {
        using(var secondSequence = second.GetEnumerator())
        {
            while(firstSequence.MoveNext() && secondSequence.MoveNext())
                yield return $"{firstSequence.Current} {secondSequence.Current}";
        }
    }
}

 

결론

 범용 컬렉션 메서드를 만들때는 이터레이터 메서드를 활용해보자.

 

 

댓글

Designed by JB FACTORY