References: Do it! 코틀린 프로그래밍

코틀린에서의 제네릭에 대해 알아봅니다.

 

 

1. 제네릭

 

제네릭(Generic)은 클래스 내부에서 사용할 자료형을 나중에 인스턴스를 사용할 때 확정합니다.

자료형의 객체들을 다루는 메서드나 클래스에서 컴파일 타임에 자료형을 검사해 적당한 자료형을 선택할 수 있도록 하기 위해 제네릭이 도입되었습니다.

제네릭을 사용하면 객체의 자료형을 컴파일 타임에 체크하므로 객체 자료형의 안정성을 높이고 형 변환의 번거로움이 줄어듭니다.

 

제네릭은 앵글 브래킷(<>) 사이에 형식 매개변수를 넣어 선언합니다.

이때 형식 매개변수는 하나 이상 지정할 수 있습니다.

형식 매개변수는 자료형을 대표하는 용어로 T와 같은 특정 영문의 대문자로 사용합니다. 이는 나중에 필요한 자료형으로 대체됩니다.

 

제네릭은 다양한 자료형을 다워랴하는 컬렉션에 많이 사용됩니다.

컬렉션은 List, Set, Map등으로 다수의 데이터를 다루는 특별한 클래스로 제네릭을 사용해 정의되어 있습니다.

 

다음 예시를 통해 제네릭에 대해 알아보겠습니다.

 

class Box<T>(t: T) { var name = t }
fun main() {
    val box1: Box<Int> = Box<Int>(1) // Box(1)
    val box2: Box<String> = Box<String>("Hello") // Box("Hello")
    println(box1.name)
    println(box2.name)
}

 

  • Box<T>(t: T): 제네릭 사용. 형식 매개변수로 받은 인자를 name에 저장.

 

위 코드를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

형식 매개변수는 T입니다. T는 보통 Type을 줄인 것을 의미하며 강제적인 것은 아닙니다.

형식 매개면수는 관습적으로 다음을 사용합니다.

  • 요소(Element) - E
  • 키(Key) - K
  • 숫자(Number) - N
  • 형식(Type) - T
  • 값(Value) - V

 

앞선 예제에서의 Box<T>를 제네릭 클래스라고 합니다.

제네릭 클래스는 형식 매개변수를 하나 이상 받는 클래스입니다.

클래스를 선언할 때 자료형을 특정하지 않고 인스턴스를 생성하는 시점에서 자료형을 정합니다.

 

 

2. 자료형 변환

 

제네릭 클래스는 가변성을 지정하지 않으면 형식 매개변수에 상, 하위 클래스가 지정되어도 서로 자료형이 변환되지 않습니다.

다음 코드를 통해 확인해 보겠습니다.

 

open class Parent
class Child: Parent()
class Cup<T>
fun main() {
    val obj1: Parent = Child()
    val obj2: Child = Parent()
    val obj3: Cup<Parent> = Cup<Child>()
    val obj4: Cup<Child> = Cup<Parent>()
    val obj5 = Cup<Child>()
    val obj6: Cup<Child> = obj5
}

 

  • obj1: Parent 형식은 Child의 자료형으로 변환될 수 있습니다.
  • obj2: 자료형이 불일치해 오류가 발생합니다.
  • obj3: 자료형이 불일치해 오류가 발생합니다. 
  • obj4: 자료형이 불일치해 오류가 발생합니다.
  • obj5: obj는 Cup<Child> 자료형이 됩니다.
  • obj6: 자료형이 일치해 오류가 발생하지 않습니다.

 

obj3, obj4에서 보듯이 제네릭 클래스에서는 형식 매개변수인 T에 상위와 하위 클래스를 지정해도 서로 관련 없는 형식이 되어 형식이 일치하지 않는 오휴가 발생합니다.

또한 제네릭 형식 매개변수는 기본적으로 Null을 허용합니다. 만약 Null을 허용하지 않게 하고 싶다면 T: Any 형식을 사용하면 됩니다.

 

 

3. 제네릭 함수 및 메서드

 

형식 매개변수를 받는 함수나 메서드를 제네릭 함수 또는 메서드라고 합니다.

제네릭 함수나 메서드는 해당 함수나 메서드 앞에 <T>와 같이 형식 매개변수를 지정합니다.

<K, V>처럼 여러 형식 매개변수를 사용할 수도 있습니다.

자료형의 결정은 함수가 호출될 때 컴파일러가 자료형을 추론할 수 있습니다.

 

다음 예시를 통해 확인해 보도록 하겠습니다.

 

fun <T> find(a: Array<T>Target: T): Int{
    for( i in a.indices ) {
        if ( a[i] == Target ) { return i }
    }
    return -1
}
fun main() {
    val arr1: Array<String> = arrayOf("Apple", "Banana", "Cherry", "Durian")
    var arr2: Array<Int> = arrayOf(1, 2, 3, 4)
    println("arr.indices ${arr1.indices}")
    println(find<String>(arr1, "Cherry"))
    println(find(arr2, 2))
}

 

  • fun <T> find: 제네릭 함수
  • Array<T>: 배열을 위한 클래스
  • a.indices: a배열의 유효 범위

 

위의 코드를 실행시키면 다음과 같은 결과를 확인할 수 있습니다.

 

 

4. 제네릭과 람다식

 

형식 매개변수로 선언된 함구의 매개 변수를 연산할 경우 자료형을 경정할 수 없어 오류가 발생합니다.

 

fun <T> add ( a: T, b: T): T { 
    return a + b // 자료형을 아직 결정할 수 없어 오류 발생. 
}

 

하지만 람다식을 매개변수로 받으면 자료형을 결정하지 않아도 실행 시 람다식 본문을 넘겨줄 때 결정되므로 이런 문제를 해결할 수 있습니다.

 

다음과 같이 코드를 작성해 봅시다.

 

fun <T> add( a: T, b: T, op: ( T, T ) -> T ): T{
    return op( a, b )
}
fun main() {
    val result = add2, 3, { a, b -> a + b } )
    println(result)
}

 

  • op: ( T, T ) -> T: 매개변수에 람다식을 받습니다.
  • { a, b -> a + b }: 매개변수에 람다식을 전달합니다.

 

람다식은 add 함수가 실행될 때 넘겨지는 인자이므로 연산식을 함수 선언부에 직접 구현하지 않고 전달하는 방법을 사용합니다. 따라서 함수의 형식 매개변수의 자료형을 특정하지 않아도 실행 가능합니다.

 

함루의 람다식 매개변수를 좀 더 읽기 좋게 단순화할 수 있습니다.

다음 예제 코드는  typealias를 사용해 람다식 매개변수에 다른 이름을 사용한 예시입니다.

 

typealias arithmetic<T> = ( T, T ) -> T
fun <T> add( a: T, b: T, op: arithmetic<T> ): T{
    return op( a, b )
}
fun main() {
    val sumInt1: (Int, Int) -> Int = { a, b -> a + b }
    val sumInt2 = { a: Int, b: Int -> a + b
}

    val result = add(2, 3, { a, b -> a + b } )
    val result1 = add( 2, 3, sumInt1 )
    val result2 = add( 2, 3, sumInt2 )

    println(result)
    println(result1)
    println(result2)
}

 

  • typealias: typealias를 사용해 람다식 매개변수를 단순화하였습니다.
  • sumInt1, sumInt2: 람다식을 간소화 후 변수에 저장해 사용하였습니다.

 

 

 

 

반응형

+ Recent posts