다재다능 코틀린 프로그래밍 책으로 코틀린 스터디를 진행하면서 발표를 위해 준비했던 글입니다.
Any와 Nothing 클래스
Any
- Java의 Object와 Kotlin의 Any클래스는 대응되는 클래스지만 Any 클래스에는 확장함수를 통해 들어오는 특별한 메소드들이 많다.
- 대표적으로 to() 확장함수가 있다.
- 다양한 Any의 확장함수는 12-5에서 살펴볼 예정
- Java Object 클래스와 마찬가지로 Any 클래스도 Kotilin의 모든 객체가 상속받으며 제한적으로 사용해야 한다. 타입 안정성을 지키자!
Nothing
- kotilin에서는 void 대신 Unit을 사용하지만 진짜 아무것도 리턴하지 않는 걸 원할 땐 Nothing을 사용하자.
- 예외는 발생시킬 수 있다. 예외는 Nothing 타입을 대표한다.
- 예외로 Int, Double, String 등 모든 클래스를 대체할 수 있는 점은 자바와 똑같다.?
- Nothing의 유일한 목적은 컴파일러가 프로그램의 타입 무결성을 검증하도록 도와주는 것
Null 가능 참조
- Optional 사용의 3가지 불리한 점
- 컴파일러가 강제하지 않고 개발자가 Optional을 사용해야 한다.
- Optional이 객체의 참조 또는 null 참조를 감쌀 때 객체가 없다면 작은 오버헤드가 생긴다.
- 무슨 말일까? 객체가 있어도 감쌌다가 풀었다가 할 때 오버헤드가 생기기에 비용이 비싸다고들 하는거 아닌가?
- Optionl을 리턴하지 않고 null을 리턴해도 Java 컴파일러는 경고를 주지 않는다.
null은 에러를 유발
- null을 피해야 하는건 누구나 알고있다. 자바에선 Optional 사용을 권장하지만 Optional을 사용하면서 발생하는 사소한? 문제 또한 코틀린은 우아하게 해결한다.
- 코틀린은 null 불가참조에 null을 할당하거나 참조타입이 null 불가인 곳에 null을 리턴하려고 하면 컴파일 오류가 난다.
fun nickName(name: String): String {
if (name == "William") {
return "Bill";
}
return null // ERROR
}
println("${nickName("William")}")
println("${nickName("Venkat")}")
println("${nickName("null)}") // ERROR
- 리턴타입이 String일 경우 null을 리턴할 수 없고 파라미터 타입이 String인 경우에도 인자로 null을 넘길 수 없다.
- 반드시 null을 받거나 리턴하거나 하는 상황에선 개발 의도를 아주 명확하게해서 코틀린이 컴파일 시간에 NPE를 발생하지 않게 할 수 있도록 조취를 취해야 한다.
null 가능 타입 사용하기
- 받는 쪽에서 null 체크를 하지 않으면 null 가능 참조를 사용할 수 없다.
- 굉장히 섬세하다..,
- null 가능 타입은 타입 이름 뒤에 ?를 붙인다.
- 위에서 봤던 코드는 아래와 같이 수정할 수 있다.
fun nickName(name: String): String? {
if (name == "William") {
return "Bill";
}
return null // 정상 작동
}
println("${nickName("William")}")
println("${nickName("Venkat")}")
// println("${nickName("null)}")
- null을 인자로 받기 위해서는 똑같이 파라미터 타입 뒤에 ? 를 붙이면 되지만 해당 파라미터 변수를 사용할 땐 무조건 null 체크를 해줘야 한다.
fun nickName(name: String?): String? {
if (name == "William") {
return "Bill";
}
// return name.reversed() // ERROR
if (name != null ) {
return name.reversed()
}
return null
}
println("${nickName("William")}")
println("${nickName("Venkat")}")
// println("${nickName("null)}")
- null 가능 타입을 사용할 때 null 체크를 까먹을 일은 없다. → 안전하지 않은 순간은 존재한다.
세이프 콜 연산자 → ? 연산자
- ? 연산자를 사용해서 null 체크와 접근을 합칠 수 있다.
if (name != null ) {
return name.reversed()
}
return null
- 위 코드를 아래와 같이 바꿀 수 있다.
return name?.reversed()
return name?.reversed()?.toUpperCase() // 이렇게도 가능
엘비스 연산자(Elvis) → ?: 연산자
- null일 경우에 default 값을 정해줄 수 있음
val result = name?.reversed()?.toUpperCase()
return if (result == null) "Joker" else result
- null일 때 Joker를 리턴하는 위 코드를 아래와 같이 바꿀 수 있다.
return name?.reversed()?.toUpperCase() ?: "Joker"
사용해선 안될 안전하지 않은 확정 연산자 → !! 연산자
- not-null 확정 연산자 → !! 연산자
- !! 연산자는 순수 악이다.
- null이 아니라고 확신했지만 null이 들어오면 NPE가 터지며 프로그램이 종료된다. gg~
- 애초에 !!를 그냥 사용하지 말자. 기억에서 지울게요.
when의 사용
- null 가능 참조로 작업할 때 참조 값에 따라 다르게 동작해야 한다면 when 사용을 고려하자.
fun nickName(name: String?): String? {
if (name == "William") {
return "Bill";
}
return name?.reversed()?.toUpperCase() ?: "Joker"
}
- 엘비스 연산자를 사용한 위의 코드를 아래와 같이 바꿀 수 있다.
fun nickName(name: String?) = when (name) {
"William" -> "Bill"
null -> "Joker"
else -> name.reversed().toUpperCase()
}
- 코틀린은 null로 부터 안정성 뿐만 아니라 우아한 연산자를 제공해 코드의 지저분함도 없애준다. 최고!
타입 체크와 캐스팅
타입 체크
- 타입을 체크하는 행위는 개방 폐쇄 원칙에 위배된다.
- 하지만 equals() 메소드를 구현할 때나 인스턴스 타입에 기반해 when 분기를 사용한다면 꼭 필요하다.
is
- 같은 타입이면 동일하다고 판단하는 equals 메소드 재정의
class Animal {
override operator fun equals(other: Any?) = other is Animal
}
스마트 캐스트
- 위에서 살펴봤던 Animal 클래스에 age 라는 인스턴스 변수가 있을 때 equals 메소드 재정의
class Animal(val age: Int) { // 오호.. 생성자 깔끔
override operator fun equals(other: Any?):Boolean {
return if (other is Animal) age == other.age else false
}
}
- 캐스트없이 other.age를 사용할 수 있다. 코틀린이 other가 Animal 타입인지를 확인했기 때문
- 아래와 같이 && 연산자로 리팩토링이 가능하다.
class Animal(val age: Int) { // 오호.. 생성자 깔끔
override operator fun equals(other: Any?) = other is Animal && age == other.age
}
명시적 타입 캐스팅
- 명시적 타입 캐스팅은 컴파일러가 타입을 확실하게 결정할 수 없어 스마트 캐스팅을 하지 못할 경우에만 사용하자.
- ex) var 변수가 체크와 사용 사이에서 변경
- 명시적 타입 캐스팅에는 as와 as? 연산자를 사용한다.
fun fetchMessage(id: Int): Any =
if (id == 1) "Record found" else StringBuilder("data not found")
val message1: String = fetchMessage(1) as String
val message2: String? = fetchMessage(1) as? String
- as 연산자는 캐스팅에 실패하면 프로그램이 죽는다
- as? 연산자는 캐스팅이 실패하면 null을 할당한다.
- 결론적으로 as? 연산자가 더 안전하다.
- null을 할당하기에 엘비스 연산자를 사용하면 적절하게 대응할 수 있다.
fun fetchMessage(id: Int): Any =
if (id == 1) "Record found" else StringBuilder("data not found")
println("${(fetchMessage(2) as? String)?.length ?: "---")
// --- 출력
- 실무 권장사항
- 가능한 스마트 캐스트를 사용
- 스마트 캐스트가 불가능한 경우엔 안전한 캐스트 연산자(as?)를 사용
- 애플리케이션이 망하는 걸 보고싶으면 안전하지 않은 연산자(as)를 사용해라
제네릭: 파라미터 타입의 가변성과 제약사항
타입 불변성
- 메소드가 클래스 T의 객체를 받을때 T의 하위 객체는 전부 전달할 수 있다.
- 하지만 T에 제네릭 오브젝트 ex) List 를 받는다면 T의 하위 객체는 전달할 수 없다.
- List을 T로 넘기면 List는 전달 불가, extends Animal이어도..
fun receiveFruits(fruits: Array<Fruit>) {
println("${fruits.size}")
}
val bananas1: Array<Banana> = arrayOf()
receiveFruits(bananas1) // ERROR: type mismatch
val bananas2: List<Banana> = listOf()
receiveFruits(bananas2) // 정상 동작
- 타입 안정적으로 만들었다면서 List는 왜 될까?
- Array는 뮤터블하다. Array를 넘겨도 Orange 객체가 add 되고 타입이 깨질 수 있다.
- List는 이뮤터블하다. List를 넘기면 Orange 객체나 다른 객체를 add하거나 변경해서 타입이 깨질 확률 이 없다.
- 코틀린은 매우 합리적이다..
- Array, List 의 차이 즉, 코틀린이 불변, 가변 차이점을 전달하는 비밀에는 out 키워드가 있다. 좀 더 알아보자.
공변성 사용하기
- 코틀린에게 제네릭 파생 타입이 허용되도록 요청해야할 때 타입 프로젝션이 필요하다.
- 아래와 같은 예제가 그런 경우이다.
fun copyFromTo(from: Array<Fruit>, to: Array<Fruit>) {
for (i in 0 until from.size) {
to[i] = from[i]
}
}
val fruits = Array<Fruit>(3) { _ -> Fruit() }
val bananas = Array<Banana>(3) { _ -> Banana() }
copyFromTo(bananas, fruits) // ERROR: type mismatch
- 원칙적으론 전달 못하는게 맞지만 from 파라미터는 파라미터의 값을 읽기만 하기 때문에 Fruit 클래스의 하위 타입이 전달돼도 아무런 위험이 없다.
- 이런 것을 타입이나 파생 타입에 접근하기 위한 파라미터 타입의 공변성이라고 이야기한다.
- out 키워드로 공변성을 사용하고 에러가 나지않게 고쳐보자.
fun copyFromTo(from: Array<out Fruit>, to: Array<Fruit>) {
for (i in 0 until from.size) {
to[i] = from[i]
// 코틀린에서 out 키워드를 사용한 파라미터를 전달하는 호출이 없는지를 확인한다.
// from[i] = Fruit() // ERROR
// from.set(i, to[i]) // ERROR
}
}
copyFromTo(bananas, fruits) // 정상 동작
- 이뮤터블한 Array 클래스를 파라미터로 사용하지만 어떤 값도 추가하지 않겠다는 것을 약속하고 공변성을 이용하는 걸 사용 가변성 혹은 타입 프로젝션 이라고 한다.
- 제네릭 타입을 사용할 때가 아니라 제네릭 클래스를 만들 때 즉 선언할 때 공변성을 사용한다고 지정하는 것을 선언처 가변성이라고 부른다.
- 선언처 가변성의 좋은 예제가 List 이다.
반공변성 사용하기
- 베이직 타입(Any)에 공변성을 요청하는 것을 반공변성이라고 한다.
val things = Array<Any>(3) { _ -> Fruit() }
val bananas = Array<Banana>(3) { _ -> Banana() }
copyFromTo(bananas, things) // ERRORL type mismatch
- 인자 to 자리에 Array를 넣으면 기본 타입 불변성으로 인해 실행이 안된다.
- 반공변성이 가능하게 요청해보자.
fun copyFromTo(from: Array<out Fruit>, to: Array<in Fruit>) {
for (i in 0 until from.size) {
to[i] = from[i]
}
}
copyFromTo(bananas, things) // 정상 동작
- copyFromTo 메소드 파라미터 to 자리에 in 키워드가 추가된 것을 확인할 수 있다.
- 반공변성인 in을 사용해서 사용처 가변성을 주었다.
- 반공변성은 파라미터 타입을 받을 수만 있고 리턴하거나 다른곳으로 보낼 수 없다.
where을 사용한 파라미터 타입 제한
- 제네릭 타입에 제약조건을 걸기
fun <T: AutoCloseable> useAndClose(input: T) {
input.close()
}
- 여러 개의 제약조건을 넣을 땐 where 키워드를 사용해야 한다.
fun <T> useAndClose(input: T)
where T: AutoCloseable,
T: Appendable {
input.append("there")
input.close()
}
스타 프로젝션
- 스타 프로젝션 <*>은 제네릭 읽기전용 타입과 raw 타입을 위한 코틀린의 기능
- 타입에 대해 정확히는 몰라도 타입 안정성을 유지하며 파라미터를 전달할 때 사용
fun printValues(values: Array<*>) {
for (value in values) {
println(value)
}
// values[0] = values[1] // ERROR 읽기만 가능!
}
printValues(arrayOf(1, 2)
// 1
// 2
- 파라미터를 Array로 작성했다면 ERROR가 발생한 values[0] = values[1] 코드가 컴파일 됐다.
- 스타 프로젝션은 이런 부주의한 오류를 예방한다.
- out T와 동일하지만 더 간결하게 작성할 수 있다. (out Fruit 같은게 아니라 out T 이다.)
- 스타 프로젝션이 선언처 가변성에서 로 정의된 반공분산으로 사용된다면 in Nothing을 사용한 것 과같다. → 읽기만 가능하니까.
구체화된 타입 파라미터
- 제네릭에서 특정 타입의 첫 번째 인스턴스를 찾는 로직을 자바식으로 코딩하면 아래와 같이 코딩했을 것이다.
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])
}
- 코드가 굉장히 어지럽다. 코틀린의 reified로 리팩토링해보자.
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
}
- reified로 파라미터 타입 T를 선언하면 함수 안에서 T를 타입 체크와 캐스팅용으로 사용 가능하다.
- 호출하는 코드마저 가독성이 좋아진다.
println(findFirst<NonFiction>(books).name)
'스터디 > 다재다능 코틀린 프로그래밍' 카테고리의 다른 글
8. 클래스 계층과 상속 (0) | 2021.11.29 |
---|---|
7. 객체와 클래스 (0) | 2021.11.29 |
5. 콜렉션 사용하기 (0) | 2021.11.29 |
4. 외부 반복과 아규먼트 매칭 (0) | 2021.11.29 |
3. 함수를 사용하자. (0) | 2021.11.29 |