이 글은 Kotlin In Action을 참고 하였습니다.
더욱 자세한 설명이나 예제는 직접 책을 구매하여 확인 하시기 바랍니다
이 글은 Kotlin In Action을 참고 하였습니다.
더욱 자세한 설명이나 예제는 직접 책을 구매하여 확인 하시기 바랍니다
7.3 Collection과 range의 convention
7.3.1 Index operator
data class Point(val x: Int, val y: Int)
operator fun Point.get(index: Int): Int {
return when(index) {
0 -> x
1 -> y
else ->
throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
fun main(args: Array) {
val p = Point(10, 20)
println(p[1])
}
syntax는 아래와 같습니다.
x[a, b] -> x.get(a, b)
즉 배열의 원소를 읽거나 쓰는것처럼 표기하면 내부적으로 get, set함수로 변환됩니다.
get의 인자로 Int가 꼭 들어올 필요는 없습니다. map 같은 경우 key의 type이 인자로 들어오겠죠?
data class MutablePoint(var x: Int, var y: Int)
operator fun MutablePoint.set(index: Int, value: Int) {
when(index) {
0 -> x = value
1 -> y = value
else ->
throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
fun main(args: Array) {
val p = MutablePoint(10, 20)
p[1] = 42
println(p)
}
set 역시 get과 동일하게 적용하면 됩니다.
인자는 여러개가 될수 있으며, 맨 마지막 인자가 value가 되고 나머지들은 index가 됩니다.
※set은 당연히 property가 var로 선언된 경우에만 가능합니다.
x[a, b] = c -> x.set(a ,b, c)
7.3.2 in convention
data class Point(val x: Int, val y: Int)
data class Rectangle(val upperLeft: Point, val lowerRight: Point)
operator fun Rectangle.contains(p: Point): Boolean {
return p.x in upperLeft.x until lowerRight.x &&
p.y in upperLeft.y until lowerRight.y
}
fun main(args: Array) {
val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect)
println(Point(5, 5) in rect)
}
a in c -> c.contains(a)
※참고로 1..10 은 1~10 까지이며, 1 until 10 은 1~9까지 입니다.
7.3.3 rangeTo convention
fun main(args: Array) {
val n = 9
println(0..(n + 1))
(0..n).forEach { print(it) }
}
start..end -> start.rangeTo(end)
Comparable 인터페이스에는 기본적인 rangeTo가 구현되어 있어 따로 만들지 않아도 가능합니다.
위 예제처럼 rangeTo 함수는 연산자 우선순위가 낫지만 괄호로 묶어서 명시적으로 사용하는게 좋습니다.
7.3.4 iterator for loop
import java.util.Date
import java.time.LocalDate
operator fun ClosedRange.iterator(): Iterator =
object : Iterator {
var current = start
override fun hasNext() =
current <= endInclusive
override fun next() = current.apply {
current = plusDays(1)
}
}
fun main(args: Array) {
val newYear = LocalDate.ofYearDay(2017, 1)
val daysOff = newYear.minusDays(1)..newYear
for (dayOff in daysOff) { println(dayOff) }
}
위 예제를 보면 실제 in 연산자는 iterator의 next()와 hasNext()로 이루어진것을 알수 있습니다.
7.4 Destructuring declaration 과 component 함수
fun printEntries(map: Map) {
for ((key, value) in map) {
println("$key -> $value")
}
}
data class Point(val x: Int, val y: Int)
fun main(args: Array) {
val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
val p = Point(10, 20)
val (x, y) = p
println(x)
println(y)
}
Map을 iteration 할때 key, value를 동시에 받아올 수 있습니다.
이는 코틀린 표준 라이브러리에서 Map대한 확장 함수로 iterator를 지원하며, Map.Entry에 대한 확장 함수로 component1 (key)과 component2 (value)를 제공합니다.
또한 변수를 두개 이상 지정하고 property를 한번에 받아오는것도 가능하면 이것을 destructuring이라 합니다.
이는 내부적으로 componentN (N은 숫자)의 함수와 연결된 convention 입니다.
val (a, b) = p -> val a = p.component1
val b = p.component2
data class는 property 순서대로 componentN을 자동 생성해 줍니다.
일반 클래스라도 componentN()을 operator fun으로 선언하면 사용이 가능합니다.
특히 이런 구조분해는 함수의 리턴값이 여러개 일때 유용하게 쓰일 수 있습니다.
data class NameComponents(
val name: String,
val extension: String)
fun splitFilename(fullName: String): NameComponents {
val (name, extension) = fullName.split('.', limit = 2)
return NameComponents(name, extension)
}
fun main(args: Array) {
val (name, ext) = splitFilename("example.kt")
println(name)
println(ext)
}
위 예제에서는 split 함수로 반환된 List 역시 component 함수로 받았습니다.
배열과 collection에서도 destructuring을 지원하나, 무한정 지원할 수는 없기 때문에 5개 까지만 허용합니다.
또한 Pair을 이용하거나, 코틀린에서 제공하는 Triple을 이용하는 방법도 있습니다.
7.5 Property의 delegation
class Foo {var p: type by Delegate()}
실제 이 코드는 아래와 같이 풀어 집니다.
class Delegate {
operator fun getValue(param1,param2....) = {...}
operator fun setValue(param1,param2....,value) = {...}
}
class Foo {
private val delegate = Delegate()
var p: Type
set(value: Type) = delegate.setValue(param1,param2....,value)
getValue() = delegate.getValue(param1,param2....)
}
즉 by를 통해서 property의 동작을 위임받는 class는 getValue(), setValue()를 반드시 구현해야 합니다. (convention임)
7.5. by lazy()를 이용한 초기화 지연
class Email { /*...*/ }
fun loadEmails(person: Person): List {
println("Load emails for ${person.name}")
return listOf(/*...*/)
}
class Person(val name: String) {
private var _emails: List? = null
val emails: List
get() {
if (_emails == null) {
_emails = loadEmails(this)
}
return _emails!!
}
}
fun main(args: Array) {
val p = Person("Alice")
p.emails // 이때 한번만 로그가 찍힌다. -> Load emails for....
p.emails
}
한번만 읽기위해 var _emails라는 내부 property를 선언해서 사용했습니다.
emails 는 val type이기 때문에 변수를 두개 사용할 수 밖에 없습니다.
단 이 작업을 아래 한줄로 바꿀수 있습니다.
class Email { /*...*/ }
fun loadEmails(person: Person): List {
println("Load emails for ${person.name}")
return listOf(/*...*/)
}
class Person(val name: String) {
val emails by lazy { loadEmails(this) }
}
fun main(args: Array) {
val p = Person("Alice")
p.emails
p.emails
}
by lazy {....}를 사용하면 아래와 같은 동작을 보장합니다.
- 한번만 초기화 한다
- 초기화시 람다{...}의 구문이 사용된다.
- Thread-safe 하다.
- lock이 따로 필요하다면 넘길 수 있다.
- Thread-safe할 필요가 없다면 없게 할수 있다.
7.5.3 Delegation property with observing pattern
import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener
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)
}
}
fun main(args: Array) {
val p = Person("Dmitry", 34, 2000)
p.addPropertyChangeListener(
PropertyChangeListener { event ->
println("Property ${event.propertyName} changed " +
"from ${event.oldValue} to ${event.newValue}")
}
)
p.age = 35
p.salary = 2100
}
기본적인 방법입니다.
봉급과 나이가 바뀌면 listener에게 notify해주는 형태입니다.
Property 변경시 notify 해주도록 set을 custom하게 변경한 형태 입니다.
하지만 setter에 중복 코드가 많으니 이를 class로 추출해 보겠습니다.
open class PropertyChangeAware {
// 위와동일
}
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) }
}
fun main(args: Array) {
// 위와동일
}
단순히 setter의 중복 부분을 class로 추출한 형태입니다.하지만 여기서도 _age 객체나, _salary 객체를 추가적으로 만들어 사용해야 합니다.
이 코드를 얼마나 더 단순화 시킬수 있는지 아래 예제와 비교해 보지요.
import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener
import kotlin.reflect.KProperty
open class PropertyChangeAware {
// 위와 동일
}
class ObservableProperty(var propValue: Int, val changeSupport: PropertyChangeSupport) {
operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue
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)
var salary: Int by ObservableProperty(salary, changeSupport)
}
fun main(args: Array) {
// 위와 동일
}
get/set convention에 따라 operator를 갖는 class를 만들었습니다.
이때 첫번째 인자로 receiver object를 받습니다. 따라서 person 객체를 받아오고 두번째 param으로 KProperty를 받아옵니다.
이는 추후에 (10장...) 자세히 언급되지만 여기서는 prop.name을 사용하여 메소드가 사용할 property 이름을 얻어온다고만 알고 있으면 됩니다.
따라서 인자에서 따로 propery name을 받아올 필요도 없습니다.
import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener
import kotlin.properties.Delegates
import kotlin.reflect.KProperty
open class PropertyChangeAware {
// 위와 동일
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)
}
fun main(args: Array) {
// 위와 동일
}
by의 우항은 꼭 새로운 instance를 만들어낼 필요는 없습니다.
함수나, 다른 property일수도 있습니다만 결론적으로 생성된 클래스가 getValue와 setValue만 명확하게 제공하면 됩니다.
7.5.4 Delegation compile
// 실제 코트
class Foo {
var prop: Type by DelegationObj()
}
fun main) {
val c = Foo()
}
// 컴파일된 표현
class Foo {
private val = DelegationObj()
var prop: Type
get() = .getValue(this, )
set() = .setValue(this, , value)
}
val x = c.prop -> val x = <delegate>.getValue(c, <property>)
c.prop = y -> <delegate>.setValue(c, <property>, y)
by를 이용하면 간단한 표현으로 property 동작의 재구성이 가능해 집니다.
이는 property는 저장하는 위치를 맵, DB, network등에 할수 있도록 하거나, 읽을때 추가작업을 넣을때 편리하게 사용할 수 있습니다.
7.5.5 Map에 property 값 저장
class Person {
private val _attributes = hashMapOf()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String
get() = _attributes["name"]!!
}
fun main(args: Array) {
val p = Person()
val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
for ((attrName, value) in data)
p.setAttribute(attrName, value)
println(p.name)
}
위 예제는 사람정보를 저장할때 이름은 기본정보로 저장하고, 추가적인 정보는 Map 담는 경우 입니다.
저장은 for 문으로 하고, p.name으로 이름을 가져올때는 "name"을 key로 갖는 정보를 정보를 가지고 옵니다.
다만 Map이나 MutableMap에서는 인터페이스에서 getValue와 setValue 모두를 제공하기 때문에 아래와 같이 더 간결하게 사용할 수 있습니다.
class Person {
private val _attributes = hashMapOf()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String by _attributes
}
fun main(args: Array) {
//의와 동일
}
'개발이야기 > Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린 Generic #1 (1) | 2018.05.12 |
---|---|
[Kotlin] 코틀린 High order function (0) | 2018.05.09 |
[Kotlin] 코틀린 연산자 오버로딩 #1 - 산술연산자, 비트연산자, equals, compareTo (3) | 2018.05.03 |
[Kotlin] 코틀린 Collection과 배열 (3) | 2018.05.02 |
[Kotlin] 코틀린 원시타입 - Unit, Nothing, Int, Boolean.. (1) | 2018.04.30 |