-
kotlin) Kotlin 의 지연과 위임kotlin 2024. 7. 18. 23:10
lateinit과 lazy
인스턴스화 시점과 프로퍼티 초기화 시점
class Person( val name: String, ) { val isKim: Boolean get() = this.name.startsWith("JS") //JS -> J* val maskingName: String get() = name[0] + (1 until name.length).joinToString("") { "*" } } fun main() { val person = Person("JS") //클래스 인스턴스화가 이루어지며, name 에 "JS"가 들어간다. }
- 클래스 인스턴스화 시점:
- val person = Person("JS")에서 Person("JS")를 호출하는 시점에 클래스 인스턴스화가 발생
- 이 시점에서 Person 클래스의 새로운 객체가 힙 메모리에 할당
- 프로퍼티 초기화 시점:
- Person("JS")를 호출할 때, 생성자가 실행되면서 클래스의 프로퍼티 name이 초기화
- 생성자의 매개변수 "JS"가 name 프로퍼티에 할당
인스턴스화 시점과 프로퍼티 초기화 시점을 분리하고 싶다면
1) name을 nullable 로 처리하자
/** * 기본값이 "홍길동" 에서 null 로 변경되었고 * 이에 따라 name 변수의 타입도 String? 로 변경되었다. */ class Person { var name: String? = null //실제 null 이 될 수 없기에, 계속 널 처리(?./?:/!!)가 들어가게 된다. val isKim: Boolean get() = this.name!!.startsWith("JS") val maskingName: String get() = name!![0] + (1 until name!!.length).joinToString("") { "*" } }
2) lateinit
- 인스턴스화 시점과 변수 초기화 시점을 분리하는것.
- 컴파일 단계에서 nullable 변수로 바뀌고, 변수에 접근하려고 할 때 null 이면 예외 발생
- primitive type(int, long) 에 사용할 수 없다
class Person { lateinit var name: String //초기값을 지정하지 않고, null이 들어갈 수 없는 변수를 선언 val isKim : Boolean get()= this.name.startsWith("JS") //null 이 들어갈 수 없으니 !! 같은 null 체크 구문이 사라짐 //null 이 들어갈 수 없으니 !! 같은 null 체크 구문이 사라짐 val maskingName: String get() = name[0] + (1 until name.length).joinToString("") {"*"} } fun main(){ val p = Person() p.isKim //에러 발생 : 초기화 시키지 않았는데 접근 }
변수를 초기화 할 때 지정된 로직을 1회만 실행
지연 초기화 1회 로직
//name 을 사용하지 않으면, Thread.sleep()이 호출되지 않지만 class Person{ //name을 쓸 때마다 sleep이 호출된다. val name: String get() { Thread.sleep(2_000) return "JS" } } //Thread.sleep() 은 1회만 호출되지만 class Person{ val name: String //name 을 사용하지 않는 경우에도 sleep 이 호출된다. init { Thread.sleep(2_000) name = "JS" } }
요구사항을 엄밀히 구현하려면, backing property 를 사용해야함 by lazy
//원초 코드 class Person{ private var _name: String? = null //name 이 사용되는 경우에만 _name 을 초기화 val name : String get(){ if (_name == null){ //꼭 필요할 때 1회만 호출 Thread.sleep(2_000L) this._name = "JS" } return this._name!! } } -> //by lazy class Person { val name: String by lazy{ //by lazy 를 통해 위 코드와 동일한 기능을 이용 Thread.sleep(2_000) "JS" } }
위임패턴
클래스가 다른 객체에게 그 책임을 위임하여 코드를 간결하고 유연하게 작성할 수 있도록 도와주는 패턴
class LazyInitProperty<T>(val init: () -> T){ private var _value: T? = null val value: T get(){ if(_value == null){ this._value = init() } return _value!! } } class Person{ //LazyInitProperty 를 갖고있게 하고 private val delegateProperty = LazyInitProperty{ Thread.sleep(2_000) "JS" } //실제 name 은 delegateProperty 를 통해 갖고오도록 설정 val name : String get()= delegateProperty.value //Person 의 getter 가 호출되면 곧바로 LazyInitProperty 의 getter 를 호출 } //위 코드를 "by" 로 간결하게 만들 수 있다. class Person { //name : 위임 프로퍼티, Lazy() : 위임 객체 val name: String by lazy { // by 뒤에 위치한 클래스는 getValue 혹은 setValue 함수를 갖고있어야 한다. Thread.sleep(2_000) // 즉 lazy 함수에는 getValue 가 제공되어있다. "JS" } }
Kotlin 의 by 로 위임해주는 코드 변화
//before class C { var prop: Type by MyDelegate() } //after class C { private val prop$delegate = MyDelegate() //private 변수를 내부에 만들어두고 var prop : Type //위임 프로퍼티의 getter / setter 가 호출될때 getVlaue / setValue 를 호출한다. get() = prop$delegate.getValue(this, this::prop) set(value: Type) = prop$delegate.setValue(this, this:prop,value) }
코틀린의 표준 위임 객체
notNull()
- 데이터의 nullability를 처리하는 데 사용
- Kotlin은 null 안전성을 언어 차원에서 지원하며, 이를 통해 null 포인터 예외(NullPointerException)를 피할 수 있음
- notNull() 함수는 주로 kotlin.properties.Delegates 객체의 일부로 제공되며, 위임된 속성의 값을 초기화하지 않고 null이 아닌 값을 보장할 때 유용
class Person{ var age: Int by notNull() //lateinit과 비슷한 역할 }
observable()
- observable() 함수는 속성 위임을 통해 속성 값의 변화를 감지하고, 값이 변경될 때마다 특정 동작을 수행할 수 있도록 함
- 객체의 상태 변화를 추적하고 반응할 필요가 있을 때 사용
- 속성 위임을 사용하여 속성 값이 변경될 때마다 호출되는 콜백을 설정
- 이 콜백은 속성 값이 변경되기 전의 값과 새로운 값을 매개변수로 받아, 값을 검증하거나, 로그를 남기거나, UI를 갱신하는 등의 작업을 수행
class Person{ var age: Int by observable(20) {_, oldValue, newValue -> println("옛날 값 : $oldValue , 새로운 값 : $newValue") } } fun mail(){ val p = Person() p.age = 30 //옛날 값 : 20 , 새로운값 : 30 p.age = 30 //옛날 값 : 30 , 새로운값 : 30 }
vetoable()
- setter가 호출될 때 onChange 함수가 true 를 반환하면 변경 적용 false 를 반환하면 이전 값이 그대로 남는다.
class Person{ var age: Int by vetoable(20) {_,_, newValue -> newValue >= 1} //1 이상인 경우에만 적용되도록 설정 즉 -10 이면 미적용 }
또 다른 프로퍼티로 위임하기
class Person{ @Deprecated("age를 사용하세요!",ReplaceWith("age")) var num: Int = 0 //코드 사용자들이 age 로 코드를 바꾸면 그때 num 을 제거 var age: Int by this::num }
Map
//getter 가 호출되면["name"] 또는 map["age"] 를 찾는다. class Person(map: Map<String,Any>){ val name: String by map val age : Int by map } fun main(){ var person = Person(mapOf("name" to "ABC")) println(person.name) println(person.age) //예외 발생 }
Iterable Sequence (feat. JMH)
우리는 데이터를 조작할 때 Collection 을 사용한다.
Iterable
ex. 2,000,000 개의 랜덤 과일 중 사과를 골라 10,000개의 가격 평균을 계산해보자!
fun main(){ val fruits = listOf( Fruit("사과",1000L), Fruit("바나나", 3000L), ) val avg = fruits .filter {it.name == "사과"} //주어진 200만건의 과일중 사과를 골라 임시 List<Fruit>를 만든다. .map {it.price} //List<fruit>에서 가격만 골라 List<Long>을 생성 .take(10_000) //List<Long> 에서 10000개를 골라 .average() //평균을 구한다. } data class Fruit( val name: String, val price: Long, )
연산의 각 단계마다 중간 Collection 이 임시로 생성된다.
Sequence
중간 Collection을 만들지 않는 방법
fun main(){ val fruits = listOf( Fruit("사과",1000L), Fruit("바나나", 3000L), ) val avg = fruits.asSequence() //asSequence 추가 .filter {it.name == "사과"} //주어진 200만건의 과일중 사과를 골라 임시 List<Fruit>를 만든다. .map {it.price} //List<fruit>에서 가격만 골라 List<Long>을 생성 .take(10_000) //List<Long> 에서 10000개를 골라 .average() //평균을 구한다. } data class Fruit( val name: String, val price: Long, )
동작 원리
- 각 단계(filter, map) 가 모든 원소에 적용되지 않을 수 있다.
- 한 원소에 대해 모든 연산을 수행하고, 다음 원소로 넘어간다.
- 또한, 최종연산이 나오기 전까지 계산 자체를 미리 하지 않는다.
- 이를 지연 연산이라고 한다.
결과
- asSequence() 하나로 50배 정도 빠르다.
- 하지만 수가 적으면 Iterable 이 더 빠르다.
주의할 점
- 연산 순서에 따라 큰 차이가 날 수 있다.
'kotlin' 카테고리의 다른 글
kotlin) 제네릭 (0) 2024.07.21 kotlin) 복잡한 함수형 크로그래밍 (0) 2024.07.20 kotlin) 추가적으로 알아두어야 할 코틀린 특성 (0) 2024.07.13 kotlin) 코틀린에서의 FP (0) 2024.07.09 kotlin) 코틀린에서의 OOP (0) 2024.07.06 - 클래스 인스턴스화 시점: