본문 바로가기
C#/Effective C#

[Effective C#] Item19 런타임에 타입을 확인하여 최적의 알고리즘을 사용하라

by 코모's 2022. 10. 4.
반응형

제네릭을 활용하면 코드를 덜 작성해도 되기 때문에 매우 유용하지만 타입이나 메서드를 제네릭화 하면 구체적인 타입이 주는 장점을 읽고 타입의 세부적인 특징을 고려한 최적화한 알고르즘도 사용할 수 없다.

 

그래서 만약 어떤 알고리즘이 특정 타입에 대해 더 효울적으로 동작한다고 생각된다면 그냥 그 타입을 이용하도록 작성하는 것도 좋다. 직전 Item 때 러럼 제약 조건을 설정하는 방법도 있지만 제약 조건이 항상 능사는 아니다.

 

특정 타입의 시퀀스를 역순으로 순회하기 위해서 다음과 같이 클래스를 만들었다.

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>만을 구현한 컬렉션들에 대해서는 여전히 비효율적으로 동작한다.

 

GetEnumaerator()를 살표보면 입력 시퀀스가 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> 내에서 수행된느 매게변수에 대한 테스트는 모두 런타임 에서 이루어 지낟.

즉, 추가 기능을 확인하는 과정도 일정 부분 비용이 발생하지만 대부분의 경우에는 이 비용은 모든 요소를 복사하는 것에 비해 훨씬 적다.

반응형