[Effective C#] Item 19: 런타임에 타입을 확인하여 최적의 알고리즘을 사용하라
아이템 19: 런타임에 타입을 확인하여 최적의 알고리즘을 사용하라
제네릭 타입의 경우 타입 매개변수에 새로운 타입을 지정하여 손쉽게 재사용할 수 있다.
타입 매개변수에 새로운 타입을 지정한다는 것은 유사한 기능을 가진 새로운 타입을 생성한다는 것을 의미한다.
제네릭을 활용하면 코드를 덜 작성해도 되기 때문에 유용하다.
하지만 타입이나 메서드를 제네릭화하면 구체적인 타입이 주는 장점을 잃고 타입의 세부적인 특성까지 고려하여 최적화한 알고리즘을 사용할 수 없게 된다.
C#
은 이헌 부분까지 고려하여 설계되었다.
만약 어떤 알고리즘이 특정 타입에 대해 더 효율적으로 동작한다고 생각된다면 그냥 그 타입을 이용하도록 코드를 작성해야 한다.
이를 위해 제약 조건을 설정하는 것이 항상 효과적인 방법은 아니다.
제네릭의 인스턴스화는 런타임을 고려하지 않으며 컴파일타임의 타입만을 고려한다.
특정 타입의 시퀀스를 역순으로 순회하기 위해서 다음과 같이 클래스를 만들었다고 가정해보자.
public sealed class ReverseEnumerable<T> : IEnumerable<T>
{
private class ReverseEnumerator : IEnumerator<T>
{
int currentIndex;
IList<T> collection;
public ReverseEnumerator(IList<T> srcCollection)
{
collection = srcCollection;
currentIndex = collection.Count;
}
// IEnumerator<T> 멤버
public T Current => collection[currentIndex];
// IDisposable 멤버
public void Dispose()
{
// 세부 구현 내용은 생략했으나 반드시 구현해야 한다.
// 왜냐하면 IEnumerator<T>는 IDisposable을 상속받기 때문이다.
// 이 클래스는 sealed로 선언되었으므로
// protected Dispose() 메서드는 필요 없다.
}
// IEnumerator 멤버
object System.Collections.IEnumerator.Current => Current;
public bool MoveNext() => --currentIndex >= 0;
public void Reset() => currentIndex = collection.Count;
}
IEnumerable<T> sourceSequence;
IList<T> originalSequence;
public ReverseEnumerable(IEnumerable<T> srcSequence)
{
sourceSequence = srcSequence;
}
// IEnumerable<T> 멤버
public IEnumerator<T> GetEnumerator()
{
// 역순으로 순회하기 위해서
// 원래 시퀀스를 복사한다.
if (originalSequnce == null)
{
originalSequence = new List<T>();
foreach (T item in sourceSequence)
{
originalSequence.Add(item);
}
}
return new ReverseEnumerator(originalSequence);
}
// IEnumerable 멤버
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => this.GetEnumerator();
}
이 코드를 살펴보면 타입 매개변수에 대한 가정이 거의 없다.
ReversEnumerable의 생성자에서 입력 매개변수가 **IEnumerable
IEnumerable
따라서 개별 요소들을 역순으로 순회하는 유일한 방법으로 생각되는 내용이 ReverseEnumerable
*만약 여기서 전달받은 IEnumerable
이 코드는 랜덤 액세스를 지원하지 않는 컬렉션에 대해서 개별 요소를 역순으로 순회하기 위한 유일한 방법이다.
하지만 대부분의 컬렉션은 랜덤 액세스를 지원하기 때문에 이와 같은 코드는 상당히 비효율적이다.
- 랜덤 액세스: 랜덤 액세스는 데이터를 저장하는 블록을 한 번에 여러 개 액세스 하는 것이 아니라 한 번에 하나의 블록만을 액세스 하는 싱글 블록 I/O 방식이다.
따라서 생성자로 전달한 인자가 IList
IEnumerable
public ReverseEnumerable(IEnumerable<T> srcSequence)
{
// 만약 sequence가 IList<T>를 구현하지 않았다면
// originalSequence는 null이 되지만
// 문제되지 않는다.
sourceSequence = srcSequence;
originalSequence = srcSequence as IList<T>;
}
IList
이렇게 하면 매개변수가 IList
하지만 이렇게 코드를 변경하게 된다면 몇몇 경우는 제대로 동작하지 않을 수 있다.
예를 들어 어떤 객체는 런타임 시에는 IList
이러한 경우를 대비하려면 IList
public ReverseEnumerable(IEnumerable<T> srcSequence)
{
sourceSequence = srcSequence;
// 만약 Sequence가 IList<T>를 구현하지 않았다면
// originalSequence는 null이 되지만
// 문제되지 않는다.
originalSequence = srcSequence as IList<T>;
}
public ReverseEnumerable(IList<T> srcSequence)
{
sourceSequence = srcSequence;
originalSequence = srcSequence;
}
이제 List
사용자에게 더 많은 기능을 제공하는 것은 아니지만 더 효율적으로 동작하도록 성능을 개선할 수 있다.
이와 같이 코드를 변경하면 대부분의 경우 성능이 개선되지만, IList
public IEnumerator<T> GetEnumerator()
{
// 역순으로 순회하기 위해서
// 원래 시퀀스를 복사한다.
if (originalSequnce == null)
{
originalSequence = new List<T>();
foreach (T item in sourceSequence)
{
originalSequence.Add(item);
}
}
return new ReverseEnumerator(originalSequence);
}
입력 시퀀스가 ICollection
다음은 ICollection
public IEnumerator<T> GetEnumerator()
{
// string은 매우 특별한 경우다.
if (sourceSequence is string)
{
// 컴파일타입에는 T는 char가 아닐 것이므로
// 캐스트에 주의해야 한다.
return new ReverseEnumerator(sourceSequence as string) as IEnumerator<T>;
}
// 역순으로 순회하기 위해서
// 원래 시퀀스를 복사한다.
if (originalSequnce == 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);
}
이 코드는 입력 시퀀스로부터 리스트를 생성하는 List
List<T>(IEnumerable<T> inputSequence);
실제 string의 정의를 살펴보면 IEnumerable
public sealed partial class String : System.Collections.Generic.IEnumerable<char>
ReverseEnumerable
string은 마치 IList
string이 제공하는 고유의 메서드를 사용하려면 제네릭 클래스 내에 string에 특화된 코드를 작성해야만 한다.
다음은 ReverseEnumerable
private sealed class ReverseStringEnumerator : IEnumerator<char>
{
private string sourceSequence;
private int currentIndex;
public ReverseStringEnumerator(string srcString)
{
sourceSequence = srcString;
currentIndex = sourceString.Length;
}
// IEnumerator<char> 멤버
public char Current => sourceString[currentIndex];
// IDisposable 멤버
public void Dispose()
{
// 세부 구현 내용은 생략했으나 반드시 구현해야 한다.
// 왜냐하면 IEnumerator<T>는 IDisposable을 상속받기 때문이다.
// 이 클래스는 sealed로 선언되었으므로
// protected Dispose() 메서드는 필요 없다.
}
// IEnumerator 멤버
object System.Collections.IEnumerator.Current => Current;
public bool MoveNext() => --currentIndex >= 0;
public void Reset() => currentIndex = sourceString.Length;
}
이 코드를 제대로 활용하려면 ReverseEnumerable
public IEnumerator<T> GetEnumerator()
{
// string은 매우 특별한 경우다.
if (sourceSequence is string)
{
// 컴파일타입에는 T는 char가 아닐 것이므로
// 캐스트에 주의해야 한다.
return new ReverseStringEnumerator(sourceSequence as string) as IEnumerator<T>;
}
// 역순으로 순회하기 위해서
// 원래 시퀀스를 복사한다.
if (originalSequnce == 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);
}
이 코드의 목표는 제네릭 클래스 내에 타입별로 구현해야 하는 부분을 잘 숨겨두는 것이다.
string의 경우 완전히 다른 구현 방식이 필요했기 때문에 추가 작업이 조금 많아지긴 했다.
제네릭을 정의할 때 우리가 알고 있는 것을 컴파일러가 빠짐없이 모두 이해하고 있으리라고 가정해서는 안 된다.
간단한 에를 통해서 타입에 대한 조건을 거의 사용하지 않으면서도 타입 매개변수로 지정될 가능성이 있는 타입들의 고유한 특성을 고려하고 특화된 기능들을 최대한 활용하여 제네릭 타입을 만드는 방법을 알아봤다.
댓글남기기