반드시 필요한 제약 조건만 설정하라

 

제네릭 타입의 제약 조건은 클라이언트 개발자와 아키텍처 개발자와의 적절한 조건을 찾아서 설계되어야 한다.

C# 컴파일러는 타입 매개변수에 대한 제약조건이 없을 시 System.Object의 public 메서드만 제공한다. 왜냐하면 컴파일러는 타입 매개변수에 대한 어떠한 추측도 할 수 없고, C#의 특성상 모든 타입은 System.Object에서 파생된다는 규칙 때문이다.

 

클라이언트 개발자와 아키텍쳐 개발자와의 적절한 조건을 조절해주는 제네릭 타입2가지 방법.

   1. 런 타임에서 구분하기

 

public class Test<T>
{
	public bool AreEqual(T a, T b)
	{
		if(a is IComparable<T> &&  b is IComparable<T>)
		{
			IComparable<T> aVal = a as IComparable<T>;
			IComparable<T> bVal = b as IComparable<T>;
			return (aVal.CompareTo(b) == 0);
		}

		throw new ArgumentException("Type does not implement IComparable<T>");
	}
}

   - 개발자가 직접 오류가 발생하지 않게 처리를 진행했다.(잘못된 코드가 있어도 컴파일 완료)

 

   2. 컴파일 타임에서 구분하기

 

public class Test<T> where T : IComparable<T>
{
	public bool AreEqual(T a, T b)
		=> (a.CompareTo(b) == 0); 	
}

   - 컴파일러를 통해 오류가 발생하지 않게 했다(잘못된 코드가 있을 경우 컴파일 오류).

 

 

System.Object의 public 메서드만 제공

public static bool AreEqal<T>(T a, T b)
	=> a.Equals(b);

  위의 코드의 Eqauls는 기본적으로 Object의 Eqauls를 호출한다. 하지만 성능상의 문제가 발생한다.

  1. object타입이 아닌 다른 참조 타입일 수도 있기 때문이다. (다른 타입 -> object 박싱).

  2. T타입 a와 b가 object.Equals를 오버라이드 하는지 탐색. (런타임에서 탐색하는 비용)

 

public static bool AreEqal<T>(T a, T b)
	where T : IEquatable<T>
    	=> a.Equals(b);

  두번째코드는 IEquatable<T>의 Equals를 호출한다.  이는 위에서 설명했던 2가지의 약간의 성능 문제를 해결할 수 있다.  하지만 다른 개발자는 해당 메서드를 사용하기 위해 Class 설계 시 IEquatable<T> 인터페이스를 구현해야 한다.

뭐 지금은 하나쯤이야 구현하면 되지 하지만 정말 다양한 상황이 발생하는 이 세상은 IEquatable<T> 인터페이스뿐만 사용하게 둘까??

 

그래서 때때로 AreEqual 메서드를 오버로드 형태로 제공하면 제공하면 위와 같은 문제(다른 개발자의 편의를 생각하는 문제)를 풀어줄 수 있다. '1. 런 타임에서 구분하기 예시 코드'처럼 말이다. 아키텍처 설계자가 직접 처리하는 것이다.

 

때때로 new 대신 default()를 사용하면 new() 제약 사항이 필요 없을 수도 있다.

실제로 잘 작성된 제네릭 타입은 지정한 타입의 기본값을 가져오기 위해서 default()를 사용한다.

아래 두 코드를 비교해보자

 

1. 조건을 만족하는 첫 번째 객체를 찾아내고 그렇지 못하면 기본값을 반환하는 코드이다.

 

public static T FirstOrDefault<T>(this IEnumerable<T> sequence, Predicate<T> test)
{
    foreach(T val in sequence)
    {
    	if(test(val))
            return val;
    }
    
    return default(T);
}

 

 

2. T 타입의 객체를 생성하는 펙토리 메서드를 사용하는데, 이 팩토리 메서드가 null을 반환하면 기본 생성자를 호출한 후 그 값을 반환한다.

 

public delegate T FactoryFunc<T>();

public static T Factory<T>(FactoryFunc<T> makeANewT) where T : new()
{
    T rVal = makeANewT();
    
    if(rVal == null)
    	return new T();
    
    return rVal;
}

차이점 

1. 1번 코드는 default()를 사용하여 new() 제약조건을 사용하지 않았다.

2. 1번 코드의 T는 값 타입인 경우 JIT 컴파일러가 null 테스트 코드를 제거해준다.

3. 1번 코드는 값 타임과 참조 타입 모두 사용 가능하다.

4. 2번 코드는 new()를 사용하여 new() 제약 조건을 사용했다. 

5. 2번 코드는 new() 제약 조건을 통해 참조 타입이기 때문에 null 체크를 해야 한다.

6. 2번 코드는 new() 제약 조건에 의해 참조 타입만 사용 가능하다.

 

new(), struct, class,를 제약 조건으로 설정하는 경우에는 항상 주의해야 한다. 위의 코드와 같이 제약 사항을 추가하면 객체의 생성을 제한하고 이는 코드의 재사용성에 영향을 줄 수 있다. 그래서 이상적으로 new(), struct, class제약 사항은 가능한 피하는 것이 좋다. new T() 보다는 default(T)!!!!!!!

 

결론 

1. 제네릭 타입은 제약사항이 없을 시 System.Object로 시작한다.

2. 너무 많은 제약사항은 클라이언트 개발자에게 독이 된다.

3. System.Object의 public 메서드가 호출될 것 같으면 오버로드 형태로 내가 직접 코딩하여 제공한다.

4. new(), struct, class 제약사항은 가능하면 피하자

5. 펙토리 메서드 작성 시 new T() 보다는 default(T)를 사용하자

 

'Program > Effective C#' 카테고리의 다른 글

Effective C# ITEM20  (0) 2020.11.25
Effective C# ITEM 19  (0) 2020.11.18
Effective C# ITEM 17  (0) 2020.11.08
Effective C# ITEM16  (0) 2020.11.05
Effective C# ITEM15  (0) 2020.11.03

+ Recent posts