educative - kotlin - 6

Sung Jun Jin·2021년 3월 14일
0

Type Safety to Save the Day

Any and Nothing Classes

코틀린에서 Any 클래스는 모든 클래스가 상속받는 자바의 Object 클래스라고 할 수 있다. 자바의 equals(), toString() 메소드가 Object 클래스의 소속인것 처럼 코틀린에도 equals(), hashCode(), toString() 다양한 메소드가 Any 클래스에 속해 있다. 실직적으로 바이트코드로 컴파일 됐을 때 자바의 Object와 일치합니다.

Nothing is deeper than void

자바의 void는 코틀린의 Unit과 비슷하다. 함수의 리턴타입을 Unit으로 지정하게 된다면 실질적으로 리턴 값이 없어야하며, 명시작으로 return을 작성하지 않아도 된다. 하지만 말그대로 정말 함수가 아무것도 리턴하지 않는다면 Nothing 클래스를 사용해줄 수 있다.

Nothing은 어떠한 값도 포함하지 않는 타입이며 인스턴스를 생성할 수 없다. 코틀린 문서에는 다음과 같이 명시되어 있다.

Nothing has no instances. You can use Nothing to represent "a value that never exists"

따라서 Nothing은 예외를 던지는 함수에서 리턴 타입으로 사용되기도 한다.

fun throwException(): Nothing {
    throw IllegalStateException()
}

아래 함수를 살펴보자 return 타입이 Double이고, 파라미터로 들어오는 n 값이 양수일때는 Double 타입이 리턴되고 음수일때는 예외가 발생하여 Nothing이 리턴된다. 음수일 경우 리턴타입이 달라 컴파일 에러가 날거 같지만 Nothing 클래스는 모든 타입의 서브 클래스이다. 따라서 아래와 같은 코드는 컴파일 에러없이 잘 돌아간다.

fun computeSqrt(n: Double): Double { 
    if(n >= 0) {
        return Math.sqrt(n) 
    } else {
        throw RuntimeException("No negative please") 
    }
}

Using nullable types

코틀린에서 nullable 한 타입을 표현하려면 ? suffix를 사용해준다.

fun nickName(name: String?): String? {
  if (name == "William") {
    return "Bill"
  }

  return name.reversed()
} 

Safe-call operator

하지만 위와 같은 코드는 문제가 있는데 만약 name 파라미터가 null로 들어가게 될 경우 reversed() 함수를 실행하면 에러가 발생한다. 따라서 파라미터에 대한 null checking이 필요한데 코틀린에서는 null checking을 ? 연산자를 사용해 간단히 작성해줄 수 있다. 다른 말로 safe-call operator라고 한다.일반적으로 작성할 수 있는 null checking 코드와 비교해보자

일반적인 null checking

if (name != null) {
	return name.reversed()
}

return null

? 연산자를 사용한 null checking

return name?.reversed()

Elvis operator

만약 위 코드에서 리턴 타입이 null일경우 특정한 값을 리턴하고 싶다면 elvis operator ?:를 사용해주면 된다. 여기서는 리턴 타입이 null일때 "Joker"라는 문자열이 리턴된다

return name?.reversed() ?: "Joker"

Type Checking & Smart Casting

코틀린에서 is를 사용하면 타입체킹을 할 수 있다.
[타입 체크할 변수] is [확인하고자 하는 타입]

Animal 클래스의 equals 메소드를 오버라이딩 해보자

class Animal {
   override operator fun equals(other: Any?) = other is Animal
}

val greet: Any = "hello" 
val odie: Any = Animal() 
val toto: Any = Animal()
println(odie == greet) //false 
println(odie == toto) //true

여기까지는 자바의 instanceof 와 비슷하지만 신기한점은 코틀린에서 is 의 결과가 true이면 해당 타입으로 자동 캐스팅을 해준다.

만약 Animal 클래스에 age라는 프로퍼티가 있다고 가정해보자, 그리고 앞서 오버라이딩했던 equals 함수를 인스턴스 타입에 대한 비교뿐만이 아니라 만약 Animal 인스턴스라면 age 또한 비교하는 쪽으로 살짝 바꿔보자. 자바였으면 다음과 같이 코드가 작성되었을 것이다.

@Override public boolean equals(Object other) { 
  if(other instanceof Animal) {
    return age == ((Animal) other).age; 
  }
  return false; 
}

과정

  • other의 인스턴스를 비교한다
  • Animal 타입의 인스턴스가 맞으면 Animal 타입으로 캐스팅을 하고 age 프로퍼티를 비교해본다

하지만 코틀린의 경우 인스턴스 비교 후 true면 자동 타입 캐스팅이 되기 때문에 바로 age 프로퍼티에 접근이 가능하다. 간단하게 코드를 작성할 수 있다 이는 is 뿐만 해당되는게 아니라 && || 연산자를 사용할때도 적용이 가능하다.

// smartcast.kts
class Animal(val age: Int) {
  override operator fun equals(other: Any?):Boolean {
    return if (other is Animal) age == other.age else false
  }
}

override operator fun equals(other: Any?) =
  other is Animal && age == other.age

만약 확실하지 않은 타입 캐스팅, 즉 unsafe casting이 필요할때는 as 연산자로 처리해준다. 다음 예시를 보자. 메세지를 처리해주는 함수가 있다. 경우에 따라서 String, StringBuilder 타입을 리턴해준다.

fun fetchMessage(id: Int): Any =
  if (id == 1) "Record found" else StringBuilder("data not found")

이 함수를 아래 반복문에서 호출했을때, String 타입이 리턴되면 정상처리가 되겠지만 StringBuilder 타입이 리턴되었을때는 다음과 같은 에러가 발생할 것이다.

for (id in 1..2) {
  println("Message length: ${(fetchMessage(id) as String).length}")
}

StringBuilder가 리턴되었을때 에러

Message length: 12
java.lang.ClassCastException: java.base/java.lang.StringBuilder cannot be cast to java.base/java.lang.String

이럴때 as를 사용해주면 된다.

val message: String = fetchMessage(1) as String
val message: String? = fetchMessage(1) as? String

null check를 하는 as? 가 as 보다는 안전하다. as?를 사용하면 캐스팅이 안될 경우 null을 리턴하지만 as 연산자를 사용할때는 에러가 발생하기 때문이다 따라서 다음과 같은 권고사항이 있다.

  • smart casting을 최대한 많이 사용해라
  • smart casting이 옵션이 아닐 경우에만 safe casting을 사용해라
  • 어플리케이션이 터지는걸 보고싶으면 unsafe casting을 사용해라...

Generics: Variance and Constraints of Parametic Types

제네릭(Generics)이란

클래스, 메소드에서 사용할 데이터 타입을 인스턴스를 생성할 때나 메소드를 호출할때 확정하는 기법이다. 객체의 타입을 컴파일 타임에 체크할 수 있어서 타입 안정성을 높이고 형변환의 번거로움이 줄어든다는 장점이 있다.

형식은 다음과 같다
List<T>
여기서 T는 타입을 지정할 수 있는 type parameter를 의미한다.

Type invariance

만약 다음과 같은 클래스 구조가 있다고 가정해보자. 가장 상위 클래스인 Fruit을 두 하위 클래스 Banana와 Orange가 상속받고 있다.

open class Fruit
class Banana : Fruit()
class Orange: Fruit()
fun receiveFruits(fruits: Array<Fruit>) {
  println("Number of fruits: ${fruits.size}")
}

val bananas: Array<Banana> = arrayOf() 
receiveFruits(bananas) //type mismatch 에러

val bananas: List<Banana> = listOf() 
receiveFruits(bananas) //OK

여기서 receiveFruits에 파라미터로 들어갈 수 있는 타입은 오로지 Fruit 타입이다. Banana와 Orange가 Fruit의 하위클래스라고 해도 들어갈 수 없다. 이런식으로 제네릭을 사용하면 sub type의 관계가 유지되지 않는것을 type invariance라고 한다.

하지만 여기서 함수의 매개변수를 List<Fruit>으로 바꿔버리면 정상동작한다. 왜냐면 Array와 List의 구조가 살짝 다르기 때문이다. Array<T>와 List<out E>

여기서 Array<out Fruit>을 사용해주면 Fruit의 파생 클래스까지 허용이 가능하다.

open class Fruit
class Banana : Fruit()
class Orange: Fruit()

fun copyFromTo(from: Array<out Fruit>, to: Array<out Fruit>) {
  for (i in 0 until from.size) {
    to[i] = from[i]
  }
}

val fruitsBasket = Array<Fruit>(3) { _ -> Fruit() }
val bananaBasket = Array<Banana>(3) { _ -> Banana() }

copyFromTo(bananaBasket, fruitsBasket) //OK

만약 어떤 타입이 올지 모르면 List<*>를 사용해주면 된다. 하지만 read only라는 점!

fun printValues(values: Array<*>) {
  for (value in values) {
    println(value)
  }
  
  //values[0] = values[1] //이런 식으로 값을 바꿔주려 하면 ERROR
}

printValues(arrayOf(1, 2)) //1\n2

Parametic type constraints using where

예를들어 다음 함수에서 인자로 들어가는 input에 close() 메소드가 구현되지 않았다면 다음과 같은 에러가 날 것이다.

fun <T> useAndClose(input: T) {
  input.close() //ERROR: unresolved reference: close
}

여기서 close() 인터페이스를 가지고 있는 인자만 넘어오도록 할 수 있다

fun <T: AutoCloseable> useAndClose(input: T) {
  input.close() //OK
}

2개 이상의 조건을 걸어주고 싶으면 where를 사용하면 된다.

fun <T> useAndClose(input: T) 
  where T: AutoCloseable,
        T: Appendable {
  input.append("there")
  input.close()
}

Applying reification

아래와 같은 상황에서 첫번째 인스턴스의 타입을 반환하는 findFirst() 메소드를 정의한다고 해보자.

val books: List<Book> = listOf(
  Fiction("Moby Dick"), NonFiction("Learn to Code"), Fiction("LOTR"))
  
 fun <T> findFirst(books: List<Book>, ofClass: Class<T>): T {
    val selected = books.filter { book -> ofClass.isInstance(book) }
    if(selected.size == 0) {
        throw RuntimeException("Not found")
    }
    return ofClass.cast(selected[0]) 
}

println(findFirst(books, NonFiction::class.java).name) //Learn to Code
inline fun <reified T> findFirst(books: List<Book>): T {
  val selected = books.filter { book -> book is T }
  
  if(selected.size == 0) {
    throw RuntimeException("Not found")
  }
  
  return selected[0] as T
}
profile
주니어 개발쟈🤦‍♂️

0개의 댓글