서론 : 제네릭이 동작하는 방식
C#컴파일러가 타입 안정성을 미리 체크,
JIT컴파일러는 이를 기반으로 최적화된 머신 코드를 생성
//제네릭 T가 참조 타입인 경우 동일한 IL을 가짐
//string도 참조 타입이다
List<string> strList = new List<string>();
List<Stream> strList = new List<Stream>();
List<MyClass> strList = new List<MyClass>();
제네릭 T가 값 타입인 경우엔 다른 IL을 생성
선언된 클래스가 로드되는 순간 번역되는 것이 아닌,
해당 제네릭 T가 최초로 호출되는 타입일 경우(특정 메소드에서 최초 호출 시) 새로운 기계어를 생성한다.
제네릭 클래스를 선언하는경우 매번 다른 형태마다 내부의 함수가 불릴 때 하나씩 만듬
일반 클래스에 있는 제네릭 메소드의 경우 불릴때 jit컴파일러가 번역한다.
해당 메소드만 번역하기 때문에 해당 번역내용이 많지 않아 성능저하가 크지 않다.
*메모리 풋프린트 : 실행될 때 프로그램이 차지하는 메모리 공간 크기
아이템 19 런타임에 타입을 확인하여 최적의 알고리즘을 사용하라
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace EffectiveCSharp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
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 => 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;
//}
//아래로 개선
public ReverseEnumerable(IEnumerable<T> sequence)
{
sourceSequence = sequence;
originalSequence = sequence as IList<T>;
}
// IEnumerable<T> 멤버
public IEnumerator<T> GetEnumerator()
{
//string은 특별한 경우
if(sourceSequence is string)
{
return new ReverseStringEnumerator(sourceSequence as string) as IEnumerator<T>;
}
// 역순으로 순회하기 위해서 원래 시퀀스를 복사한다.
if (originalSequence == null)
{
//originalSequence = new List<T>();
//foreach (T item in sourceSequence)
// originalSequence.Add(item);
//아래로 개선
ICollection<T> source = sourceSequence as ICollection<T>;
originalSequence = new List<T>(source.Count);
}
else
originalSequence = new List<T>();
return new ReverseEnumerator(originalSequence);
}
// IEnumerable 멤버
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => this.GetEnumerator();
}
private sealed class ReverseStringEnumerator : IEnumerator<char>
{
private string sourceSequence;
private int currentIndex;
public ReverseStringEnumerator(string source)
{
sourceSequence = source;
currentIndex = source.Length;
}
public char Current => sourceSequence[currentIndex];
public void Dispose()
{
//반드시 구현필요 (내용 생략)
}
//IEnumerator 멤버
object IEnumerator.Current => sourceSequence[currentIndex];
public bool MoveNext() => --currentIndex >= 0;
public void Reset() => currentIndex = sourceSequence.Length;
}
}
}
위는 랜덤 액세스를 지원하지 않는 컬렉션을 대비해, 기존 컬렉션을 역순조회를 위해 컬렉션을 역순으로 복사후 반환하는 비효율적 구조 예시
*public interface IList<T> : ICollection<T>, IE numerable<T>, IEnumerable
IEnumerator<T>는 역순조회를 지원하지 않지만 IList<T>는 지원함, 대부분의 경우 IList<T>를 구현하기 때문에, 위와같이 as로 생성자에서 IList<T>의 경우 그대로 반환하도록 하면된다.
직접 작성할 일은 없겠지만, 제네릭 컬렉션 구조의 학습에 중요한 부분같아서 남겨둠.
IList, IEnumerable, ICollection 등 컬렉션 인터페이스들 학습후 복습 예정.
결론 : 특정 알고리즘에 유리한 타입을 이용하는 것이 좋음
아이템 21 타입 매개변수가 IDisposable을 구현한 경우를 대비하여 제네릭 클래스를 작성하라
public interface IEngine
{
void Dowork();
}
public class EngineDriverOne<T> where T : IEngine, new()
{
//T가 IDisposable을 구현하는경우 해제에 대한 부분이 없어서 메모리 누수가 발생할 수 있음.
public void GetThingsDone()
{
T driver = new T();
//driver.Dowork();
//Dispose()를 호출해주기 위해 using블록 사용
using(driver as IDisposable)
{
driver.Dowork();
}
}
}
Lazy<T> : 이거 따로 알아보기 new T()도
멤버의 특정 값이 Disposable을 구현하는 경우
public sealed class EngineDriver2<T> : IDisposable where T : IEngine, new()
{
private Lazy<T> driver = new Lazy<T>(() => new T());
public void Dispose()
{
if(driver.IsValueCreated)
{
var resource = driver.Value as IDisposable;
resource?.Dispose();
}
}
}
아이템 24 : 베이스 클래스나 인터페이스에 대해서 제네릭을 특정하지 말라
베이스 클래스나 인터페이스로 제네릭 메서드를 오버로딩할 경우, 명시적 변환을 해줘야 오버로딩된 메서드가 호출된다는 것을 보여준다.
static void WriteMessage<T>(T obj)
{
// 내용
}
static void WriteMessage(MyBase b) // 베이스 클래스로 오버로딩
{
// 내용
}
static void WriteMessage(IMessageWriter obj) // 인터페이스로 오버로딩
{
// 내용
}
// MyBase 를 상속
public class MyDerived : MyBase
{
// 내용
}
// IMessageWriter를 구현
public class MyMessageWriter : IMessageWriter
{
// 내용
}
// 두가지 경우 모두 WriteMessage<T>(T obj)가 호출된다.
WriteMessage(new MyDerived());
WriteMessage(new MyMessageWriter ());
// 명시적 형변환을 해줘야 오버로딩된 메서드가 호출된다.
WriteMessage(new MyDerived() as MyBase);
WriteMessage(new MyMessageWriter () as IMessageWriter);
해당 예제처럼 명시적으로 형변환을 하지 않는 경우에 제네릭 클래스가 선택되기 때문에 잘못된 사용을 할 수 있다.
따라서 베이스 클래스나 인터페이스에 대하여 제네릭을 특정해서 사용하는 것을 지양한다.
만약 제네릭을 특정하기로 결정했다면 해당 타입뿐만 아니라 이 타입을 상속한 모든 파생 타입에 대해서도 특정해 사용해야 한다.
아이템 25 : 타입 매개변수로 인스턴스 필드를 만들 필요가 없다면 제네릭 메서드를 정의하라
제네릭 클래스를 사용하는 대신 제네릭 메서드를 사용해서 정의 해보라는 말인데,
그 장점은
- 각 메서드 별로 제약 조건을 설정할 수 있다.
- 타입별로 특정된 메서드를 정의할 수 있다.
- 매개변수를 명시적으로 지정할 필요가 없다.
public static class Utils<T>
{
public static T Max(T left, T right) => Comparer<T>.Default.Compare(left, right) < 0 ? right : left;
public static T Min(T left, T right) => Comparer<T>.Default.Compare(left, right) < 0 ? left : right;
}
double d1 = 4;
double d2 = 5;
Utils<double>.Max(d1, d2);
제네릭 클래스를 사용시 문제점은 항상 타입을 지정해줘야 하고, Max, Min 같은 메서드를 이미 갖고 있는 타입이라도 모두 Compare를 사용해야 한다는 것이다.
public static class Utils
{
public static T Max<T>(T left, T right) => Comparer<T>.Default.Compare(left, right) < 0 ? right : left;
public static double Max(double left, double right) => Math.Max(left, right)
// 다른 타입에 대한 Max 메서드 생략
public static T Min<T>(T left, T right) => Comparer<T>.Default.Compare(left, right) < 0 ? left : right;
// 다른 타입에 대한 Min 메서드 생략
}
double d1 = 4;
double d2 = 5;
Utils.Max(d1, d2);
Utils 클래스를 일반 클래스로 바꾸고 Min, Max 메서드를 제네릭으로 바꿨고,
이렇게 되면 메서드를 사용할 때에 타입을 명시할 필요가 없어진다.
또, 특정한 타입들에 대해 오버로딩을 하여 특정한 메서드를 정의할 수 있다.
제네릭 클래스를 사용해야 하는 경우
- 클래스 내에 매개변수 타입으로 내부 상태를 유지해야 하는 경우 (ex 컬렉션)
- 제네릭 인터페이스를 구현하는 클래스를 만들어야 하는 경우
이 경우를 제외하고는 제네릭 클래스보다 제네릭 메서드를 사용하는 것이 좋다.
나한테는 해당 안됬던 내용
아이템18 반드시 필요한 제약 조건만 설정하라
where T : new() => 이걸 잘 쓸일이 없어서 이쪽으로 분류
C#의 default() 연산자는 특정 타입의 기본 값을 가져온다. (값 타입에 대해서는 0, 참조 타입에 대해서는 null을 가져온다)
해당 T의 기본 생성자가 있어야함
다른제약조건과 함게 쓸때 마지막에 쓰여야함
런타임에 너무 많은 제약 조건을 하는 경우 성능 저하를 피해라
그래서 제약은 컴파일 타임에 확인하는 경우가 좋음
그러므로 코드로 if~ else 로 조건 체크하는것보다
가능하다면 아래처럼 컴파일러를 통해 제약하기
public static bool AreEqual<T>(T left, T right)
where T : IComparable<T> => left.CompareTo(right) == 0;
default (T) 를 사용하는 경우 new()연산자가 필요 없을 수도 있음.
new(), struct, class를 제약 조건으로 설정하는 경우에는 항상 주의해야 한다.
아이템20 IComparable<T>로 선후 관계 정의
타입내에 관계 연산자(<, >, <=, >=)를 재정의하면, 해당 타입에 최적화된 방식으로 객체의 선후 관계를 판단할 수 있으므
로, 기본 관계 연산자의 구현 기능을 이용 시 발생할 수 있는 비효율 문제를 개선할 수 있다.
IComparable을 구현할 때는 반드시 명시적으로 인터페이스를 구현하고 추가적으로 강력한 타입(strongly typed)의 public
오버로드 메서드도 함께 구현해야 한다.
이 오버로드 된 메서드를 사용하면 더 빠르게 비교 연산을 수행할 수 있고, CompareTo 메서드의 오용 가능성을 줄일 수 있다.
// 관계 연산자
public static bool operator <(Customer left, Customer right) => left.CompareTo(right) < 0;
public static bool operator <=(Customer left, Customer right) => left.CompareTo(right) <= 0;
public static bool operator >(Customer left, Customer right) => left.CompareTo(right) > 0;
public static bool operator >=(Customer left, Customer right) => left.CompareTo(right) >= 0;
아이템 22 타입의 가변성 : 특정 타입이 다른 타입으로 변환할 수 있는 것
상속관계에서의 지원에 관한 얘기
BaseClass base = new ChildClass1();
이런식의 경우 base가 ChildClass2, 3의 형을 담을 수도 있는 과정에서 헷갈릴 수 있는
조금 뻔해 보이는 문제를 다룸
아이템 23 타입 매개변수에 대해 메서드 제약 조건을 설정하려면 델리게이트를 활용하라
아이템 26 : 제네릭 인터페이스와 논제네릭 인터페이스를 함께 구현하라
제네릭이 생기기 이전 개발된 코드를 무시 할 수 있다면, 패스~ 아니라면 논제네릭 방식을 지원할 수 밖에 없다.
새로운 라이브러리를 개발할 때에 제네릭 타입뿐만 아니라 고전적인 방식도 함께 지원하면 라이브러리의 활용도를 좀 더 높일 수 있기 때문이다.
논제네릭 방식이 되는 요소 세가지가 있는데 이는
- 클래스와 인터페이스
- public 속성
- serialize 대상이 되는 요소
이 세가지다.
Equals() 메서드 내에서 IEquatable.Equals()를 호출하도록 하기
IEquatable 를 구현하였을 때에 Object.Equals() 메서드에서 IEquatable.Equals()를 호출하도록 할 수 있다.
// class MyClass : IEquatable<MyClass>
public override bool Equals(object obj)
{
if (obj.GetType() == typeof(MyClass))
return this.Equals(obj as MyClass);
else
return false;
}
위 코드의 5번째 줄에서 obj의 타입을 체크하는 이유는 obj가 Name을 상속한 파생 클래스일 경우에 타입이 다른데도 true가 될 수 있기 때문이다.
주의) Equals() 메서드를 override 했을 경우에는 GetHashCode() 메서드도 override 해야한다.
IEquatable<T>를 구현했다면 operator==와 operator!=도 함께 구현 하기
// class MyClass : IEquatable<MyClass>
public static bool operator ==(MyClass left, MyClass right)
{
if (left == null)
return right == null;
return left.Equals(right);
}
public static bool operator !=(Name left, Name right)
{
if (left == null)
return right != null;
return !left.Equals(right);
}
IComparable를 구현했다면 IComparable와 operator도 함께 구현하기
마찬가지로 선후관계를 확인하기 위해 IComparable를 구현했다면,
제네릭이 아닌 IComparable과 operator(<, >, <=, >=)도 함께 구현하는 것이 좋다.
제네릭이 없던시절.. 너무 옛날 주제를 다룸
아래는 모두 확장메서드 관련
확장 메소드가 범용성이 넓고 좋게 사용할 수 있지만, 여럿이 개발하는 프로젝트에서 상호 합의하에서 개발되는 것이 바람직한데, 쉽지 않아보여서 우선은 이쪽으로
아이템 27 : 인터페이스는 간략히 정의하고 기능의 확장은 확장 메서드를 사용하라
확장 메서드를 이용하면 인터페이스에 새로운 동작을 추가할 수 있다.
인터페이스에서 정의하는 멤버들은 이를 구현하는 클래스에서 반드시 구현해야 하는데, 반드시 구현해야 하는 멤버의 수는 최소한으로 줄이고, 확장 메서드를 통해 다양한 기능을 제공할 수 있다.
// IFoo 인터페이스
public interface IFoo
{
int Marker { get; set; }
}
// IFoo 의 확장 메서드
public static class FooExtenstions
{
public static void NextMarker(this IFoo thing) => thing.Marker += 1;
}
// IFoo의 구현체 MyType 클래스
public class MyType : IFoo
{
public int Marker { get; set; }
}
// MyType 클래스에는 NextMarker() 메서드가 정의되어 있지 않지만 호출할 수 있다.
MyType t = new MyType();
t.Marker = 1;
t.NextMarker();
아이템 28 : 확장 메서드를 이용하여 구체화된 제네릭 타입을 개선하라
List<T>나 Dictionary<T>와 같이 제네릭 컬렉션에 타입 매개변수를 지정하여 사용할 것이다. 기존에 사용 중인 컬렉션 타입에 영향을 주지 않으면서 새로운 기능을 추가하고 싶다면 구체화된 컬렉션 타입에 대해 확장 메서드를 작성하면 된다.
예시로, System.Linq.Enumerable 클래스는 특정 IEnumerable 타입에 대한 확장 메서드들이 정의되어 있다.
public static class Enumerable
{
public static int Average(this IEnumerable<int> sequnece);
public static int Max(this IEnumerable<int> sequence);
public static int Min(this IEnumerable<int> sequence);
public static int Sum(this IEnumerable<int> sequence);
// 다른 메서드 생략
}
// 동욱님이 따로 분리해둔 mdnf코드
public static class EnumerableExtensionMethods
{
public static void ForEach<T>(this ICollection<T> collection, Action<T> action)
{
foreach (T obj in collection)
{
action(obj);
}
}
public static bool IsNullOrEmpty<T>(this IEnumerable<T> enumerable)
{
return enumerable == null || !enumerable.Any();
}
public static bool IsEmpty<T>(this ICollection<T> collection)
{
return collection.Count == 0;
}
// 다른 메서드 생략
}
dungeonStackableItemList_.ForEach(e => e.Value.ResetCooltime()); // 요런코드..
구체화된 제네릭 타입을 상속하여 메서드를 추가하기보다는 확장 메서드를 구현하는 편이 훨씬 낫다
확장 메서드를 사용했을 때의 장점
- 단순한 기능을 제공하는 메서드를 다양하게 재사용할 수 있다.
- 컬렉션 고유의 저장소 모델과 무관하게 기능을 구현할 수 있다. (IEnumerable 등 사용)
'개발 공부 유니티, C# > 스터디' 카테고리의 다른 글
제프리 리처의 CLR via C# 2부 타입 설계 part.1 (0) | 2023.06.25 |
---|---|
제프리 리처의 CLR via C# 1부 CLR 기본 (0) | 2023.06.25 |
Effective C# Chapter 4 (0) | 2023.01.08 |
Effective C# Chapter 2 (0) | 2023.01.04 |
Effective C# Chapter 1 (0) | 2023.01.04 |