9.1 제네릭 타입 파라미터
- 제네릭스를 사용하면 타입 파라미터를 받는 타입을 정의할 수 있다.
- 컴파일러는 타입 인자도 추론할 수 있다.
val authors = listOf("Dmitry", "Svetlana") // <String> 안붙이고 타입 추론을 했다
- 반면에 빈 리스트를 만들어야 한다면 타입 인자를 추론할 근거가 없기 때문에 직접 타입 인자를 명시해야한다.
val readers: MutableList<String> = mutableListOf()
val readers = mutableListOf<String>()
9.1.1 제네릭 함수와 프로퍼티
- 제네릭 함수를 호출할 때는 반드시 구체적 타입으로 타입 인자를 넘겨야한다.
- 실제로는 대부분 컴파일러가 타입 인자를 추론할 수 있으므로 그럴 필요가 없다.
public fun <T> List<T>.slice(indices: IntRange): List<T>
fun main() {
val letters = ('a'..'z').toList()
println(letters.slice<Char>(0..2)) // [a, b, c]
println(letters.slice(10..13)) // [k, l, m ,n] (타입 추론 O)
}
- 람다 파라미터에 대해 자동으로 만들어진 변수 it의 타입은 T라는 제네릭 타입이다.
- 컴파일러는 filter가 List<T> 타입의 리스트에 대해 호출될 수 있다는 사실과 filter의 수신 객체인 reader의 타입이 List<String>이라는 사실을 알고 T가 String이라는 사실을 추론한다.
fun<T>List<T>.filter(predicate: (T) -> Boolean): List<T>
fun main() {
val authors = listOf("Dmitry", "Svetlana") // List<String>
val readers = mutableListOf<String>(/*....*/)
readers.filter { it !in authors }
}
- 제네릭 함수를 정의할 땍와 마찬가지 방법으로 제네릭 확장 프로퍼티를 선언할 수 있다.
val <T>List<T>.penultimate: T // 모든 리스트 타입에 이 제네릭 확장 프로퍼티를 사용할 수 있다.
get() = this[size - 2]
fun main() {
println(listOf(1,2,3,4).penultimate) // 3
// 이 호출에서 타입 파라미터는 Int로 추론된다.
}
- 확장 프로퍼티만 제네릭하게 만들 수 있다. 일반 프로퍼티는 타입 파라미터를 가질 수 없다.
- 클래스 프로퍼티에 여러 타입의 값을 저장할 수는 없으므로 제네릭한 일반 프로퍼티는 말이 되지 않는다.
- 일반 프로퍼티를 제네릭하게 정의하면 컴파일러가 다음과 같은 오류를 표시한다.
val <T> x: T = TODO() // Type parameter of a property must be used in its receiver type
9.1.2 제네릭 클래스 선언
- 코틀린에서도 타입 파라미터를 넣은 <>를 클래스 이름 뒤에 붙이면 클래스를 제네릭하게 만들 수 있다.
interface List<T> { // List 인터페이스에 T라는 타입 파라미터 정의
operator fun get(index: Int): T // 인터페이스 안에서 T를 일반 타입처럼 사용할 수 있다.
// ...
}
- 제네릭 클래스를 확장하는 클래스를 정의하려면 기반 타입의 제네릭 파라미터에 대해 타입 인자를 지정해야 한다.
class StringList: List<String> { // 이 클래스는 구체적인 타입 인자로 String을 지정해 List를 구현한다
override fun get(index: Int): String {
TODO("Not yet implemented")
}
}
class ArrayList<T>: List<T> { // ArrayList의 제네릭 타입 파라미터 T를 List의 타입 인자로 넘긴다
override fun get(index: Int): T {
TODO("Not yet implemented")
}
}
9.1.3 타입 파라미터 제약
- 타입 파라미터 제약은 클래스나 함수에 사용할 수 있는 타입인자를 제한하는 기능이다.
- 어떤 타입을 제네릭 타입의 타입 파라미터에 대한 상한으로 지정하면 그 제네릭 타입을 인스턴스화할 때 사용하는 타입 인자는 반드시 그 상한 타입이거나 그 상한 타입의 하위 타입이어야한다.
fun <T: Number> List<T>.sum(): T //타입 파라미터에 Number라는 상한을 지정했다
listOf(1,2,3).sum() // 실제 타입 인자 Int가 Number를 확장하므로 실행에 문제가 없다.
- 타입 파라미터 T에 대한 상한을 정하고 나면 T타입의 값을 그 상한 타입의 값으로 취급 할 수 있다.
fun <T: Number> oneHalf(value: T): Double { // Number를 타입 파라미터 상한으로 정한다
return value.toDouble() / 2.0 // Number 클래스에 정의된 메소드를 호출한다.
}
fun main() {
println(oneHalf(3)) // 1.5
}
9.1.4 타입 파라미터를 널이 될 수 없는 타입으로 한정
- 제네릭 클래스나 함수를 정의하고 그 타입을 인스턴스화할 때는 널이 될 수 있는 타입을 포함하는 어떤 타입으로 타입 인자를 지정해도 타입 파라미터를 치환할 수 있다.
- 상한을 정하지 않은 타입 파라미터는 Any? 를 상한으로 정한 파라미터와 같다.
class Processor<T> {
fun process(value: T) {
value?.hashCode() // value는 nullable하기 때문에 안전한 호출을 사용해야 한다.
}
}
- 널이 될 수 없는 타입만 타입 인자로 받게 만들려면 타입 파라미터에 제약을 가해야한다.
class Processor<T: Any> { // null이 될 수없는 타입 상한을 지정한다.
fun process(value: T) {
value.hashCode() //value는 null이 될 수 없다
}
}
9.2 실행 시 제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터
- jvm의 제네릭스는 보통 타입 소거를 사용해 구현된다.
- 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다는 뜻이다.
- 함수를 inline으로 만들면 타입 인자가 지워지지 않게 할 수 있다.
9.2.1 실행 시점의 제네릭: 타입 검사와 캐스트
- 코틀린 제네릭 타입 인자 정보는 런타임에 지워진다.
- ex. List<String> 런타임에는 List로만 본다
- 컴파일러는 두 리스트를 서로 다른 타입으로 인식하지만 실행 시점에 그 둘은 완전히 같은 객체다.
fun main() {
val list1: List<String>
val list2: List<Int>
}
타입 소거로 인해 생기는 한계
- 타입 인자를 따로 저장하지 않기 때문에 런타임에 타입 인자를 검사할 수 없다.
타입 소거의 장점
- 저장해야하는 타입 정보의 크기가 줄어들어서 메모리 사용이 줄어들긴한다.
스타 프로젝션
- 코틀린에서는 타입 인자를 명시하지 않고 제네릭 타입을 사용할 수 없다.
- 어떤 값이 집합이나 맵이 아니라 리스트라는 사실을 확인 할때는 스타 프로젝션을 이용하면 된다.
fun main() {
val value = listOf(1)
if (value is List<String>) { // Cannot check for instance of erased type: List<String>
//...
}
}
fun main() {
val value = listOf(1)
if (value is List<*>) { // good
//...
}
}
- 인자를 알 수 없는 제네릭 타입을 표현할때 스타 프로젝션을 사용한다.
- as나 as? 캐스팅에도 여전히 제네릭 타입을 사용할 수 있다.
- 실행 시점에는 제네릭 타입의 타입 인자를 알 수 없으므로 캐스팅은 항상 성공한다.
- 컴파일러가 캐스팅 관련 경고를 한다는 점을 제외하면 문제없이 실행된다.
fun printSum(c: Collection<*>) {
val intList = c as? List<Int> // Unchecked cast: Collection<*> to List<Int>
?: throw IllegalArgumentException("list is expected")
println(intList.sum())
}
fun main() {
printSum(listOf(1, 2, 3))
}
- 하지만 잘못된 타입의 원소가 들어있는 리스트를 전달하면 실행 시점에 ClassCastException이 발생한다.
- 어떤 값이 List<Int>인지 검사할 수는 없으므로 IllegalArgumentException이 발생하지는 않는다.
- 따라서 as? 캐스트가 성공하고 문자열 리스트에 대해 sum 함수가 호출되고, 예외가 발생한다.
fun printSum(c: Collection<*>) {
val intList = c as? List<Int> // Unchecked cast: Collection<*> to List<Int>
?: throw IllegalArgumentException("list is expected")
println(intList.sum())
}
fun main() {
printSum(listOf('1', '2', '3')) // ClassCastException
}
- 코틀린 컴파일러는 컴파일 시점에 타입 정보가 주어진 경우에는 is 검사를 수행하게 허용한다.
- 컴파일 시점에 c 컬렉션이 Int 값을 저장한다는 사실이 알려져 있으므로 is 검사를 할 수 이싿.
fun printSum(c: Collection<Int>) {
if (c is List<Int>) {
println(c.sum())
}
}
9.2.2 실체화한 타입 파라미터를 사용한 함수 선언
- 제네릭 함수가 호출되도 그 함수의 본문에서는 호출 시 쓰인 타입 인자를 알 수 없다.
- 하지만 인라인 함수의 타입 파라미터는 실체회되므로 실행 시점에 인라인 함수의 타입 인자를 알 수 있다.
- 함수를 인라인 함수로 만들고 타입 파라미터를 reified로 지정하면 타입 인자를 실행 시점에 검사할 수 있다.
inline fun <**reified** T> isA(value: Any) = value is T
fun main() {
println(isA<String>("abc")) // true
println(isA<String>(123)) // false
}
- 타입 인자를 실행 시점에 알 수 있다.
fun main() {
val items = listOf("one",2,"three")
println(items.**filterIsInstance**<String>()) // [one, three]
}
- 코틀린 표준 라이브러리에 있는 filterIsInstance 선언을 간단하게 정리한 코드
inline fun <reified T> // reified 키워드는 이 타입 파라미터가 실행 시점에 지워지지 않음을 표시한다.
Iterable<*>.filterIsInstance(): List<T> {
val destination = mutableListOf<T>()
for (element in this) {
if (element is T) { // 각 원소가 타입 인자로 지정한 클래스의 인스턴스인지 검사할 수 있다.
destination.add(element)
}
}
return destination
}
인라인 함수에서만 타입 인자를 실행 시점에 알 수 있는 이유
- 컴파일러는 타입 인자로 쓰인 구체적인 클래스를 참조하는 바이트코드를 생성해 삽입할 수 있다.
- 타입 파라미터가 아니라 구체적인 타입을 사용하므로 만들어진 바이트코드는 실행 시점에 벌어지는 타입 소거의 영향을 받지 않는다.
- filterIsInstance<String> 호출은 아래와 동등한 바이트 코드를 만들어낸다.
for (element in this) {
if (element is String) {
destination.add(element)
}
}
9.2.3 실체화한 타입 파라미터로 클래스 참조 대신
- 표준 자바 api인 ServiceLoader를 사용해 서비스를 읽어 들이려면 아래와 같이 호출해야한다.
- Service::class.java 라는 코드는 Service.class 라는 자바코드와 완전히 같다.
val serviceImpl = ServiceLoader.load(Service::class.java)
- 읽어 들일 서비스 클래스를 loadService 함수의 타입 인자로 지정한다.
fun main() {
val serviceImpl = loadService<Service>()
}
inline fun <**reified** T> loadService() {
return ServiceLoader.load(T::class.java)
}
9.2.4 실체화한 타입 파라미터의 제약
can
- 타입 검사와 캐스팅
- 코틀린 리플렉션 API
- 코틀린 타입에 대응하는 Java.lang.Classfmf djerl
- 다른 함수를 호출할 때 타입 인자로 사용
can’t
- 타입 파라미터 클래스의 인스턴스 생성하기
- 타입 파라미터 클래스의 동반 객체 메소드 호출하기
- 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
- 클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정하기
마지막 제약으로 인해 파급효과가 하나 생긴다
- 실체화한 타입 파라미터를 인라인 함수에만 사용할 수 있으므로 실체화한 타입 파라미터를 사용하는 함수는 자신에게 전달되는 모든 람다와 함께 인라이닝된다.
9.3 변성: 제네릭과 하위 타입
변성이란?
- List<String>, List<Any>와 같이 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관게가 있는지 설명하는 개념이다.
9.3.1 변성이 있는 이유: 인자를 함수에 넘기기
- 이 함수는 각 원소를 Any로 취급하며 모든 문자열은 Any타입이기도 하므로 안전하다.
fun printContents(list: List<Any>) {
println(list.joinToString())
}
fun main() {
printContents(listOf("a", "b")) // a, b
}
- 이 경우에는 컴파일 에러가 발생한다.
fun addAnswer(list: MutableList<Any>) {
list.add(42)
}
fun main() {
val strings = mutableListOf("a", "b")
addAnswer(strings)
}
- 코틀린에서는 리스트의 변경 가능성에 따라 적절한 인터페이스를 선택하면 안전하지 못한 함수 호출을 막을 수 있다.
9.3.2 클래스, 타입, 하위 타입
- 클래스와 타입의 제일 큰 차이점은 코틀린 클래스가 적어도 둘 이상의 타입을 구성할 수 있다는 것이다.
- 타입 사이의 관계를 논하기 위해 하위 타입이라는 개념을 잘 알아야한다.
하위 타입
- 어떤 타입 A의 값이 필요한 모든 장소에 어떤 타입 B의 값을 넣어도 문제가 없다면 타입 B는 타입 A의 하위 타입이다. (상위 타입이 더 Scope이 크다)
- 컴파일러는 변수 대입이나 함수 인자 전달 시 하위 타입 검사를 매번 수행한다.
- 어떤 값의 타입이 변수 타입의 하위 타입인 경우에만 값을 변수에 대입하게 허용한다.
- 어떤 인터페이스를 구현하는 클래스의 타입은 그 인터페이스 타입의 하위 타입이다.
fun test(i: Int) {
val n: Number = i // Int가 Number의 하위 타입이어서 컴파일 가능
fun f(s: String){
}
f(i) // Int가 String의 하위 타입이 아니어서 컴파일되지 않는다.
}
- 널이 될 수 없는 타입은 널이 될 수 있는 타입의 하위 타입이다.
fun main() {
val s: String = "A"
val t: String? = s // String이 String?의 하위 타입이므로 합법이다.
}
- 제네릭 타입을 인스턴스화 할 때 타입 인자로 서로 다른 타입이 들어가면 인스턴스 타입 사이의 하위 타입 관계가 성립하지 않으면 그 제네릭 타입을 무공변이라고 한다.
- 예를들어 type A ≠ type B 일때 MutableList<A>는 MutableList<B>의 하위 타입이 아니다.
- 자바에서는 모든 클래스가 무공변이다.
- A가 B의 하위 타입이면, List<A>는 List<B>의 하위 타입이다. 그런 클래스나 인터페이스를 공변적이라 말한다.
9.3.3 공변성: 하위 타입 관계를 유지
- 예를 들어 Cat이 Animal의 하위 타입이기 때문에 Producer<Cat>은 Producer<Animal>의 하위 타입이다. 이를 하위 타입 관계가 유지된다고 말하고 이때 Producer는 공변적이다.
- 코틀린에서 제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시하려면 타입 파라미터 이름 앞에 out을 넣어야한다.
interface Producer<out T> { // 클래스 T에 대해 공변적이라고 선언한다.
fun produce(): T
}
- 클래스의 타입 파라미터를 공변적으로 만들면 함수 정의에 사용한 파라미터 타입과 타입 인자의 타입이 정확히 일치하지 않더라도 그 클래스의 인스턴스를 함수 인자나 반환 값으로 사용할 수 있다.
- 타임 파라미터를 공변적으로 지정하면 클래스 내부에서 그 파라미터를 사용하는 방법을 제한한다.
- 타입 안전성을 보장하기 위해 공변적 파라미터는 항상 아웃 위치에만 있어야한다. ⇒ 클래스가 T 타입의 값을 생산할 수는 있지만 T타입의 값을 소비할 수는 없다는 뜻
- 클래스 멤버를 선언할 때 타입 파라미터를 사용할 수 있는 지점은 모두 인과 아웃위치로 나뉜다.
- T가 함수의 반환 타입에 쓰이면 out (T타입의 값을 생산)
- T가 함수의 파라미터 타입에 쓰인다면 T는 in (T 타입의 값을 소비)
interface Transformer<T> {
fun translation(t: T/***in*/**): T/***out*/**
}
- 클래스 타입 파라미터 T 앞에 out 키워드를 붙이면 out 위치에서만 T를 사용하게 허용한다
- out 키워드는 T의 사용법을 제한하며 T로 인해 생기는 하위 타입 관계의 타입 안전성을 보장한다.
- 이 클래스를 공변적으로 선언해도 안전하다
class Herd<out T: Animal> {
val size: Int get() {...}
operator fun get(i: Int): T {...} // T를 반환 타입으로 사용한다 (out)
}
타입 파라미터 T에 붙은 out 키워드의 의미
- 공변성: 하위 타입 관계가 유지된다.
- 사용 제한: T를 아웃 위치에서만 사용할 수 있다.
- MutableList는 T에 대해 공변적일 수 없다.(T가 in 위치에 있기 때문)
interface MutableList<T> : List<T>, MutableCollection<T> {
override fun add(element: T /***in*/**): Boolean
}
- 만약 공변적으로 선언된 경우는?
interface MutableList<out T> : List<T>, MutableCollection<T> {
override fun add(element: T /*Type parameter T is declared as 'out' but occurs in 'in' position in type T*/): Boolean
}
생성자 파라미터는 인이나 아웃 어느 쪽도 아니다
- 변성은 위험할 여지가 있는 메소드를 호출할 수 없게 만듦으로써 제네릭 타입의 인스턴스 역할을 하는 클래스 인스턴스를 잘못 사용하는 일이 없게 방지하는 역할을 한다.
- 변성 규칙은 클래스 외부에서 클래스를 잘못 사용하는 일을 막기 위한것이므로 클래스 내부 구현에는 적용되지 않는다.(private 메소드의 파라미터는 인도 아니고 아웃도 아닌 위치다)
9.3.4 반공변성: 뒤집힌 하위 타입 관계
반공변성
- 반공변 클래스의 하위 타입 관계는 공변 클래스의 경우와 반대다.
- 이 인터페이스의 메소드는 T 타입의 값을 소비하기만 한다.
interface Comparator<in T> {
fun compare(e1: T, e2: T): Int {...} // T를 in 위치에 사용한다.
}
- 타입 B가 타입 A의 하위 타입인 경우 Consumer<A>가 Consumer<B>의 하위 타입인 관계가 성립하면 제네릭 클래스 Consumer<T>는 타입 인자 T에 대해 반공변이다.
- in 키워드를 타입 인자에 붙이면 그 타입 인자를 오직 인 위치에서만 사용할 수 있다는 뜻이다.
공변성 반공변성 무공변성
Producer<out T> | Consumer<in T> | MutableList<T> |
타입 인자의 하위 타입 관계가 제네릭 타입에서도 유지된다. | 타입 인자의 하위 타입 관계가 제네릭 타입에서 뒤집힌다. | 하위 타입 관계가 성립하지 않는다 |
Producer<Cat>은 Producer<Animal>의 하위 타입이다. | Consumer<Animal>은 Consumer<Cat>의 하위 타입이다. | |
T를 아웃 위치에서만 사용할 수 있다. | T를 인 위치에서만 사용할 수 있다. | T를 아무데서나 사용 가능하다. |
- 클래스나 인터페이스가 어떤 타입 파라미터에 대해서는 공변적이면서 다른 타입 파라미터에 대해서는 반공변적일 수도 있다.
interface Function1<in P, out R> {
operator fun invoke(p: P): R
}
- Animal은 Cat의 상위 타입이며 Int는 Number의 하위 타입이므로, 이 코드는 합법이다.
fun enumerateCats(f: (Cat) -> Number) {...}
fun Animal.getIndex(): Int = ...
enumerateCats(Animal::getIndex)
9.3.5 사용 지점 변성: 타입이 언급되는 지점에서 변성 지정
선언 지점 변성
- 클래스를 선언하면서 변성을 지정하는 것
사용 지점 변성
- 자바같이 해당 타입 파라미터를 하위 타입이나 상위 타입중 어떤 타입으로 대치할 수 있는지 명시하는 것
- 코틀린도 사용 지점 변성을 지원한다.
- 따라서 클래스 안에서 어떤 타입 파라미터가 공변적이거나 반공변적인지 선언할 수 없는 경우에도 특정 타입 파라미터가 나타나는 지점에서 변성을 정할 수 있다.
fun <T : R, R> copyData(source: MutableList<T>, destination: MutableList<R>) {
for (item in source) {
destination.add(item)
}
}
fun main() {
val ints = mutableListOf(1, 2, 3)
val anyItems = mutableListOf<Any>()
copyData(ints, anyItems) // Int가 Any의 하위 타입이므로 이 함수를 호출 할 수 있다.
println(anyItems) // [1,2,3]
}
- 코틀린에서는 우아하게 표헌할 수 있다.
- 함수 구현이 아웃 위치에 있는 타입 파라미터를 사용하는 메소드만 호출한다면 그런 정보를 바탕으로 함수 정의 시 타입 파라미터에 변성 변경자를 추가할 수 있다.
fun <T> copyData(source: MutableList<out T>, destination: MutableList<T>) {
/*out 키워드를 타입을 사용하는 위치 앞에 붙이면 T타입을 in 위치에 사용하는 메소드를 호출하지 않는다는 뜻이다.*/
for (item in source) {
destination.add(item)
}
}
- 타입 선언에서 타입 파라미터를 사용하는 위치라면 어디에나 변성 변경자를 붙일 수 있다.
- 변성 변경자를 붙이면 타입 프로젝션이 일어난다.
타입 프로젝션
- 변성 변경자를 붙이면 프로젝션을 한 타입으로 만든다.(제약을 가한)
- 컴파일러는 타입 파라미터T를 함수 인자 타입으로 사용하지 못하게 막는다.
- List와 MutableList처럼 공변적인 읽기 전용 인터페이스와 무공변적인 인터페이스가 나뉘어 있지 않은 클래스의 경우 프로젝션이 유용하다.
- 사용 지점 변성을 사용하면 타입 인자로 사용할 수 있는 타입의 범위가 넓어진다.
9.3.6 스타 프로젝션: 타입 인자 대신 * 사용
- 제네릭 타입 인자 정보가 없을 표현하기 위해 스타 프로젝션을 사용한다.
- MutableList<*>는 MutableList<Any?>와 같지 않다.
- MutableList<*>는 어떤 정해진 구체적인 타입의 원소만을 담는 리스트지만 그 원소의 타입을 정확히 모른다는 사실을 표현한다.
- 반면 MutableList<Any?>는 모든 타입의 원소를 담을 수 있다. (코틀린에서 모든 타입의 상위 타입이 Any? 이기 때문이다.)
- 스타 프로젝션을 쓰는 쪽에 더 간결하지만 제네릭 타입 파라미터가 어떤 타입인지 굳이 알 필요가 없을 때만 스타 프로젝션을 사용할 수 있다.
- 스타 프로젝션을 사용할 때는 값을 만들어내는 메소드만 호출할 수 있고 그 값의 타입에는 신경을 쓰지 말아야한다.
fun printFirst(list: List<*>) { // 모든 리스트를 인자로 받을 수 있다.
if (list.isNotEmpty()) { // isNotEmpty()에서는 제네릭 타입 파라미터를 사용하지 않는다.
println(list.first()) // first()는 이제 Any?를 반환하지만 여기서는 그 타입만으로 충분하다.
}
}
>>> printFirst(listOf("Svetlana", "Dmitry"))
Svetlana
9.4 요약
- 자바와 마찬가지로 제네릭 타입의 타입 인자는 런타임시에는 존재하지 않는다.
- 그래서 is 연산자를 사용해 검사할 수 없다.
- 인라인 함수의 타입 매개변수를 reified로 표시해서 실체화하면 실행 시점에 그 타입을 is로 검사하거나 java.lang.Class 인스턴스를 얻을 수 있다.
- 제네릭 클래스의 타입 인자가 어떤 타입인지 정보가 없거나 타입 인자가 어떤 타입인지가 중요하지 않을 때 스타 프로젝션 구문을 사용할 수 있다.
'kotlin' 카테고리의 다른 글
[jackson] 데이터가 null일때 empty list로 바꾸는 옵션 (0) | 2022.09.19 |
---|---|
11. DSL 만들기 (0) | 2022.04.28 |
10. 애노테이션과 리플렉션 (0) | 2022.03.26 |
8. 고차 함수: 파라미터와 반환 값으로 람다 사용 (0) | 2022.02.13 |
7. 연산자 overloading과 관례 (0) | 2022.01.23 |