Generics 는 자바에도 있었던 개념이다. 클래스나 함수 등을 정의할 때, 파라미터 및 변수의 타입을 명시하지 않는 것이다. 즉, 어떤 타입이든 수용할 수 있는 형태가 된다. 따라서 클래스나 함수를 다양한 타입으로 오버로딩하여 여러 가지 정의해두지 않아도, 외부에서 타입을 정해줄 수 있다. 이번 포스팅에선 이 Generics 를 활용한 클래스와 함수 등을 정의하는 방법에 대해서 알아보자.
타입이 정해지지 않는 변수 및 파라미터의 경우, 함수의 이름 앞에 명시를 해줘야 한다. 아래 예시는 T 라는 제너릭 타입 변수를 사용하기 때문에 함수 이름 앞에 <T>
라고 명시해주었다. T 타입 파라미터 a
과 b
를 더하여 T 타입 형태로 결과를 리턴하는 동작을 한다.
fun <T> add(a: T, b: T): T {
return (a.toString().toDouble() + b.toString().toDouble()) as T
}
fun main() {
println(add(1, 2))
println(add(1.5, 2.7))
println(add(3L, 5))
println(add(1.5, 4))
}
3
4.2
8
5.5
제너릭 함수를 정의할 땐, 호환성을 높이기 위해 최대한 특정 데이터 타입과 관계없이 동작을 하게끔 해야 하는 것이 중요하다.
마찬가지로 제너릭 타입을 명시해줘야 하는데, 함수와 달리 이름 다음에 명시해주면 된다.
class Rectangle<T>(val width: T, val height: T)
그러나, 코틀린에선 제너릭 클래스를 인스턴스화 할 때 자바와 달리 제너릭 타입을 명시해줘야 한다.
val recA = Rectangle<Int>(10, 20)
val recB = Rectangle<String>("aa", "bb")
라고 했지만, 사실 킹갓 코틀린은 타입 추론이 가능하기 때문에 개발자로서는 생략해도 상관없다.
val recA = Rectangle(10, 20)
val recB = Rectangle("aa", "bb")
착각해선 안되는 게, 제너릭은 여러 개 있을 수 있다. (제너릭 타입 네이밍 룰은 다루지 않겠다)
class Rectangle<T, K, E>(val width: T, val height: T, val name: K, val color: E)
라고 느꼈다면 잘 따라온 것이다. 당연히 지금까지 살펴본 예제들은 오류가 뿜뿜하는 모양이다.
class Rectangle<T>(val width: T, val height: T) {
fun getArea(): T {
return (width.toDouble() * height.toDouble()) as T
}
}
당장 얘만 보더라도, width
와 height
에 꼭 숫자만 와야되는 상황임에도 불구하고
val recB = Rectangle("aa", "bb")
이런 식으로 문자열이 들어가는 것도 허용돼버리는 문제가 발생한다. 이렇게 되면 "aa" 는 toDouble()
이 불가능하기 때문에 에러가 발생하고 말 것이다.
따라서, 제너릭에는 Constraints
(제한, 제약) 라는 개념이 있다. 개발 의도대로 전달 인자에 숫자만 허용되도록 만들고 싶다면, 아래와 같이 '제약'을 걸어주면 된다. 다형성을 활용하여 Super Type
을 명시해주는 것이다.
class Rectangle<T: Number>(val width: T, val height: T) {
fun getArea(): T {
return (width.toDouble() * height.toDouble()) as T
}
}
이렇게 제너릭의 Super Type
을 명시해주면, 이외의 타입은 컴파일 단계에서 에러를 발생하도록 해준다.
위 예제에서는 T
의 SuperType 으로 Number
만 허용하도록 했다. 여기에 추가해서 더 많은 제약을 걸어줄 수 있다. where
이라는 키워드를 사용하면 된다. 아래 예시에서 T
는, Number
의 자식 클래스여야 하고 Comparable
인터페이스를 구현한 객체여야 한다는 제약을 걸어주었다.
class Rectangle<T>(val width: T, val height: T)
where T: Number, T: Comparable<T> {
fun getArea(): T {
return (width.toDouble() * height.toDouble()) as T
}
}
다들 알겠지만 코틀린은 클래스의 경우 단일 상속만 가능하기 때문에 제약을 2개 이상 걸어준다 함은 1개 클래스를 상속하고, 1개 이상의 인터페이스를 구현한 형태를 허용하는 것이다.
이렇게 이번 포스팅에선 제너릭 함수와 클래스를 정의하는 방법에 대해서 알아보았다. 제너릭에는 불변성과 공변성이라는 개념이 등장하는데, 다음 포스팅에선 이러한 개념들과 이외 다양한 키워드에 대해서 알아보도록 하자.
https://kotlinlang.org/docs/generics.html#generic-constraints
헤로님 글 항상 잘읽고있어용