[Effective C# 개정판 3판 19장] 런타임에 타입을 확인하여 최적의 알고리즘을 사용하라

1. 참조

2. 소개

  • 제네릭을 활용하면 코드를 덜 작성해도 되기 때문에 매우 유용하지만 타입이나 메서드를 제너릭화하면 구체적인 타입이 주는 장점을 잃고 타입의 세부적인 특징을 고려한 최적화한 알고리즘도 사용할 수 없습니다.
  • 만약 어떤 알고리즘이 특정 타입에 대해 더 효율적으로 동작한다고 생각되다면, 그냥 그 타입을 이용하도록 작성하는 것이 좋습니다.
  • 제약 조건을 설정하는 방법도 있지만, 제약 조건이 항상 능사는 아닙니다.

3. 제네릭을 사용할 때 가장 좋은 조건

  • 반복적인 소스코드가 들어 있는 경우
  • 각각 개별 타입에 대한 고유한 특성을 고려하고, 그 특화된 기능들을 살릴수 있는 경우
  • 타입에 따라 내부 기능에 이상이 없는 경우
  • 다양한 상황에 대처할 필요가 없을 경우
  • 위의 4가지 경우를 제외한 반대의 경우에는 제네릭을 사용하지 않는 편이 좋습니다.

4. 예제

  • 특정 타입의 시퀀스를 역순으로 순회하기 위해서 다음과 같이 클래스를 만들었습니다.
using System.Collections;
using System.Collections.Generic;


namespace Chapter  
{  
class Program  
{  
static void Main(string\[\] args)  
{  
}  
}

public sealed class ReverseEnumerable<T> : IEnumerable<T>
{
    private class ReverseEnumerator<T> : IEnumerator<T>
    {
        int currentIndex;
        IList<T> collection;

        public ReverseEnumerator(IList<T> srcCollection)
        {
            collection = srcCollection;
            currentIndex = collection.Count;
        }

        public T Current => collection[currentIndex];

        public void Dispose()
        {
            // 세부 구현 내용은 생략했으나 반드시 구현해야 한다.
            // 왜냐하면 IEnumerator<T>는 IDisposable을 상속하고 있기 때문이다.
            // 이 클래스는 sealed 클래스로 선언되었으므로
            // protected Dispose() 메서드는 필요 없다.
        }

        //IEnumerator 멤버
        object IEnumerator.Current => this.Current;
        public bool MoveNext() => --currentIndex >= 0;
        public void Reset() => currentIndex = collection.Count;
    }

    IEnumerable<T> sourceSequence;
    IList<T> originalSequence;

    public ReverseEnumerable(IEnumerable<T> sequence)
    {
        sourceSequence = sequence;
    }

    // IEnumerable<T> 멤버
    public IEnumerator<T> GetEnumerator()
    {
        // 역순으로 순회하기 위해서
        // 원래 시퀀스를 복사한다.
        if (originalSequence == null)
        {
            originalSequence = new List<T>();
            foreach (T item in sourceSequence)
                originalSequence.Add(item);
        }
        return new ReverseEnumerator(originalSequence);
    }

    // IEnumerable 멤버
    IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
 }
}
  • 위 코드는 잘 동작하는 편이고 랜덤 액세스를 지원하지 않는 컬렉션에 대해서 개별 요소를 역순으로 순회하기 위한 유일한 방법이기도 합니다.
  • 하지만 대부분의 컬렉션들이 랜덤 액세스를 지원하기 때문에 이와 같은 코드는 매우 비효율적입니다.
  • 생성자로 전달한 인자가 IList<T>를 지원한다면 이처럼 복제본을 만들 이유가 없습니다.
  • IEnumerable<T> 를 구현하고 있는 대부분의 타입들이 IList<T> 또한 구현한다는 사실에 착안하여 코드를 좀더 효율적으로 개선할 수 있습니다.

public ReverseEnumerable(IEnumerable sequence)  
{  
sourceSequence = sequence;

// 만약 sequence가 IList<T>를 구현하지 않았다면
// originalSequence가 null이 되지만
// 문제되지 않습니다.
originalSequence = sequence as IList<T>;

}
  • 그리고 매개변수가 IList<T> 타입인 것을 컴파일타임에 알 수 있는 생성자 를 하나 더 추가할 수 있습니다.

public ReverseEnumerable(IEnumerable sequence)  
{  
sourceSequence = sequence;

// 만약 sequence가 IList<T>를 구현하지 않았다면
// originalSequence가 null이 되지만
// 문제되지 않습니다.
originalSequence = sequence as IList<T>;

}

public ReverseEnumerable(IList sequence)  
{  
sourceSequence = sequence;  
originalSequence = sequence;  
}
  • 이제 IList<T>를 사용하면 IEnumerable<T> 만을 사용할 때 보다 더 효율적으로 동작하도록 개선할 수 있습니다.
  • 하지만 IList<T> 를 구현하지 않고 ICollection<T> 만을 구현한 컬렉션들에 대해서는 여전히 비효율적으로 동작합니다.
  • GerEnumerator()를 살펴보면 입력 시퀀스가 ICollection<T> 만을 구현한 경우 입력 시퀀스에 대한 복제본을 생성해야 하므로 매우 느리게 동작할 수 밖에 없습니다.
  • 따라서, 런타임 중에 타입을 확인하고 ICollection<T> 가 제공하는 Count 속성을 활용하여 저장소 공간을 미리 초기화하도록 코드를 조금 개선해야 합니다.

// IEnumerable 멤버  
public IEnumerator GetEnumerator()  
{  
// string은 매우 특별한 경우입니다.  
if(sourceSequence is string)  
{  
// 컴파일타임에 T는 char가 아닐 것이므로  
// 캐스트에 주의해야 합니다.  
return new ReverseEnumerator(sourceSequence as string) as IEnumerator;  
}

// 역순으로 순회하기 위해서
// 원래 시퀀스를 복사한다.
if (originalSequence == null)
{
    if (sourceSequence is ICollection<T>)
    {
        ICollection<T> source = sourceSequence as ICollection<T>;
        originalSequence = new List<T>(source.Count);
    }
    else
    {
        originalSequence = new List<T>();
    }

    foreach (T item in sourceSequence)
        originalSequence.Add(item);
}
return new ReverseEnumerator(originalSequence);

}
  • ReverseEnumerable<T> 내에서 수행되는 매개변수에 대한 테스트는 모두 런타임 에 이루어집니다.
  • 즉, 추가 기능을 확인하는 과정도 일정 부분 비용이 발생하지만 대부분의 경우에는 이 비용은 모든 요소를 복사하는 것에 비해 훨씬 적습니다.



5. 정리

  • 타입에 대한 제약 조건을 거의 사용하지 않으면서도 타입 매개변수로 지정될 가능성이 있는 타입들의 고유한 특성을 고려하고 특화된 기능들을 최대한 활용하여 제네릭 타입을 만드는 방법을 살펴 보았습니다.
  • 이와 같은 코드를 작성하면 재사용성이 높으면서도 개별 타입에 최적화된 코드 작성이 가능합니다.
    ```
728x90

이 글을 공유하기

댓글

Designed by JB FACTORY