Kotlin

[Kotlin] Type checks와 형변환(casts)

Darane 2021. 10. 6. 14:15

is와 !is 연산자

is 연산자와 !is 연산자는 object가 주어진 type인지 Runtime에 확인(checks)해줍니다.

if (obj is String) {
    print(obj.length)
}
if (obj !is String) { //!(obj is String) 와 같습니다.
    print("Not a String")
} else {
    print(obj.length)
}

 

Smart casts

대부분 Kotlin에서 명시적으로 형변환(Explicit casts)할 필요는 없습니다.

컴파일러가 is -checks를 추적하고 불변의 값(immutable values)이 명시적으로 형변환(cast)이 필요하면 safe casts를 자동으로 넣어줍니다.

fun demo(x: Any) {
  if (x is String) {
    print(x.length) // x는 String으로 자동 변환됩니다.
  }
}

 

컴파일러는 !is를 체크하고 return 되는 경우 이후에 object의 cast가 안전하다는 것을 알고 있습니다.

if (x !is String) return

print(x.length) // x는 자동으로 String으로 변환된다.

 

또는 if문에서 && 나 || 연산자 오른쪽에 object는 cast가 안전하다고 알고 있습니다.

// || 오른쪽에 위치한 x는 String으로 변환된다.
if (x !is String || x.length == 0) return

// && 오른쪽에 위치한 x는 String으로 변환된다
if (x is String && x.length > 0) {
    print(x.length) // x는 String으로 변환된다.
}

 

Smart casts는 when 표현식이나 while loops 에서도 동작합니다.

when (x) {
    is Int -> print(x + 1)
    is String -> print(x.length + 1)
    is IntArray -> print(x.sum())
}

 

Smart casts는 컴파일러가 검사와 사용(check and usage) 사이에 변수가 변경되지 않음을 보장하는 경우에만 동작합니다.

Smart casts는 다음과 같은 조건에서 사용할 수 있습니다.

  • val 지역변수 - 항상 Smart casts 가능합니다.
    다만 local delegated properties는 동작하지 않습니다.
  • val properties - property가 private나 internal이나 property가 선언된 동일한 모듈에서 검사하는 경우 가능합니다.
    하지만 open properties나 사용자가 custom getter를 정의한 경우 동작하지 않습니다.
  • var 지역변수 - 검사와 사용(check and usage) 사이에 변수가 수정되지 않은 경우,
    변수를 수정하는 lambda에 캡쳐되지 않은 경우, local delegated property가 아닌 경우 가능합니다.
  • var properties - 변수의 값이 언제든지 변경가능하기 때문에 Smart casts를 할 수 없습니다.

 

반응형

 

"Unsafe" cast operator, 안전하지 않은 cast연산자

일반적으로 형변환 연산자(cast operator)는 변환이 불가능한 경우 throw를 던집니다.

그래서 안전하지 않습니다(unsafe).

Kotlin 안전하지 않은 cast은 중위연산자 as로 수행합니다.

val x: String = y as String

 

이 object는 nullable이 아니기 때문에 null을 String으로 cast할 수 없습니다.

y가 null이면 위의 코드는 예외(throw)가 발생합니다

null값에 대해 올바르게 만드려면 cast 오른쪽에 nullable형식을 사용하면 됩니다.

val x: String? = y as String?

 

 

"Safe" (nullable) cast operator, 안전한 cast 연산자

예외(Exception)를 방지하려면 cast 실패 했을 때 null을 반환하는 safe cast 연산자를 사용하면 됩니다.

val x: String? = y as? String

as?의 오른쪽의 String은 non-null type이지만 결과적으로 nullable을 cast할 수 있습니다.

 

 

Type erase(Type 소거) 및 generic type 검사

Kotlin은 Compile time(컴파일 타임)에 generic이 포함된 연산에 대한 type 안정성을 보장하지만

Runtime에 generic type의 instance(인스턴스)는 실제 type에 대한 정보를 가지고 있지 않습니다.

예를 들어 List는 List<*>로 소거(erase)됩니다.

 

일반적으로 instance가 runtime에 특정 type argument를 가진 generic type에 속하는지 확인할 수 없습니다.

그래서 컴파일러는 ints is list 또는 list is T(type parameter)와 같이 type 소거로 인해 Runtime에 수행할 수 없는 is -checks를 하지 않습니다. 그러나 star-projected type에 대해 instance를 확인할 수 있습니다.

if (something is List<*>) {
    something.forEach { println(it) } // Any? type로 입력됩니다.
}

 

유사하게, 정적으로 검사된 instance의 type arguments가 이미 있는 경우 is -check 또는 type generic이 아닌 부분을 포함하는 cast를 만들 수 있습니다.

이 경우 꺽쇠 괄호가 생략됩니다.

fun handleStrings(list: List<String>) {
    if (list is ArrayList) {
        // list는 ArrayList<String> 로 smart cast됩니다.
    }
}

 

동일 구문이지만 type argument가 생략된 경우 type argument를 고려하지 않은 cast를 사용할 수 있습니다. list as ArrayList

구체화된 type parameter가 있는 경우 inline 함수에는 호출한 곳에 inline된 실제 type argument가 있습니다. 이렇게 하면 arg is T가 type parameter를 확인할 수 있지만 arg가 generic type 자체의 instance인 경우 해당 type argument는 여전히 소거됩니다.

inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {
    if (first !is A || second !is B) return null
    return first as A to second as B
}

val somePair: Pair<Any?, Any?> = "items" to listOf(1, 2, 3)
val stringToSomething = somePair.asPairOf<String, Any>()
val stringToInt = somePair.asPairOf<String, Int>()
val stringToList = somePair.asPairOf<String, List<*>>()
val stringToStringList = somePair.asPairOf<String, List<String>>() // 컴파일되지만 type 안정성이 깨집니다.

 

 

Unchecked casts, check할 수 없는 형변환

Type erase는 runtime에 generic type instance의 실제 type argument를 확인하는 것을 불가능하게 만듭니다. 또한 generic type은 컴파일러가 type 안전성을 충분히 보장하지 않습니다.

그럼에도 type 안전성을 암시하는 높은 수준의 프로그램 로직을 가지고 있습니다.

예를 들어:

fun readDictionary(file: File): Map<String, *> = file.inputStream().use {
   TODO("Read a mapping of strings to arbitrary elements.")
}

// 파일에 Int가 있는 map을 저장했습니다.
val intsFile = File("ints.dictionary")

// 경고: Unchecked cast: `Map<String, *>` 을 `Map<String, Int>` 로
val intsDictionary: Map<String, Int> = readDictionary(intsFile) as Map<String, Int>

마지막 줄의 cast에 대한 경고가 나타납니다. 컴파일러는 runtime에 이를 확인할 수 없고 Map의 값이 Int임을 보장하지 않습니다.

 

검사되지 않은 캐스트를 피하기 위해 프로그램 구조를 다시 디자인할 수 있습니다. 위의 예에서 다른 유형에 대한 유형 안전 구현과 함께 DictionaryReader 및 DictionaryWriter 인터페이스를 사용할 수 있습니다. 호출 사이트에서 구현 세부 정보로 확인되지 않은 캐스트를 이동하기 위해 합리적인 추상화를 도입할 수 있습니다. generic variance을 적절히 사용하는 것도 도움이 될 수 있습니다.

 

Unchecked cast 경고는 @Suppress("UNCHECKED_CAST") annotation을 추가하여 발생하지 않게 할 수 있습니다.

inline fun <reified T> List<*>.asListOfType(): List<T>? =
    if (all { it is T })
        @Suppress("UNCHECKED_CAST")
        this as List<T> else
        null
반응형