-
[코틀린] 타입 안정성코틀린 2022. 3. 7. 22:16
1. Any와 Nothing 클래스
Any
코틀린의 모든 클래스의 상위 클래스.
각 클래스에 대해서 equals, hashCode, toString 같은 메소드들을 지원한다.
자바에서 Object와 유사한 기능을 하고 뿐만아니라 to(), let(), run(), apply(), also()같은 확장 함수를 제공한다.
Nothing
표현식이 리턴하지 않을 때 Unit을 사용한다고 했다. 하지만 함수가 예외를 발생시켜 정말 아무것도 리턴하지 않을 때는 Nothing을 리턴 타입으로 갖는다. Nothing은 모든 클래스로 대체할 수 있어 리턴 타입에 어떤 타입이 있으면 따로 명시를 하지 않아도 된다.
fun computeSqrt(n: Double): Double { // Nothing이 반환될 수 있지만 표기하지않아도 됨 if(n >= 0) { return Math.sqrt(n) } else { throw RuntimeException("No negative number") } }
2. Null 가능 참조
null은 에러를 유발
코틀린에서는 null을 리턴하도록 허용되지 않은 함수이면 null을 리턴할 수 없다
fun returnNull(n: Int): String { if(n > 0) { return "Natural number" } return null } // Kotlin: Null can not be a value of a non-null type String
n이 0보다 작거나 같으면 null을 리턴하도록 되어있지만 리턴 타입은 String이므로 위의 함수는 컴파일 되지 않는다.
Null 가능 타입 사용하기
기본적으로 null 불가 타입 뒤에 ?를 붙여 null 가능 타입으로 만들 수 있다.
fun returnNull(n: Int): String? { if(n > 0) { return "Natural number" } return null } // 리턴타입을 String?으로 명시하여 null을 리턴할 수 있도록 변경 println(returnNull(1)) // Natural number println(returnNull(-1)) // null
함수 아규먼트 또한 null 가능 타입으로 만들 수 있다.
fun returnNull(n: Int?): String? { if(n > 0) { return "Natural number" } return null }
위 함수를 실행시키면 코틀린은 아래의 컴파일러 에러를 발생시킨다. 만약 n이 null로 주어질경우 n > 0을 실행할 수 없기 때문이다.
Kotlin: Operator call corresponds to a dot-qualified call 'n.compareTo(0)' which is not allowed on a nullable receiver 'n'.
코틀린에서 nullable 메소드를 호출할 때는 safe call 연산자나 null이 아님을 확인해주는 연산자 프리픽스가 요구된다. 위 코드를 nullable과 함께 실행하려면 null 체크를 추가해주어야한다.
fun returnNull(n: Int?): String? { if(n != null && n > 0) { return "Natural number" } return null }
세이프 콜 연산자(safe call operator)
? 연산자를 세이프 콜 연산자라고 하며 객체의 바로 뒤에 붙어 객체가 null일 경우 전체 연산을 null처리하는 역할을 한다.
아래 함수의 경우 s가 null 일 경우를 체크 하고 특정 문자열이 포함되었는지를 확인한다.
fun reverseString(s: String?): String? { if(s != null) { return s.reversed() } return "not null" } println(reverseString("reverse")) // esrever println(reverseString(null)) // give not null
? 연산자를 활용하면 아래처럼 간단하게 확인할 수 있다.
fun reverseStringWithSafeCallOp(s: String?): String? { return s?.reversed() ?: "give not null" } println(reverseStringWithSafeCallOp("reverse")) // esrever println(reverseStringWithSafeCallOp(null)) // give not null
엘비스 연산자(Elvis Operator) ?:
위의 경우에서처럼 삼항연산자의 결과가 참일경우 그 결과를 그대로 리턴하고 싶을 때가 있다. 그럴 때 사용하는 ?: 연산자를 엘비스 연산자라고 한다.
fun reverseStringWithSafeCallOp(s: String?): String? { // if(s?.reversed() != null) // return s?.reversed() // return "give not null" return s?.reversed() ?: "give not null" // 위의 3줄은 ?:를 이용한 1줄과 같다. }
간단히 말해서 좌측 표현식이 참일 경우 그대로 리턴하고 null 일경우 우항을 리턴한다.
not null 확정 연산자 (not-null assertion) !!
이제까지 봤듯이 코틀린에서 nullable일때는 null 체크를 한 후에 관련 메소드를 사용할 수 있다. 만약 nullable 타입에 대해서 절대 null일리 없다는 표시를 해주고 싶다면 !!를 사용하면 된다.
return s!!.reversed()
이 책에서는 !!를 사용해야 할 경우 코드 자체를 잘못 짠 것으로 의심하고 다시 점검하라고 한다.
when의 사용
null 체크는 when 내에서 수행될 수도 있다.
아래는 if, elvis operator 를 사용하여 만든 로직을 when 표현식을 사용하여 구현한 것이다.
fun reverseString(s: String?): String { if(s == "NPC") { return "NPC" } return s?.reversed() ?: "Anonymous" } fun reverseString(s: String?): String = when (s) { "NPC" -> "NPC" null -> "Anonymous" else -> s.reversed() }
3. 타입 체크와 캐스팅
타입체크
타입 체크는 새로운 타입이 추가 될때마다 같이 관리해주어야 하고 개방폐쇄 원칙을 위배하기도 한다. 런타임에 타입체크를 하는 것은 꼭 필요할 때만 해야한다. equals로 두 객체를 비교하거나 when으로 분기를 할 때에는 타입 체크를 반드시 해야한다.
is 사용하기
코틀린에서 is를 사용하면 해당 객체가 참조로 특정 타입인지 아닌지를 알 수 있다.
class Animal { override operator fun equals(other: Any?) = other is Animal } val animal = Animal() val animalString: Any = "Animal" println(animal == Animal()) // true println(animal == animalString) // false
is의 부정은 !is를 사용한다.
스마트 캐스트
코틀린에서는 참조의 타입이 확인되면 스마트 캐스트를 한다.
class Animal(val age: Int) { override operator fun equals(other: Any?): Boolean { return if(other is Animal) age == other.age else false } } val tiger = Animal(10) val cat = Animal(10) val monkey = Animal(3) println(tiger == cat) // true println(tiger == monkey) // false
other is Animal 이 true 로 통과되면 other의 타입은 Any에서 Animal로 캐스트되고 other.age를 실행 시킬 수 있게된다.
if 문 뿐만 아니라 ||나 &&에도 적용되어 위의 equals는 아래처럼 다시 쓸 수 있다.
class Animal(val age: Int) { override operator fun equals(other: Any?) = other is Animal && age == other.age }
이런 스마트 캐스팅은 nullable이 null 이 아니라고 판별될 때도 발생해서 자동으로 null 불가 타입으로 캐스팅 하기도 한다.
when과 함께 타입 체크와 스마트 캐스트 사용
when에서 타입 체크 하는 조건이 있고 그 타입이 맞다면 이후 표현식에서는 그 타입으로 스마트 캐스트 된다.
fun smartcastWithWhen(input: Any): String = when (input) { is String -> "$input length is ${input.length}" is IntArray -> input.joinToString(",", "[", "]") else -> "Default" } println(smartcastWithWhen("String")) println(smartcastWithWhen(intArrayOf(1, 2, 3)))
위에서 input이 String타입인것이 확인되면 length 프로퍼티를 사용할 수 있고 IntArray라면 joinToString 메소드를 사용할 수 있게된다.
4. 명시적 타입 캐스팅
코틀린 컴파일러가 타입을 결정하지 못하는 경우에는 명시적으로 타입을 캐스팅해주어야한다.
코틀린은 타입 캐스팅을 위해서 as와 as?를 제공한다.
fun fetchMessage(id: Int): Any = if(id == 1) "Record found" else StringBuilder("data not found") println((fetchMessage(1) as String).length) // 12 출력 println((fetchMessage(2) as String).length) // ClassCaseException 런타임 에러 발생
fetchMessage의 경우 두 가지 서로 다른 타입의 리턴을 갖는다. 이 결과를 받아서 length를 출력 하려고 할 때 as를 이용하여 타입 캐스팅을 해야한다. 이 때 String 타입이 아닌 경우에는 런타임 에러가 발생하게 된다.
아래와같이 as? 연산자를 사용하면 String 타입으로 변환할 수 없는 경우 null로 리턴된다. 따라서 전체 결과또한 null이 된다.
println((fetchMessage(2) as? String)?.length) // null 출력됨 println((fetchMessage(2) as? String)?.length ?: 0) // 0 출력됨
5. 제네릭
타입불변성
타입 불변성은 어떤 타입이 필요하다고 명시한 경우 그 타입을 상속 받은 타입이라고 하더라도 사용할 수 없는 것을 말한다. 예를들어 T의 제네릭 오브젝트(List<T>)가 필요한 곳에 T의 자식으로 이루어진 객체를 전달할 수는 없으면 타입불변성을 갖고 있는 것이다.
open class Fruit class Banana : Fruit() class Orange : Fruit() fun printFruits(basket: Array<Fruit>) { basket.forEach { e -> println(e) } } fun printFruit(f: Fruit) { print("${f::class}") } val fruits = arrayOf<Fruit>(Banana()) val bananas = arrayOf<Banana>(Banana()) printFruits(fruits) // Generic$Banana@69d9d322 printFruit(Banana()) // class Generic$Banana printFruits(bananas) // 에러 발생
위의 예시에서 Fruit대신 Banana 객체를 넣을 수는 있지만 Array<Fruit> 대신에 Array<Banana>를 인자로 사용할 수는 없다.
반면에 List는 위와 같은 동작을 할 수 있다. 아래 예시가 있다.
fun printFruitsList(basket: List<Fruit>) { println("Fruit basket ${basket.size}") } printFruitsList(listOf(Banana(), Orange())) // 2출력
Array와 List가 어떤 차이가 이런 특징을 만든 것일까
공변성(variance)
코틀린에서 Array는 기본적으로 타입 불변성을 가지고 있기 때문에 Array<Fruit>을 필요로 하는 인자에 Array<Banana>타입을 넘겨주면 에러가 발생했다. (List는 기본적으로 공변성을 가지고 있어서 허용된다.)
코틀린은 공변성을 허용해서 Array<Fruit>타입이 명시된 인자에 Array<Banana> 타입을 줄 수 있다.
fun printFruits(basket: Array<out Fruit>) { basket.forEach { e -> println(e) } } printFruits(bananas) // 출력: Generic$Banana@480f1311
Array<out Fruit>은 Fruit을 상속받는 Array타입을 인자로 받을 수 있다.
아래는 두 Fruit Array간의 원소를 복사하는 메소드이다. from의 Array에는 Fruit의 어떤 서브타입이 와도 상관없으므로 <out Fruit>을 명시해 Fruit의 자식 타입의 Array를 받아도 된다.
fun copyFruit(from: Array<out Fruit>, to: Array<Fruit>) { for(i in from.indices) { to[i] = from[i] } }
반공변성(contravariance)
공변성과 반대로 타입 파라미터에 어떤 타입의 수퍼타입을 받을 수 있도록 지정할 수도 있다. 이것을 반공변성이라고 하고 in로 나타낸다.
fun copyFruit(from: Array<out Fruit>, to: Array<in Fruit>): String { for(i in from.indices) { to[i] = from[i] } return "copied" } val anyArray: Array<Any> = arrayOf(1) val oranges: Array<Orange> = arrayOf(Orange()) println(copyFruit(oranges, anyArray)) // copied 출력
where를 사용한 파라미터 타입 제한
제네릭을 사용할 때 타입 제한을 할 수 있다. 아래 함수의 경우 input이 close 메소드를 반드시 가지고 있어야한다. 이런 경우 T를 close를 가진 타입으로 제한할 수 있다.
fun <T> useAndClose(input: T) { input.close() } // 아래 함수의 인자는 AutoClosable을 구현한 T만 가능 fun <T: AutoCloseable> useAndClose(input: T) { input.close() }
만약 여러 제약조건을 명시하고 싶다면 where을 사용해서 아래처럼 써야한다.
fun <T> useAndCloseAndAppend(input: T) where T: AutoCloseable, T: Appendable { input.append() input.close() }
스타 프로젝션(*, star projection)
읽기 전용으로 타입 파라미터가 필요할 때 사용한다. 스타 프로젝션으로 선언하면 변경은 할 수 없다.
fun printer(input: Array<*>) { for(e in input.iterator()) println(e) // input[0] = 10 이런것은 허용되지 않음 } printer(arrayOf(1, 2, 3))
6. 구체화된 타입 파라미터(Reified type Parameter)
코틀린 컴파일타임에 타입파라미터T에 대한 정보가 사라지기 때문에 아래처럼 함수 본문에서 타입파라미터를 사용할 수가 없다.
fun <T> findFruit(fruits: List<Fruit>): T { val result = fruits.filter { fruit -> fruit is T } // T를 사용 불가능. 컴파일타임에 T가 지워짐 return result[0] as T } findFruit<Orange>(listOf<Fruit>(Orange(), Banana()))
이럴 때에는 함수를 inline으로 만들고 타입파라미터에 reified 키워드를 붙여 주어야 한다.
inline fun <reified T> findFruit(fruits: List<Fruit>): T { val result = fruits.filter { fruit -> fruit is T } return result[0] as T } println(findFruit<Orange>(listOf<Fruit>(Orange(), Banana()))) // Generic$Orange@672ba9cc 출력
참고
다재다능 코틀린 프로그래밍 6장
'코틀린' 카테고리의 다른 글
[코틀린] 클래스와 상속 (0) 2022.03.13 [코틀린] 객체와 클래스 (0) 2022.03.13 [코틀린] 컬렉션 (0) 2022.03.07 [코틀린] 외부 반복과 아규먼트 매칭 (0) 2022.03.07 [코틀린] 함수 (0) 2022.03.06