타입 파라미터

List라는 타입이 있다면 그 안에 들어가는 원소의 타입을 안다면 도움이 될 것이다. 타입 파라미터를 사용한다면 ‘이 변수는 리스트다’ 대신 ‘이 변수는 문자열을 담는 리스트다’ 로 의미를 명확하게 할 수 있다.

코틀린에서 ‘문자열을 담는 리스트’ 를 표현하는 구문은 자바와 같은 List<String>이다. 이런 타입 파라미터는 여러개 있을 수 있다(Map<String, Person>).

코틀린은 타입 인자 또한 추론이 가능하다.

val authors = listOf("Dmitry", "Svetlana")

listOf에 전달돤 값이 String이기 때문에 컴파일러는 이 리스트가 List<String>임을 추론할 수 있다. 다만 빈 리스트를 만들 때는 추론할 근거가 없기 때문에 직접 명시해야 한다. 변수의 타입을 직접 지정할 수도 있고 타입 인자를 지정할 수도 있다.

val readers: MutableList<String> = mutableListOf()
val readers = mutableListOf<String>()

제네릭 함수와 프로퍼티

제네릭 함수를 선언하는 방법은 다음과 같다.

fun <T> List<T>.slice(indices: IntRange): List<T>

제네릭 함수를 선언할 때는 타입 파라미터를 fun 키워드 오른쪽에 선언한다. 그러면 타입 파라미터로 선언한 T를 수신 객체와 반환 타입에 사용할 수 있다. 이 함수는 다음과 같이 호출할 수 있다.

val letters = ('a'...'z').toList()
letters.slice<Char>(0..2)

위 함수를 호출할 때는 타입 인자를 명시적으로 지정할 수 있지만 대부분은 컴파일러가 타입 인자를 추론할 수 있기 때문에 타입 인자를 생략할 수 있다.

letters.slice(10..13) //컴파일러가 T가 Char라는 사실을 추론한다.

제네릭 고차 함수

조금 더 응용해 제네릭 고차 함수를 만들 수 있다.

fun <T> List<T>.filter(predicate: (T) -> Boolean): List<T> { /*...*/ }

val authors = listOf("Dmitry", "Svetlana")
val readers = mutableListOf<String>( /* ... */ )
readers.filter { it !in authors }

여기서 it의 타입은 T라는 제네릭 타입이다. 컴파일러는 filterList<T>타입의 리스트에서 호출할 수 있으며 filter의 수신 객체인 reader의 타입이 List<String>임을 통해 TString임을 추론할 수 있다.

제네릭 확장 프로퍼티

제네릭 함수와 마찬가지로 제네릭 확장 프로퍼티도 선언할 수 있다. 여기서 중요한 것은 일반 프로퍼티는 타입 파라미터를 가질 수 없기 때문에 제네릭하게 만들 수 없다.

val <T> List<T>.penultimate: T //리스트의 마지막 원소 바로 앞의 원소를 반환
	get() = this[size - 2]

println(listOf(1, 2, 3, 4).penultimate)
// 3

제네릭 클래스

자바와 마찬가지 타입 파라미터를 넣은 꺽쇠 괄호(<>)를 클래스 또는 인터페이스 이름 뒤에 붙여 제네릭한 클래스 또는 인터페이스를 만들 수 있다.

interface List<T> {
	operator fun get(index: Int): T //T를 일반 타입처럼 사용할 수 있다.
  //...
}

제네릭 클래스를 확장하는 클래스를 정의하려면 기반 타입의 제네릭 파라미터에 대해 타입 인자를 지정해야 한다. 구체적인 타입을 지정할 수도 있고 타입 파라미터로 받은 타입을 넘길 수도 있다.

class StringList : List<String> {
  override fun get(index: Int): String = ...
}
class ArrayList<T> : List<T> {
  override fun get(index: Int): T = ...
}

클래스가 자기 자신을 타입 인자로 참조할 수도 있다.

class String : Comparable<String> {
  override fun compareTo(other: String): Int = /* ... */
}

타입 파라미터 제약

클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능이다. 제약을 가하려면 타입 파라미터 이름 뒤에 콜론을 표시하고 그 뒤에 상한 타입을 적으면 된다. 자바의 <T extends Number>와 같은 개념이다.

fun <T : Number> List<T>.sum(): T

타입 파라미터에 상한을 정하면 그 타입의 값을 상한 타입의 값으로 취급할수 있다. Number를 예시로 들면

fun <T : Number> oneHalf(value: T): Double {
  return value.toDouble() / 2.0 //Number클래스에 정의된 메소드를 호출할 수 있다.
}

여러 제약을 가하기

드물지만 파라미터에 둘 이상의 제약을 가해야 할 수도 있다. 이럴 땐 다음과 같이 한다.

fun <T> ensureTrailingPeriod(seq: T)
	where T : CharSequence, T : Appendable {
    if(!seq.endsWith('.')) {
      seq.append('.')
    }
  }

타입 파라미터를 널이 될 수 없는 타입으로 한정하기

아무런 상한을 정하지 않은 타입 파라미터는 결과적으로 Any?를 상한으로 정한 파라미터와 같다.

class Processor<T> {
  fun process(value: T) {
    value?.hashCode() //value 는 null이 될 수 있기 때문에 안전한 호출을 사용해야 한다.
  }
}

널이 될 수 없는 타입만 타입 인자로 받으려면 타입 파라미터에 제약을 가해야 한다.

class Processor<T : Any> {
	fun process(value: T) {
		value.hashCode()
	}
}

이런 제약은 T 타입이 항상 널이 되지 않음을 보장하기 때문에 String?같은 타입을 거부한다(String?Any?의 자손 타입이고, Any?Any보다 덜 구체적인 타입이다).


© 2021. All rights reserved.

Powered by Hydejack v9.1.6