반응형
Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- @configuration
- 정렬
- @Data
- 고차원 함수
- 스프링 스케쥴러
- Java
- @Getter
- 알고리즘
- 영속 자료구조
- 모던 자바 인 액션
- 코딩 테스트
- 기능개발
- 해시
- @EnableScheduling
- 완주하지 못한 선수
- 다리를 지나는 트럭
- kubenetes
- 프로그래머스
- 루씬 인 액션
- 쿠버네티스
- 검색 기능 확장
- 롬복 어노테이션
- @Setter
- H-index
- 전화번호 목록
- K번째수
- 가장 큰 수
- 크론 표현식
- 스택/큐
- 커링
Archives
- Today
- Total
Today I Learned
오류를 예방하는 타입 안정성 (2) 본문
728x90
제네릭 : 파라미터 타입의 가변성과 제약사항
- 제네릭을 사용하면 다양한 타입에서 사용 가능한 코드를 만들 수 있다.
- Java의 제네릭
- 타입 불변성
- <? extends T>와 <? super T> 문법을 사용해서 파라미터 타입 T의 자식 클래스와 부모 클래스도 사용할 수 있다.
- 하지만 이 문법은 제네릭을 사용할 땐 사용 가능하고(사용처 가변성), 제네릭을 선언할 때는 사용이 불가능하다(선언처 가변성).
타입 불변성
- 메소드가 타입 T의 제네릭 오브젝트를 받는다면(ex. List<T>), T의 파생클래스(부모, 자식)를 전달할 수 없다.
open class Fruits
class Banana : Fruit()
class Orange : Fruit()
fun receiveFruits(friuts: Array<Fruit>) {
println("Number of fruits: ${fruits.size}")
}
val bananas: Array<Banana> = arrayOf()
receiveFruits(bananas) //ERROR: type mismatch
- Array<Banana>가 Array<Fruit>을 인자로 받는 메소드에 인자로 전달될 수 있다면 Orange를 Array<Fruit>에 담게 될 때 문제가 발생한다.
- 이번에는 Array<Fruit> 대신에 List<Fruit>으로 파라미터 타입을 변경해보자.
fun receiveFruits(friuts: List<Fruit>) {
println("Number of fruits: ${fruits.size}")
}
val bananas: List<Banana> = listOf()
receiveFruits(bananas) //OK
- List<T>는 이뮤터블하기 때문에 Orange를 List<Fruit>에 추가할 수 없다. 따라서 에러가 발생하지 않는다.
공변성 사용하기
fun copyFromTo(from: Array<Fruit>, to: Array<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) // ERROR: typeMismatch
- Banana가 아닌 Fruit을 Array<Banana>에 추가하면 안되기 때문에 에러가 발생한다.
fun copyFromTo(from: Array<out Fruit>, to: Array<Fruit>) {
for (i in 0 until from.size) {
to[i] = from[i]
}
}
copyFromTo(bananaBasket, fruitsBasket) // OK
- from 파라미터는 파라미터의 값을 읽기만 하기 때문에 Array<T>의 T에 Fruit 클래스나 하위 클래스가 전달되더라도 위험이 없다.
이것을 타입이나 파생 타입에 접근하기 위한 파라미터 타입의 공변성이라고 한다. - T의 자식 클래스들을 전달 가능하게 만들기 위하여 from: Array<out T> 문법을 사용할 수 있다.
- Array<T> 클래스는 T 타입의 객체를 읽고, 쓰는 메소드를 모두 가지고 있지만, 공변성을 사용하기 위해서 Array<T> 파라미터에서 어떤 값도 추가하거나 변경하지 않겠다는 약속을 해야 한다.
선언처 가변성: 선언하는 시점에 공변성을 사용하겠다고 지정(ex. List<out T>)
사용처 가변성: 사용하시는 시점에 공변성을 사용하겠다고 지정 = 타입 프로젝션 (ex. Array<out T>)
반공변성 사용하기
val things = Array<Any>(3) { _ -> Fruit() }
val bananaBasket = Array<Banana>(3) { _ -> Banana() }
copyFromTo(bananaBasket, things) //Error:type mismatch
- to 파라미터를 유연하게 사용하고자 Array<Any> 타입의 파라미터를 넣으면 에러가 발생한다.
fun copyFromTo(from: Array<out Fruit>, to: Array<in Fruit>) {
for (i in 0 until from.size) {
to[i] = from[i]
}
}
copyFromTo(bananaBasket, things) // OK
- to 파라미터를 Array<in T> 형식으로 변경하면 에러가 발생하지 않는다.
- in 키워드는 메소드가 파라미터에 값을 설정할 수 있게 만들고, 값을 읽을 수 없게 만든다.(반공변성)
- <in T>로 정의되면 전체적으로 파라미터 타입을 받을 수만 있고 리턴하거나 다른 곳으로 보낼 수는 없는 반공변성으로 특정된다.
where를 사용한 파라미터 타입 제한
- 제네릭을 사용할 때 제약조건이 필요한 경우도 있다.
fun <T> useAndClose(input: T){
input.close() //ERROR: unresolved reference: close
}
- 위의 예제에서 타입 T는 close() 메소드를 서포트해야하지만, 어떤 타입은 close() 메소드가 없다.
- 인터페이스를 통해서 close() 메소드만 들어올 수 있도록 제약을 걸 수 있다.
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\n2
구체화된 타입 파라미터
abstract class Book(val name: String)
class Fiction(name: String) : Book(name)
class NonFiction(name: String) : Book(name)
val books: List<Book> = listOf(
Fiction("Moby Dick"), NonFiction("Lean to Code"), Fiction("LOTR"))
- 위 코드에서 List<Book>에서는 Fiction과 NonFiction이 섞여있다.
- 이제 list 안의 Fiction과 NonFiction 중 특정 타입의 첫 번째 인스턴스를 찾아야 한다고 생각해보자.
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(book, NonFiction::class.java).name) //Learn to Code
- 바이트코드로 컴파일되면서 파라미터 타입 T가 지워지기 때문에 함수 안에서 T를 book is T나 selected[0] as T 처럼 연산자와 함께 사용할 수 없다.
- 따라서 원하는 객체 타입을 파라미터로 던지고 ofClass로 타입 체크와 타입 캐스팅을 해야한다.
- 하지만 코틀린에서는 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
}
- inline 함수는 컴파일 시점에 확정되므로 함수 호출 시에 오버헤드가 없는 함수이다. 자세한건 추후에 다시 다루겠다.
- 타입 T를 reified로 선언하면 함수 안에서 T를 타입 체크와 캐스팅 용으로 사용 가능하다.
- Reified 타입 파라미터는 함수에 추가적인 클래스 정보를 전달하지 않도록 만들어주고, 안전하게 캐스팅하는데 도움을 주며 컴파일 시간 안정성을 확보한 채로 리턴타입을 커스터마이징 할 수있게 해준다.
728x90
'Kotlin > 다재다능 코틀린 프로그래밍' 카테고리의 다른 글
클래스 계층과 상속 (0) | 2023.08.24 |
---|---|
객체와 클래스 (0) | 2023.08.01 |
오류를 예방하는 타입 안정성(1) (0) | 2023.07.27 |
콜렉션 사용하기 (0) | 2023.07.27 |
외부 반복과 아큐먼트 매칭 (0) | 2023.07.21 |
Comments