[Effective C# 개정판 3판 19장] 런타임에 타입을 확인하여 최적의 알고리즘을 사용하라
- C#/Effective C# 책 정리
- 2021. 6. 11. 21:28
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
'C# > Effective C# 책 정리' 카테고리의 다른 글
[Effective C#] Item 23 - 타입 매개변수에 대해 메서드 제약 조건을 설정하려면 델리게이트를 활용하라 (0) | 2021.08.07 |
---|---|
[Effective C# 21장] 타입 매개변수가 IDisposable을 구현한 경우를 대비하여 제네릭 클래스를 작성하라 (0) | 2021.07.14 |
[Effective C# 개정판 3판 ] 17장 표준 Dispose 패턴을 구현하라 (0) | 2021.06.03 |
[Effective C# item 2] const보다는 readonly가 좋다 (0) | 2021.05.22 |
[Effective C# item 15] 불필요한 객체를 만들지 말라 (0) | 2021.05.21 |
이 글을 공유하기