[Kotlin Tutorial] Operator overload 와 convention #2
출처 : Kotlin in action
7.3. Destructuring declarations and component functions
-
val p = Point(10, 20)
val (x, y) = p // destructuring
println(x)
println(y)
-
destructuring 도 convention 을 사용한다.
componentN 이 호출된다. ( N 은 숫자 )
val (a, b) = p
// val a = p.component1()
// val b = p.component2()
-
data class 는 compiler 가 primaryConstructor 에 정의된 모든 property 에 대해 componentN 함수를 자동 생성한다.
// data class 였다면 아래 component 함수들은 자동 생성
class Point(val x: Int, val y: Int){
operator fun component1() = x
operator fun component2() = y
}
-
destructuring declaration 이 유용한 경우는 한 개의 function에서 여러개의 value 를 return 하고자 할 때이다.
data class NameComponents(val name:String, val extension: String)
fun splitFilename(fullName: String): NameComponents{
val (name, extension) = fullName.split(‘.’, limit = 2) // componentN 은 array 와 collection 에도 정의되어 있다.
return NameComponents(name, extension)
}
val (name, ext) = splitFilename(“example.kt”)
println(name)
println(ext)
여러 개의 value 를 return 할 때 Pair 와 Triple 을 사용하는 것이 더 간단하고 좋은 경우도 많다.
7.4.1. Destructuring declarations and loops
-
destructuring 이 가장 유용하게 사용되는 사례 중 하나는 map iteration 이다.
fun printEntries(map: Map<String, String>){
for((key, value) in map){
println(“$key -> $value”)
}
}
map 에 iterator 가 구현되어 있다는 것과,
Entry 에 component1 과 component2 가 구현되어 있다는 것을 알 수 있다.
7.5. Reusing property accessor logic: delegated properties
-
delegated properties 는 backing field 에 값을 저장하는 것이 아닌 조금 더 복잡한 로직을
모든 accessor 를 override 하지 않고 구현할 수 있다.
예를 들면 property 는 value 를 database table 에 저장한다던지, browser session 에 저장한다던지, map 에 저장한다던지 등등..
이것의 기본 원리는 delegation 이다.
7.5.1. Delegated properties: the basics
-
아래가 delegated property 의 기본 syntax 이다.
class Foo{
var p: Type by Delegate()
}
p 의 accessor 의 기능을 Delegate class 에 위임한다.
아래와 같은 코드가 생성된다고 보면 된다.
class Foo{
private val delegate = Delegate()
var p: Type
set(value:Type) = delegate.setValue(…, value)
get() = delegate.getValue(…)
}
-
Delegate class 는 convention 으로써 getValue 와 setValue(read-only 가 아닐 때만) 가 있어야 한다.
member 함수로 있어도 되고 extension 이어도 된다.
class Delegate{
operator fun getValue(…){ … }
operator fun setValue(…, value:Type){ …}
}
7.5.2. Using delegated properties: lazy initialization and “by lazy()”
-
class Person(val name:String){
private var _emails: List<Email>? = null
val emails: List<Email>
get(){
if(_emails == null){
_emails = loadEmails(this)
}
return _emails!!
}
}
_emails 와 emails 를 분리시킨 것을 backing property technique 이라고 부른다.
lazy initialization 에 주로 쓰이는 방법이다.
좋은 방법이긴 하지만 이는 thread-safe 하지 않고, lazy init 해야 하는 것들이 많으면 코드 복잡성이 높아진다.
-
Kotlin 이 이런 것들을 그냥 내둘리가 없다.
lazy 라는 standrad lib function 으로 해결 할 수 있다.
class Person(val name:String){
val emails by lazy{ loadEmails(this) }
}
lazy 는 lambda 를 param 으로 받는다.
lazy 는 기본적으로 thread-safe 하다. 어떤 lock 을 쓸지 option 을 줄 수 있고, thread-safe 옵션을 끌 수도 있다.
7.5.3. Implementing delegated properties
-
어떤 property 가 변경되었음을 알기 위해서 Java 에서는 PropertyChangeSupport 와 PropertyChangeEvent class 를 사용했어야 한다.
open class PropertyChangeAware{
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener){
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener){
changeSupport.removePropertyChangeListener(listener)
}
}
class Person(val name:String, age: Int, salary: Int) : PropertyChangeAware(){
var age: Int = age
set(newValue){
val oldValue = field
field = newValue
changeSupport.firePropertyChange(“age”, oldValue, newValue)
}
var salary: Int = salary
set(newValue){
val oldValue = field
field = newValue
changeSupport.firePropertyChange(“salary”, oldValue, newValue)
}
}
코드를 refactoring 해보자.
class ObservableProperty(val propName:String, var propValue: Int, val changeSupport: PropertyChangeSupport){
fun getValue(): Int = propValue
fun setValue(newValue: Int){
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(propName, oldValue, newValue)
}
}
class Person(val name:String, age:Int, salary:Int):PropertyChangeAware(){
val _age = ObservableProperty(“age”, age, changeSupport)
var age:Int
get() = _age.getValue()
set(value){ _age.setValue(value) }
val _salary = ObservableProperty(“salary”, salary, changeSupport)
var salary: Int
get() = _salary.getValue()
set(value){ _salary.setValue(value) }
}
또 한번 refactoring 해보자.
class ObservableProperty(var propValue: Int, val changeSupport: PropertyChangeSupport){
operator fun getValue(p:Person, prop:KProperty<*>): Int = propValue // KProperty 에 대해서는 나중에 배운다
operator fun setValue(p:Person, prop:KProperty<*>, newValue: Int){
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
}
class Person{ val name:String, age:Int, salary: Int): PropertyChangeAware(){
var age: Int by ObservableProperty(age, changeSupport) // 알아서 hidden property 를 만들어 처리해준당
var salary: Int by ObservableProperty(salary, changeSupport)
}
-
여기서 끝이 아니겠지… ObservableProperty 를 직접 구현할 필요도 없다…
Kotlin 의 standard lib 에는 이미 ObservableProperty 비슷한 녀석이 구현되어 있다. 그러나 위의 예와는 다르게 PropertyChangeSupport 와 연결되어 있지는 않다.
class Person(val name:String, age:Int, salary:Int):PropertyChangeAware(){
private val observer = {
prop:kProperty<*>, oldValue:Int, newValue:Int -> changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
var age: Int by Delegates.observable(age, observer)
var salary: Int by Delegates.observable(salary, observer)
}
-
by 의 오른쪽은 new instance creation 일 필요가 없다.
이 부분은 다른 function call, 다른 property 연결, 다른 expression 이 될 수 있다. ( 해당 값에 compiler 가 getValue, setValue 를 부를 수만 있다면 )
7.5.4. Delegated-property translation rules
-
class C{
var prop: Type by MyDelegate()
}
val c = C()
위의 코드는 아래 코드를 생산한다고 보면 된다.
// <delegate> 와 <property> 는 적절한 이름을 보여줄 수 없어서.. 그냥 저렇게 씀
class C{
private val <delegate> = MyDelegate()
var prop:Type
get() = <delegate>.getValue(this, <property>)
set(value:Type) = <delegate>.setValue(this, <property>, value)
}
val x = c.prop // val x = <delegate>.getValue(c, <property>)
c.prop = x // <delegate>.setValue(c, <property>, x )
7.5.5. Storing property values in a map
-
delegated property 가 잘 사용되는 일반적인 경우는 동적으로 attribute set 이 결정되는 경우이다. ( nosql 처럼 생각하면 될듯 )
이것을 expando objects 라고 부른다.
class Person{
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName:String, value:String){
_attributes[attrName] = value
}
val name:String
get() = _attributes[“name”]!!
// delegation 을 쓰면 간단해 질 수 있다.
// val name:String by _attributes
}
Map, MutableMap interface 에 getValue, setValue 를 extension function 으로 define 해놓았기 때문에 가능하다.
7.5.6. Delegated properties in frameworks
-
property 의 저장과 수정방법을 변경하는 것은 framework 개발자에게 아주아주 유용하다.
이것이 db framework 에 어떻게 쓰이는지는 나중에 배우겠다.
object Users: IdTable(){
val name = varchar(“name”, length=50).index() // Column<String> type
val age = integer(“age”) // Column<Int> type
}
class User(id:EntityID): Entity(id){
var name: String by Users.name
var age: Int by Users.age
}
Column class 는 framework 에서 getValue 와 setValue 를 정의해 놓았다.
7.6. Summary
-
Kotlin 은 몇몇의 mathematical operation 의 overload 를 지원한다.
이 overload 는 convention 이라고 해서 이름에 mapping 되며, 새로운 operator 를 정의할 수는 없다.
-
비교 operator 들은 equals 와 compareTo 에 매핑된다.
-
get, set, contains convention function 은 [], in operation 을 가능하게 한다.
-
range 를 만들고 collection 과 array 를 iterating 하는 것 역시 convention 을 통해 된다.
-
destructing declaration 은 한 개의 object 로부터 여러 개의 variable 을 한번에 assign 하는 것을 가능하게 한다.
이것은 한 function 에서 여러 개의 결과값을 return 할 때 유용하다.
data class 는 primary constructor 와 연결되어 자동으로 사용할 수 있고, componentN convention 을 사용해서 스스로 define 할 수도 있다.
-
delegated properties 는 property value 가 어떻게 store, initialiize, access, modify 되는지를 control 할 수 있다.
이것은 framework 를 만드는데 매우 유용한 툴이 될 수 있다.
-
lazy 함수는 property 를 lazy init 하는데 아주 큰 도움을 준다.
-
Delegates.observable 함수는 property change observer 를 달 수 있게 도와준다.
-
delegated properties 는 object 가 다양한 attribute set 을 가졌을 경우 map 과 함께 사용하는 것을 편하게 해준다.
댓글