본문 바로가기
책/Effective C#

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

by 코딩하는 돼징 2023. 12. 10.
반응형

만약 어떤 알고리즘이 특정 타입에 대해 더 효율적으로 동작한다고 생각된다면 그냥 그 타입을 이용하도록 코드를 작성하자. 이를 위해 제약 조건을 설정하는 것은 항상 효과적인 방법은 아니다. 제네릭의 인스턴스화는 런타임의 타입을 고려하지 않으며 컴파일타임의 타입만을 고려한다. 효율적인 코드를 작성하려면 이러한 사실을 반드시 알고있어야 한다.


컴파일 타임(Compile Time)

컴파일러에 의해 소스코드에서 목적코드(기계어 또는 중간코드)로 변환되는 동안을 가리킨다. 컴파일 타임에는 코드의 구문분석, 타입검사, 최적화등이 이루어진다. 제네릭의 인스턴스화는 주로 컴파일 타임에 이루어진다. 코드가 실행되기 전에 컴파일러는 제네릭 코드를 실제 타입으로 변환하고 최적환한다.

public class MyPiggy<T>
{
    public void PiggyType()
    {
        Console.WriteLine(typeof(T));
    }
}

class Program
{
    static void Main()
    {
        // 컴파일 타임에MyPiggy<int>로 인스턴스화
        MyPiggy<int> intInstance = new MyPiggy<int>();
        intInstance.PiggyType(); // 출력 결과: System.Int32
    }
}

 

런타임(RunTime)

코드가 이미 컴파일되어 실행 파일로 만들어진 후에 실제로 프로그램이 실행되는 시간이다. 런타임에는 메모리에 프로그램이 로드되고 사용자 입력에 응답하며 데이터를 처리하고 함수 및 메서드가 호출되는 등 동작이 수행된다.


그러므로 제네릭의 인스턴스화가 "컴파일 타임의 타입"을 고려한다는 것은 제네릭 코드가 컴파일시에 구체적인 타입 정보를 받아들이고 그에 따라 컴파일러가 특정 타입에 맞게 최적화된 코드를 생성한다는 것이다. 이는 런타임에서의 타입 정보와는 독립적으로 이루어진다.

즉 런타임의 타입은 프로그램이 실행 중일때 변수 또는 객체의 실제 타입을 나타내며 컴파일 타임의 타입은 코드가 컴파일될 때 제네릭 코드에 대한 구체적인 타입을 나타낸다.


01 타입 매개변수에 대한 가정이 없는 경우

기존 코드는 인자로 받은 IEnumerable<T> 타입의 sequence를 sourceSequence에 할당하고, 필요할 때 originalSequence를 생성하여 사용한다. 모든 IEnumerable<T>를 IList<T>로 복사하고 새로운 리스트를 만든다.

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

02 캐스팅 추가

IEnumerable<T>를 IList<T>로 캐스팅하여 originalSequence에 할당한다. 이 경우 입력으로 받은 sequence가 IList<T>를 구현하고 있다면 originalSequence에 할당되며 그렇지 않다면 originalSequence는 null로 남게 된다. 복제하지 않고 기존 시퀀스를 직접 참조한다.

public ReverseEnumerable(IEnumerable<T> sequence)
{
    sourceSequence = seuqence
    originalSequence = sequene as IList<T>
}

03 생성자 타입 변경

생성자의 인자를 IList<T>로 변경하여 인자로 받은 sequence를 sourceSequence 및 originalSequence에 직접 할당한다. 이는 입력으로 받은 시퀀스가 IList<T>를 구현하든지 구현하지 않든지에 상관없이 바로 할당된다. 이로써 별도의 캐스팅 없이 sequence를 IList<T>로 참조한다.

public ReverseEnumerable(IList<T> sequence)
{
    sourceSequence = seuqence
    originalSequence = sequene
}

이렇게 변경을 하게 되면 매개변수가 IList<T>타입인 것을 컴파일타임에 알 수 있으므로 구현이 더 용이할 것 같지만 몇몇 경우에 제대로 동작하지 않을 수 있다.

 

예를 들어 어떤 객체는 런타임시에 IList<T>객체라 하더라도 컴파일 타임에는 IEnumerable<T>로 간주되는 경우가 있기 때문에 이를 대비하여 IList<T>타입 매개변수를 취하는 생성자 오버로드 메서드외에도 런타임 타입을 확인하도록 하는 코드를 작성해야 한다.

public ReverseEnumerable(IEnumerable<T> sequence)
{
    sourceSequence = seuqence
    originalSequence = sequene as IList<T>
}
public ReverseEnumerable(IList<T> sequence)
{
    sourceSequence = seuqence
    originalSequence = sequene
}

다른 예들

01 입력 시퀀스가 ICollection을 구현하는 경우 Count속성을 활용하여 초기화

입력시퀀스가 ICollection<T>만을 구현한 경우 입력 시퀀스에 대한 복제본을 생성해야 하므로 느리게 동작할 수 밖에 없다. 그러므로  IEnumerable가 ICollection을 구현하고 있다면 Count 속성을 이용하여 새로운 리스트를 미리 초기화한다. 이로써 원본 시퀀스의 길이를 알고 있으므로 미리 리스트의 용량을 할당하여 성능을 향상시킬 수 있다.

public ReverseEnumerable(IEnumerable<T> sequence)
{
    // ICollection을 구현하는 경우
    if (sourceSequence is ICollection<T> collection)
    {
        ICollection<T> source = sourceSequence as ICollection<T>;
        originalSequence = new List<T>(source.Count);
    }
    else
    {
        // ICollection을 구현하지 않는 경우
        originalSequence = new List<T>();
    }
}

02 string

string은 마치 IList<char>를 구현한 것처럼 랜덤 액세스가 가능하지만  실제로 구현한 것은 아니다. 이를 활용하면 코드를 개선할 수 있다.

public ReverseEnumerable(IEnumerable<T> sequence)
{
    // 입력 시퀀스가 string인 경우
    if (sourceSequence is string)
    {
        return new ReverseStringEnumerator(sourceSequence is string) as IEnumerator<T>;
    }
}

결론

제약 조건을 거의 사용하지 않아도 각 타입들의 고유한 특성을 이용해 재사용성이 높으면서도 개별타입에 최적화된 코드를 작성할 수 있다.

 

 

 

 

본 게시글은 Effective C#을 읽고 정리하였습니다.

 

 

반응형

댓글