-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
타입으로 견고하게 다형성으로 유연하게 4주차 - 이정안 #478
Open
fkdl0048
wants to merge
1
commit into
main
Choose a base branch
from
타입으로-견고하게-다형성으로-유연하게-4주차---이정안
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
The head ref may contain hidden characters: "\uD0C0\uC785\uC73C\uB85C-\uACAC\uACE0\uD558\uAC8C-\uB2E4\uD615\uC131\uC73C\uB85C-\uC720\uC5F0\uD558\uAC8C-4\uC8FC\uCC28---\uC774\uC815\uC548"
+190
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
190 changes: 190 additions & 0 deletions
190
2025/RobustWithTypeFlexibleWithPolymorphism/jeonglee/Chapter04.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
## 두 다형성의 만남 | ||
|
||
### 제네릭 클래스와 상속 | ||
|
||
```cs | ||
abstract class List<T> | ||
{ | ||
T get(int index); | ||
} | ||
``` | ||
|
||
- *C#의 제네릭 클래스는 불변이다.* | ||
|
||
다음과 같은 추상 클래스를 상속받아 서브타입 클래스를 만들었을 때, 앞서 다룬 제네릭과 같이 서브타입 클래스의 제네릭 타입은 부모 클래스의 제네릭 타입을 따른다. | ||
|
||
```cs | ||
class ArrayList<T> : List<T> | ||
{ | ||
T get(int index) { ... } | ||
} | ||
``` | ||
|
||
여기서 핵심은 상속받는 추상 클래스의 제네릭 타입을 `T`로 정의했기에 서브타입 클래스에서도 제네릭의 특성을 이용하고 싶다면 `T`로 정의해야 한다는 것이다. A가 B를 상속하면 A가 B의 서브타입이기 때문에 이 원리 그대로 제네릭 클래스에도 적용된다. | ||
|
||
반대로 제네릭의 특성을 이용할 필요가 없기 타입이 명확한 서브타입을 만들고 싶다면 제네릭 타입을 명시적으로 정의해야 한다. | ||
|
||
```cs | ||
class IntArrayList : List<int> | ||
{ | ||
int get(int index) { ... } | ||
} | ||
``` | ||
|
||
### 타입 매개변수 제한 | ||
|
||
앞서 3장에서 정리한 내용과 같이 제네릭 함수의 경우 매개 변수가 아무 타입이나 나타낼 수 있다고 가정하기에 타입 변수 T에 대해서 출력하거나 반환할 수는 있어도 덧셈이나 곱셈과 같이 특별한 능력이 필요한 곳에는 사용할 수 없다고 했다. 이 내용은 사용 의도 자체를 잘 나타내는 것으로 제네릭 함수를 정의하는 것은 결국 여러 타입으로 사용될 수 있는 함수를 만드는 것이고, 인자가 특별한 능력을 가져야 한다면 그 함수는 여러 타입으로 사용될 수 없다. | ||
|
||
*3장에 대한 추가 정리 내용* | ||
|
||
```cs | ||
class Person | ||
{ | ||
int age; | ||
... | ||
} | ||
|
||
class Student : Person {...} | ||
|
||
Person Elder(Person p1, Person p2) | ||
{ | ||
return p1.age > p2.age ? p1 : p2; | ||
} | ||
|
||
Student s = elder(s1, s2); | ||
``` | ||
|
||
위 코드는 타입 검사기를 통과하지 못한다. 정확하게는 `elder(s1, s2)`까지는 타입 검사를 통과한다. Student는 Person의 서브타입이기 때문에 Person 타입으로 반환할 수 있다. 하지만 결과를 Person으로 반환하는 순간 Student 타입으로 다운 캐스팅해야 하기 때문에 타입 검사를 통과하지 못한다. | ||
|
||
이 경우 실제 메모리에 적재된 객체가 분면 Student 타입임을 알지만 문제가 된다. *정적 타입 검사이기 때문* 이를 해결하기 위해 제네릭 함수로 교체한다고 해도 .age 필드 값을 읽으려 했기 때문에 여전히 타입 검사를 통과하지 못한다. | ||
|
||
이 상황을 해결하기 위해선 타임 **매개변수 제한**이라는 기능이 필요하다. *C#에서는 where 절을 사용한다.* 직관적인 해석은 T가 최대 Person타입까지 커질 수 있음을 나타낸다. 3장에서 예로 INumber<T>를 사용한 것과 같은 방식이다. | ||
|
||
이전까지 제네릭 함수가 아무 타입이나 인자로 받을 수 있던 것과 달리, 타입 매개변수 제한을 사용한 제네릭 함수를 사용할 때는 정의된 상한을 타입 인자가 따라야 한다. *함수뿐만 아니라 클래스도 사용 가능하다.* | ||
|
||
### 재귀적 타입 매개변수 제한 | ||
|
||
타입 매개변수가 자기 자신을 제한하는 데 사용될 수 있다. 이를 재귀적 타입 매개 변수 제한(F-bounded quantification)이라 부른다. *재귀 함수가 자기 자신을 호출하는 함수인 것과 비슷하다.* | ||
|
||
만약 정렬 함수가 있다면 이를 일반화하기 위해서 제네릭을 사용할 수 있다. 여기서 문제는 위와 같이 타입 제한이 없다면 코드 검사를 통과하지 못할 것이라는 것이다. 따라서 이에 맞는 `Compareble` 인터페이스(or 추상 클래스)를 정의한다. | ||
|
||
여기까지는 단순한 구현이지만 `Comparable`의 반환 타입이 Boolean인 것은 맞지만 실제 값을 구별할 클래스가 단순 자료형인지 사용자 정의 클래스인지 달라질 수 있기 때문에 이를 제네릭으로 확장하는 것이다. | ||
|
||
*단순 자료형으로 이를 사용하기 위해서는 확장 메서드를 사용해야 할 것 같다.* | ||
|
||
이를 C#으로 나타내면 다음과 같다. | ||
|
||
```cs | ||
void sort<T>(List<T> lst) where T : IComparable<T> | ||
{ | ||
if (lst[...].gt(lst[...])) { ... } | ||
} | ||
``` | ||
|
||
여기서 T의 상한에 T 자기 자신이 사용되었으니 이 코드는 재귀적 타입 매개변수 제한이다. | ||
|
||
결국 이 코드는 T가 반드시 Comparable<T>의 서브타입이어야 한다는 뜻(인터페이스를 구현해야 한다.)으로 해석할 수 있다. 이것은 재귀적 개념과 동일하다. | ||
|
||
### 가변성 | ||
|
||
가변성(variance)은 제네릭 타입 사이의 서브타입 관계를 추가로 정의하는 기능이다. | ||
|
||
앞서 다룬 `List<Person>`과 `List<Student>`의 관계를 생각해보자. List<Student>는 List<Person>은 서로 다른 타입이다. 동시에 그렇다고 서브타입이 아니라고도 볼 수 없다. 만약 함수에서 주어진 Person의 값을 평균내는 함수라면 Student도 동작해야 마땅하다. | ||
|
||
앞서 배운 방법대로 제네릭 함수로 만들되 타입 매개변수 제한을 사용하면 해결이 된 것처럼 보인다. 하지만 매번 추가적인 타입 인자를 정의할 때마다 매개변수 타입을 Student로 바꿔야 한다. 이는 매우 번거로운 일이다. | ||
|
||
*즉, 제네릭의 목적성이 다형성의 측면이 강한 것에 비해 덩치가 커져버리는 구현이라는 것이다. 단순하게 서브타입에 의한 다형성이나 단순 구현으로도 해결이 가능하다.* | ||
|
||
이를 구현하려면 `List<Student>`을 `List<Person>`의 서브타입으로 만들어야 한다. 이것이 가능하기 위해서는 조건들이 붙는다. | ||
|
||
```cs | ||
abstract class List1<T> | ||
{ | ||
T get(int index); | ||
} | ||
|
||
abstract class List2<T> | ||
{ | ||
T get(int index); | ||
void add(T t); | ||
} | ||
``` | ||
|
||
이런 두 가지 추상클래스가 존재하고 `Student`클래스가 `Person`클래스를 상속받은 서브타입의 관계일 때, List1의 경우에는 List1<Student>가 List1<Person>의 서브타입이 된다. 반면 List2의 경우에는 List2<Student>가 List2<Person>의 서브타입이 되지 않는다. | ||
|
||
실제 동작을 예로 `List2<Person> people = students;`의 관계에서 people에 add메서드를 통해 person을 추가한다면 이후 get을 통해 값을 꺼낼 때 학생인지 사람인지 알 수 없다. *실행중에 오류발생* | ||
|
||
이를 정리하면 "List가 원소 읽기만 허용하면 그래도 되고, 원소 추가도 허용하면 그렇지 않다."라고 답할 수 있지만 좀 더 용어적인 설명이 필요하기에 가변성이 등장한다. "어떤 제네릭 타입은 타입 인자의 서브타입 관계를 보존하지만, 어떤 제네릭 타입은 인자 사이의 관계를 분류할 수 있다." 여기서 이 분류가 **가변성**이다. | ||
|
||
다시 말해 가변성이란 제네릭 타입과 타입 인자 사이의 관계를 뜻한다. 다음이 가변성의 종류이다. | ||
|
||
- 공변성(convariance): List1이 여기에 해당하며 B가 A의 서브타입일 때 List1<B>가 List1<A>의 서브타입이다. | ||
- 불변(invariance): List2가 여기에 해당하며 B가 A의 서브타입일 때 List2<B>가 List2<A>의 서브타입이 아니다. | ||
- 반변(contra-variance): 함수 타입이 여기에 해당하며 제네릭 타입이 타입 인자의 서브타입 관계를 뒤집는 것이다. 결과 타입을 C로 고정할 때 B가 A의 서브타입이면 B => C는 A => C의 슈퍼타입이다. | ||
|
||
앞서 다룬 가변성을 좀 더 단순하고 일반화하여 설명한다면 제네릭 타입의 이름을 G, 타입 매개변수의 이름을 T라고 한다. | ||
|
||
**G가 T를 출력에만 사용하면 공변, 입력에만 사용하면 반변, 출력과 입력에 모두 사용하면 불변이다.** | ||
|
||
| G에 해당하는 타입 | T를 출력에 사용 | T를 입력에 사용 | 가변성 | | ||
| ----------------- | --------------- | ---------------- | ------- | | ||
|List1<T> | O | X | 공변 | | ||
|List2<T> | O | O | 불변 | | ||
|int => T | O | X | 공변 | | ||
|T => int | X | O | 반변 | | ||
|
||
지금까지의 내용은 개발자가 가변성을 판단하는 방법이지 타입 검사기가 가변성을 판단한느 것과는 별개의 문제이다. 직관적으로는 "타입 매개변수를 사용한 곳에 따라 정해진다"라고 알면 되지만 실제로 코드를 작성하라면 타입 검사기가 가변성을 판단하는 방법을 알아야 한다. | ||
|
||
타입 검사기의 서브타입 판단 방법이 두 가지인 것처럼 가변성 판단 방법 역시 두 가지이다. 하나는 제네릭 타입을 정의할 때 가변성을 지정하도록 한 뒤 그에 따르는 것이고, 다른 하나는 사용할 때 가변성을 지정하도록 한 뒤 그에 따르는 것이다. *언어마다 사용하는 방법이 다르다.* | ||
|
||
#### 정의할 때 가변성 지정하기 | ||
|
||
가변성은 각 제네릭 타입의 고유한 속성이다. 따라서 제네릭 타입을 정의할 때 가변성을 지정하는 것이 가장 직관적이다. 이를 C#에서는 `out` 키워드로 지정한다. *참고로 C#의 제네릭은 기본적으로 불변이다.* | ||
|
||
```cs | ||
abstract class List3<out T> | ||
{ | ||
T get(int index); | ||
} | ||
``` | ||
|
||
`List3<Any>` == `List3<Person>` == `List3<Student>`이다. | ||
|
||
따라서 out키워드를 붙이고 내부에서 값을 수정하는(제네릭 타입 매개변수를 지닌) 메서드를 사용한다면 타입 검사기가 코드를 거부한다. | ||
|
||
타입 매개변수를 반변으로 만들고 싶다면 `in` 키워드를 사용한다. 이는 그 타입 매개변수를 입력에만 사용한다는 뜻이다. | ||
|
||
```cs | ||
abstract class Map<in K, V> | ||
{ | ||
V get(K key); | ||
void add(K key, V value); | ||
} | ||
``` | ||
|
||
K의 경우 입력에서만 사용되니 반변이 가능하고 V의 경우에는 불변이다. 따라서 Map<A, C>가 Map<B, C>의 서브타입이 가능하다. | ||
|
||
공변과 반변은 일종의 트레이드 오프로 타입을 공변으로 만든다면 타입 매개변수를 입력에 사용하는 절반을 모두 포기해야 하고, 반볍으로 만든다면 나머지 절반을 포기해야 한다. 그러니 공변이나 반변으로 만든 클래스는 반쪽짜리 클래스를 만들 수밖에 없다. | ||
|
||
#### 사용할 때 가변성 지정하기 | ||
|
||
제네릭 타입을 사용할 때 가변성을 지정하는 경우, 제네릭 타입을 정의할 때는 가변성을 지정할 수 없다. 모든 제네릭 타입은 불변으로 정의되며 타입 매개변수를 아무 데서나 사용할 수 있다. | ||
|
||
따라서 List가 불변이므로 B가 A의 서브타입이라면 List<B>가 List<A>의 서브타입이 아니다. 따라서 out이나 in을 타입 인자 앞에 붙여서 사용한다. List<out Person>이나 List<in Student>와 같이 사용한다. *이런 타입들은 제네릭 타입을 공변이나 반변으로 만드는 대신 기존 제네릭 타입보다 적은 기능을 제공한다.* | ||
|
||
out을 붙인 경우 실제 클래스에 존재하는 메서드중 출력 기능만 사용이 가능한 객체가 주어진다. in의 경우 입력 기능만 사용이 가능한 객체가 주어진다. | ||
|
||
### 정리 | ||
|
||
개인적으로 `C#`의 IEnumerable에 대해서 공부할 때 해당 내용을 정리한 경험이 있어서 잘 이해가 된 것 같다. 당시에도 어렵게 이해한 내용이 많았고, C# 키워드에서도 `in`, `out`키워드가 존재해서 좀 더 쉽게 이해한 것 같다. | ||
|
||
### 논의사항 | ||
|
||
저는 가변성 부분에서 IEnumerable이나 C#의 제네릭 델리게이트 타입인 Func이 생각이 났습니다. 아마 제가 자주 사용해서 그런 것 같은데 다른 언어에도 이렇게 사전에 정의되어 있는 제네릭 델리게이트가 있는지 궁금합니다. 책에서는 함수 타입이라고 하는 것 같습니다. | ||
|
||
```cs | ||
public delegate TResult Func<in T,out TResult>(T arg); | ||
``` | ||
|
||
- Func의 경우 반환 값이 있기 때문에 out을 사용했기에 TResult는 공변이고, T는 반변 | ||
- 비슷하게 Action의 경우엔 in만 존재 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
자바에도 있습니다. 그 예제에 대해서 @ymkim97 님이 간단하게 설명해 주셨던 것 같아요.
하지만 delegate는 없기 때문에 제네릭 클래스와 사용 방법이 같을 겁니다.
그 외에 타입스크립트는 잠깐 검색해 보니 같은 특징을 모두 가지고 있는 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추가로 제가 몇 년 전에도 아래 코드가 왜 같다고 판단 못하는거지? 라는 의문을 많이 가지고 있었는데
이번에 책 읽고 또 정확하게 알게 됐습니다.
이걸 공식 문서 정의를 보면 다음과 같긴 합니다.
Func< T, TResult >
Predicate< T >
그래서 이 책을 읽기 전 지식으로는 Func의 TResult나 Predicate T나 타입이 같으면 같다고 봐야 하는게 아닌가? 였는데
이제 이 책을 읽고 난 후 TResult는 out parameter가 공변이고 T는 in 파라미터라 반공변이라서 이제 설명할 수 있습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러가 다음과 같이 나오는데 애초에 타입 자체가 안맞다... 로 표현하네요.
시그니처는 같아도 Func와 Predicate는 다른 타입이다라고 해석하는 것 같습니다.
error CS0029: Cannot implicitly convert type 'System.Predicate' to 'System.Func<int, bool>'