# Effective 3장
## 각 장별 요약
- 1장 : C# 언어 요소
- 2장 : .NET 리소스 관리
- 3장 : 제네릭 활용
- 4장 : LINQ 활용
- 5장 : 예외 처리
---
## 3 장 : 제네릭 활용
> 제네릭을 이용한 다양한 활용에 대해 학습한다.
### 아이템 18 : 반드시 필요한 제약 조건만 설정하라
- 타입 매개변수에 대한 제약조건은 클래스가 작업을 올바르게 수행하기 위해서 타입 매개변수로 전달할 수 있는 타입의 유형을 제한하는 방법이다.
- 해당 제약 조건을 선언하면 제약 형식의 작업 및 메서드 호출을 사용할 수 있다.
- 제네릭 클래스 또는 메서드가 단순 할당 또는 System.Object에서 지원하지 않는 메서드 호출 이외의 작업을 제네릭 멤버에서 사용하는 경우 형식 매개 변수에 제약 조건을 적용한다.
- 제약조건을 설정하지 않으면 런타임에 더 많은 검사를 수행할 수 밖에 없다.
- 그러나 필요한 만큼만 제약조건을 설정했다고 생각하겠지만, 그마저도 과도할 수 있다는 것을 항상 염두에 둬야 한다.
- 제약조건을 과도하게 설정하는 것 또한 좋지 않다.
- 적당히 제약조건을 설정하면 다음과 같은 상황에서 도움이 될 수 있다.
```csharp=
// # 1
public static bool AreEqual<T>(T left, T right)
{
if (left == null)
return right == null;
if (left is IComparable<T>)
{
IComparable<T> Ival = left as IComparable<T>;
if (right is IComparable<T>)
return Ival.CompareTo(right) == 0;
else
throw new ArgumentException("Type dose not implement IComparable<T>", nameof(right));
}
else
{
throw new ArgumentException("Type dose not implement IComparable<T>", nameof(left));
}
}
// #2
public static bool AreEqual2<T>(T left, T right)
where T : IComparable<T> =>
left.CompareTo(right) == 0;
```
- `# 1` , `# 2` 메소드 모두 런타임 오류를 방지할 수 있는 `AreEqual` 메소드이다.
- 하지만 1번의 경우 코드를 통해 이를 방지했고, 2번의 경우 컴파일러를 통해 방지했다.
- 이와같이 제약조건을 적당히 사용하면 코드도 더 간결해지고, 컴파일러를 통해 코딩상의 실수를 알려줄 수 도 있다.
---
### 아이템 19 : 런타임에 타입을 확인하여 최적을 알고리즘을 사용하라
- 제네릭 타입의 경우 타입 매개변수에 새로운 타입을 지정하여 손쉽게 재사용할 수 있다.
- 또한 제네릭을 활용하면 코드를 덜 작성해도 되기 때문에 매우 유용하다.
- 타입 매개변수에 새로운 타입을 지정한다는 것은 유사한 기능을 가진 새로운 타입을 생성한다는 것을 의미한다.
- 하지만 타입이나 메서드를 제네릭화 하면 구체적인 타입이 주는 장점을 잃고 타입의 세부적인 특성까지 고려하여 최적화한 알고리즘을 사용할 수 없게 된다.
- 제네릭의 인스턴스화는 런타임의 타입을 고려하지 않으며 컴파일타임의 타입만을 고려한다.
- 어떤 알고리즘이 특정 타입에 대해 더 효율적으로 동작한다고 생각된다면 해당 타입을 이용하도록 코드를 작성하라.
- 효율적인 코드를 작성하려면 이러한 사실을 반드시 알고 있어야 한다.
---
### 아이템 20 : `IComparable<T>` 와 `IComparer<T>` 를 이용하여 객체의 선후 관계를 정의하라
- 컬렉션을 정렬하거나 검색하려면 타입 내에 객체의 선후 관계를 판단할 수 있는 기능을 정의해야한다.
- .NET Framework 에서는 객체의 선후 관계를 정의하기 위해서 `IComparable<T>` 와 `IComparer<T>` 2개의 인터페이스를 제공한다.
- `IComparable<T>` 는 타입의 기본적인 선후관계를 정의하는 인터페이스이고 `IComparer<T>` 를 이용하면 기본적인 선후 관계 이외에 추가적인 선후 관계를 정의할 수 있다.
- 또한 타입 내에 관계 연산자를 재정의하면 해당 타입에 최적화된 방식으로 객체의 선후 관계를 판단할 수 있다.
- `IComparable<T>` 인터페이스는 `CompareTo()` 메소드만을 담고있고 이는 C언어의 `strcmp` 와 같은 동작을 한다.
- 비교 대상이 작으면 0 보다 작은값, 같으면 0, 크면 0보다 큰 값을 반환한다.
- `IComparable<T>` 를 구현할 때는 하위호환성을 고려하여 `IComparable` 도 함께 구현해야 한다.
- `IComparable` 은 매 호출마다 박싱 / 언박싱이 필요하므로 비 효율적이다.
- ex ) 1000개의 항목을 정렬하기 위해서 2만번 이상의 박싱 / 언박싱이 필요하다.
---
### 아이템 21 : 타입 매개변수가 `IDisposable` 을 구현한 경우를 대비하여 제네릭 클래스를 작성하라
- 제약 조건은 두 가지 역할을 한다.
1. 런타임 오류가 발생할 가능성이 있는 부분을 컴파일 타임 오류로 대체할 수 있다.
2. 타입 매개변수로 사용할 수 있는 타입을 명확히 규정하여 사용자에게도 도움을 준다.
- 하지만 제약 조건은 타입 매개변수가 무엇을 해야 하는지만을 규정할 수 있고, 무엇을 해서는 안되는지를 정의할 수 없다.
- 대부분의 경우 타입 매개변수로 지정하는 타입이 제약조건을 통해 요구하는 작업 외에 다른 작업을 추가로 수행할 수 있는지에 대해서 신경쓰지 않는다.
- 하지만 타입 매개변수로 지정하는 타입이 `IDisposable`을 구현하고 있다면 특별한 추가 작업이 반드시 필요하다
```csharp=
public interface IEngine
{
void DoWork();
}
public class EngineDriverOne<T> where T : IEngine, new()
{
public void GetTHingsDone()
{
T driver = new T();
driver.DoWork();
}
}
```
- `<T>` 가 `IDisposable` 을 구현한 타입일 경우 리소스 누수가 발생할 수 있다.
- 따라서 `<T>` 타입으로 지역변수를 생성할 때마다 `<T>` 가 `IDisposable` 을 구현하고 있는지 확인해야 하며, 만약 `IDisposable` 을 구현하고 있다면 추가적인 처리를 해야한다.
```csharp=
public void GetTHingsDone()
{
T driver = new T();
using (driver as IDisposable)
{
driver.DoWork();
}
}
```
- 이처럼 코드를 작성하면 컴파일러는 `IDisposable` 로 형변환된 객체를 저장하기 위해서 숨겨진 지역변수를 생성한다.
- 만약 `<T>` 가 `IDisposable` 을 구현하지 않았다면 이 지역변수의 값은 `null` 이 된다.
- C# 컴파일러는 이 지역변수의 값이 `null` 인 경우 `Dispose()` 가 호출되지 않는다.
- 반대로 `<T>` 가 `IDisposable` 을 구현했다면 `using` 블록을 종료할 때 `Dispose()` 메서드가 호출된다.
---
- 타입 매개변수로전달한 타입을 이용하여 멤버 변수를 선언한 경우에는 이보다 조금 더 복잡하다.
- `IDisposable` 을 구현했을 가능성이 있는 타입으로 멤버 변수를 선언한 것이기 때문이다.
- 이 경우 제네릭 클래스에서 `IDisposable` 을 구현하여 해당 리소스를 처리해야 한다.
```csharp=
public sealed class EngineDriverOne<T> : IDisposable where T : IEngine, new()
{
private Lazy<T> driver = new Lazy<T>(() => new T());
public void GetTHingsDone() => driver.Value.DoWork();
public void Dispose()
{
if (driver.IsValueCreated)
{
var resource = driver.Value as IDisposable;
resource?.Dispose();
}
}
}
```
- 하지만 제네릭 클래스의 인터페이스를 조금 변경하면 이런 복잡한 설계를 피할 수 있다
ex ) `Dispose` 의 호출 책임 / 객체의 소유권을 제네릭 클래스 외부로 옮기기 등
---
### 아이템 22 : 공변성과 반공변성을 지원하라
- 타입의 가변성, 즉 공변과 반공변은 특정 타입의 객체를 다른 타입의 객체로 변환할 수 있는 성격을 일걷는다.
- 이러한 변환을 지원하려면 제네릭 인터페이스나 델리게이트의 정의 부분에 제네릭 공변 / 반공변을 지원한다는 의미의 데코레이터를 추가해야한다.
- 가변성의 반대는 불변성이라고 한다.
> 공변성 : 자신과 자식 타입으로만 형변환. out 키워드
> 반공변성 : 자신과 부모 타입으로만 형변환. in 키워드
- 타입 매개변수로 주어지는 타입들이 상호 호환 가능할 경우 이를 이용하는 제네릭 타입도 호환 가능함을 추론하는 기능이다.
- `X` 를 `Y` 로 바꾸어 사용할 수 있는 경우 `C<X>` 를 `C<Y>` 로도 바꿔 사용할 수 있다면,
`C<T>` 는 공변이다.
- `Y` 를 `X` 로 바꾸어 사용할 수 있는 경우 `C<X>` 를 `C<Y>` 로도 바꿔 사용할 수 있다면,
`C<T>` 는 반공변이다.
ex ) `IEnumerable<Object>` 타입의 매개변수를 취하는 메서드는 `IEnumerable<MyDerivedType>` 타입 객체도 받아들일 수 있어야 하고, `IEnumerable<MyDerivedType>` 타입의 객체를 반환하는 메서드의 결과값이 `IEnumerable<Object>` 타입 객체에 할당될 수 있어야한다.
- C# 4.0 이전에는 제네릭 타입이 이러한 가변성을 지원하지 않았다.
- 그 이후 in 과 out 키워드가 추가됐는데 이를 이용하면 제네릭을 좀 더 유용하게 사용할 수 있다.
- 이 데코레이터는 제네릭 인터페이스와 델리게이트 선언시에 사용할 수 있다.
- .NET Base Class Library ( BCL ) 에 포함된 델리게이트의 정의도 가변성을 지원한다.
```csharp=
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
```
---
### 아이템 23 : 타입 매개변수에 대해 메서드 제약조건을 설정하려면 델리게이트를 활용하라
- C# 에서 제약조건을 설정하는 방법은 한계가 많은 것 처럼 보인다.
1. 베이스 클래스의 타입
2. 특정 인터페이스
3. class 타입
4. struct 타입
5. 생성자의 매개변수 유무
- 위의 다섯가지 조건 정도만을 설정할 수 있기 때문이다.
- 임의의 정적 메서드를 반드시 구현해야 한다거나,
매개변수를 취하는 여타의 생성자를 반드시 구현하도록 제약조건을 설정할 수 는 없다.
- 아래와 같이 제한적이나마 인터페이스를 선언 / 상속하여 다양한 요건을 만족하도록 설정이 가능하긴 하다.
```csharp=
// 인터페이스 선언
public interface IAdd<T>
{
public T Add(T arg1, T arg2);
}
// 인터페이스 내부 메소드 구현 강제
class Program : IAdd<int>
{
public int Add(int a, int b)
{
return a + b;
}
public static void Main()
{
Program MyProgram = new Program();
Console.WriteLine(MyProgram.Add(1, 2));
}
}
```
- 함수를 강제하긴 했지만 인터페이스 상속과 더불어 닫힌 제네릭 설정, 그에 맞는 메소드 구현 등 사용자가 해야할 일이 많아진다.
- 하지만 델리게이트를 사용한다면 사용자가 원하는 기능을 람다로 바로 적용할 수 있다.
```csharp=
// 델리게이트 선언
public class DelegateAdd
{
public static T Add<T>(T left, T right, Func<T, T, T> AddDelegate) => AddDelegate(left, right);
}
class Program
{
public static void Main()
{
Program MyProgram = new Program();
// 람다로 바로 원하는 작업 수행
Console.WriteLine(DelegateAdd.Add(1, 2, (x, y) => x + y));
}
}
```
---
### 아이템 24 : 베이스 클래스나 인터페이스에 대해서 제네릭을 특화하지 말라
- 제네릭 메서드가 등장함에 따라 여러개의 오버로드된 메서드가 있는 경우, 이 중 하나를 선택하는 과정이 꽤 복잡해졌다.
- 컴파일러는 제네릭 메서드의 타입 매개변수가 다른 타입으로 다양하게 변경될 수 있음을 고려하여 오버로드된 메서드 중 하나를 선택한다.
- 그런데 자칫 이러한 동작 방식을 간과하게 되면 응용프로그램이 이상하게 동작할 수도 있다.
- 제네릭 클래스나 제네릭 메서드를 작성할 때는 사용자가 가능한 한 안전하고 혼돈스럽지 않도록 작성해야 한다.
- 특히 오버로드된 메서드가 여러개인 경우 컴파일러가 이중 하나를 어떻게 선택하는지 정확히 알고 있어야 한다.
<details>
<summary>예제</summary>
<div>
```csharp=
public class MyBase
{
}
public interface IMessageWriter
{
void WriteMessage();
}
public class MyDerived : MyBase, IMessageWriter
{
public void WriteMessage() => WriteLine("Inside MyDerived.WriteMessage");
}
public class AnotherType : IMessageWriter
{
public void WriteMessage() => WriteLine("Inside AnotherType.WriteMessage");
}
class Program
{
static void WriteMessage(MyBase b)
{
WriteLine("Inside WriteMessage(MyBase)");
}
static void WriteMessage<T>(T obj)
{
Write("Inside WriteMessage<T>(T): ");
WriteLine(obj.ToString());
}
static void WriteMessage(IMessageWriter obj)
{
Write("Inside WriteMessage(IMessageWriter): ");
obj.WriteMessage();
}
public static void Main()
{
MyDerived d = new MyDerived();
WriteMessage(d);
WriteLine();
WriteMessage((IMessageWriter)d);
WriteLine();
WriteMessage((MyBase)d);
WriteLine();
AnotherType anObject = new AnotherType();
WriteMessage(anObject);
WriteLine();
WriteMessage((IMessageWriter)anObject);
WriteLine();
}
}
/* 결과
Inside WriteMessage<T>(T): ConsoleApp1.MyDerived - d는 MyBase 보다 제네릭 T랑 더 가깝다
Inside WriteMessage(IMessageWriter): Inside MyDerived.WriteMessage
Inside WriteMessage(MyBase)
Inside WriteMessage<T>(T): ConsoleApp1.AnotherType
Inside WriteMessage(IMessageWriter): Inside AnotherType.WriteMessage */
```
</div>
</details>
- 사용자 명시적으로 타입을 설정한 일반 메서드 보다 제네릭 메서드가 우선적으로 선택되는 경우에 대해서도 명확히 이해하고 있어야 한다.
- 따라서 베이스 클래스와 이로부터 파생된 클래스에 대해서 모든 메소드가 수행 가능하도록 하기 위해 제네릭을 특화하려는 시도는 바람직하지 않다.
---
### 아이템 25 : 타입 매개변수로 인스턴스 필드를 만들 필요가 없다면 제네릭 메서드를 정의하라
- 제네릭을 사용하다 보면 자칫 무작정 제네릭 클래스를 만드는 습관에 빠지곤 한다
- 하지만 유틸리티 성격의 클래스를 만드는 경우에는 일반 클래스 내에 제네릭 메서드를 작성하는 편이 훨씬 좋다.
- 왜냐하면 제네릭 클래스를 작성하면 컴파일러의 입장에서는 전체 클래스에 대하여 타입 매개변수에 대한 제약 조건을 고려하여 컴파일을 해야하기 때문이다.
- 반면 유틸리티 성격의 클래스를 만들 때 일반 클래스 내에 제네릭 메서드들을 배치하면 각 메서드별로 제약 조건을 달리 설정할 수 있다.
- 이처럼 메서드별로 제약조건을 달리 설정하면 요청되는 메서드의 원형에 좀 더 정확히 부합하는 메서드를 생성 할 수 있으므로 사용자 입장에서도 메서드를 활용하기가 수월해진다.
- 추가적으로 제네릭 메서드를 정의하면 타입 매개변수에 대한 제약 조건을 메서드 수준으로 지정할 수 있다.
- 반면 제네릭 클래스를 정의하면 클래스 전체에 대항 제약 조건을 고려해야만 한다.
- 이렇듯 제약조건의 적용 범위가 넓어지면 넓어질수록 코드를 수정하기가 점점 더 까다로워진다.
- 둘 중 하나를 선택할 수 있는 상황이라면 제네릭 메서드를 이용하는 편이 낫다.
- 정리하면 타입 매개변수로 인스턴스 필드를 만들어야 하는 경우에는 제네릭 클래스를 작성하고, 그렇지 않은 경우에는 제네릭 메서드를 작성하라.
<details>
<summary>예제</summary>
<div>
```csharp=
public static class Utils<T>
{
public static T Max(T lef, T right) =>
Comparer<T>.Default.Compare(left, right) < 0 ? right : left;
public static T Min(T lef, T right) =>
Comparer<T>.Default.Compare(left, right) < 0 ? left : right;
}
```
- 위와같이 작성한 메서드는 잘 동작한다.
- 하지만 숫자 타입에 사용가능한 Math.Min / Max 메서드를 사용할 수 없다는 단점이 있다.
- 따라서 클래스를 일반 클래스로 정의하고 제네릭 메서드를 구현해보자
```csharp=
public static class Utils
{
public static T Max(T lef, T right) =>
Comparer<T>.Default.Compare(left, right) < 0 ? right : left;
public static double Max(double left, double right) =>
Math.Max(left, right);
...
}
```
- 위와 같이 작성한다면 타입별로 다른 메서드에서 최적의 효율을 낼 수 있다.
- 또한 정적 메서드를 사용할 때 타입을 명시할 필요가 없어진다.
</div>
</details>
---
### 아이템 26 : 제네릭 인터페이스와 논제네릭 인터페이스를 함께 구현하라
- C# 에 제네릭이 포함되기 이전에 개발됐던 코드를 모조리 무시할 수 있다면 좋겠지만, 여러 이유로 이전 코드를 무시하기가 어렵다.
- 새로운 라이브러리를 개발할 때 제네릭 타입 뿐 아니라 고전적인 방식도 함께 지원한다면 라이브러리 활용도를 좀 더 높일 수 있다.
- 만약 제네릭 타입이 아닌 방식도 지원하겠다고 결정했다면 다음과 같은 대상들도 고려해야한다.
1. 클래스와 인터페이스
2. public 속성
3. 시리얼라이즈 대상이 되는 요소
- 위 세가지에 대해서 논제네릭 방식을 지원해야 한다.
- 대부분의 경우 논제네릭 인터페이스를 추가하는 작업은 적절한 원형의 메서드를 추가하는 수준에서 간단히 해결된다.
- Visual Studio 를 포함하여 다양한 도구들이 이같은 인터페이스를 손쉽게 구현할 수 있는 기능을 제공한다.
- 이 기능을 이용하면 인터페이스 내에 정의된 메서드의 원형을 쉽게 추가할 수 있다.
- 또한 논제네릭 인터페이스를 구현할 때는 반드시 명시적인 방법으로 구현하는 것이 좋다.
---
### 아이템 27 : 인터페이스는 간략히 정의하고 기능의 확장은 확장 메서드를 사용하라
> 확장 메서드 : 별도의 namespace를 가진 정적 메서드로 매개변수에 this 키워드를 추가하여 사용할 수 있다. 기존의 메서드에 기능을 추가 / 확장 할 수 있기 때문에 확장 메서드라고 불린다.
- 확장 메서드를 이용하면 인터페이스에 새로운 동작을 추가할 수 있다.
- 인터페이스에는 가능한 한 최소한의 기능만을 정의하고, 확장 메서드를 세트로 함께 구현하면 손쉽게 기능을 확장할 수 있다.
- 특히 API를 추가적으로 정의하지 않고도 새로운 기능을 추가할 수 있다.
- `System.Linq.Enumberable` 클래스가 이 기법을 활용한 대표적인 예다.
- 확장 메서드를 사용하면 이미 해당 인터페이스나 메서드를 구현하고 있는 클래스를 수정할 필요가 없다.
- ex) 이전에 `IEnumberable`을 구현하고 있는 클래스는 새로운 메서드를 구현해야할 필요가 없으므로 이전과 동일하게 `Geteunumerator` 만 구현하면된다.
- 따라서 새로운 타입을 작성해야 한다고 생각이 든다면 우선 이 같은 확장 기법을 적용할 수 있을지 검토하자.
- 새로운 인터페이스를 작성하는 경우에도 동일한 패턴을 사용할 수 있다.
- 다양한 기능을 제공하도록 인터페이스를 정의하지 말고, 반드시 필요한 기능만을 포함하도록 간단히 인터페이스를 작성하자
- 그리고 사용자 편의를 위해 다양하게 제공하려는 기능은 확장 메서드 방식으로 작성하는 것이다.
- 인터페이스 내에 다양한 기능을 정의하는 것에 비해 확장 메서드를 이용하는편이 필수로 작성해야 하는 메서드의 수도 줄일 수 있고 사용자에게 더 풍부한 기능을 제공할 수 있다.
---
### 아이템 28 : 확장 메서드를 이용하여 구체화된 제네릭 타입을 개선하라
- 응용프로그램을 개발하다보면 `List<int>`, `Dictionary<T>` 와 같이 제네릭 컬렉션에 타입 매개변수를 지정하여 사용할 때가 있다.
- 이러한 컬렉션을 사용하는 이유는 특정 타입의 집합을 다루거나 컬렉션의 고유 기능 활용을 위함 일 것이다.
- 기존에 사용중인 컬렉션 타입에 영향을 주지않으며 새로운 기능을 추가하고 싶다면, 구체화 된 컬렉션 타입에 대해 확장 메서드 작성이 도움이 된다.
- 일례로 `Enumerable`에는 `IEnumberable<T>` 타입을 특정 타입으로 구체화 했을 때만 사용할 수있는 메서드들도 상당수 포함되어있다.
```csharp=
public static class Enumerable
{
public static int Average(this IEnumberable<int> sequence);
public static int Max(this IEnumberable<int> sequence);
public static int Min(this IEnumberable<int> sequence);
public static int Sum(this IEnumberable<int> sequence);
...
}
```
- 이 패턴은 타입 매개변수로 특정 타입이 주어질 때, 해당 타입에 대하여 가장 효과적으로 동작하도록 코드를 분리하여 구현하는 방법이다.
- 구체화된 제네릭 타입을 상속하여 메서드를 추가하기보다는 확장 메서드를 구현하는 편이 훨씬 낫다.
- 게다가 컬렉션 고유의 저장소 모델과 무관하게 기능을 구현할 수있다.
---
### 느낀점
> 3장에서는 실제로도 많이 사용하는 제네릭에 대하여 알아보는 시간이었다.
>
> 모든 부분을 제네릭으로 치환하면 안좋은 점과
> 또 하위호환성을 고려해서 논제네릭 메서드등을 작성해야한다는 것,
> 앞서 2장에서 배운 Dispose 패턴을 적용하여
> 멤버로 관리하는 제네릭 변수들도 관리 해줘야 하는 등
> 제네릭에 대한 장점과 단점, 또 이를 극복하는 방법을 알아보았다.
>
> 이론공부 외에도 실습이나 경험이 더 쌓여야 완전한 내 지식이 될 것 같아서
> 정리한 내용을 토대로 여러 테스트케이스와 구현을 해보면 좋을 것 같다.