Kotlin

Kotlin 문법 - 중수편

팅리엔 2022. 10. 3. 23:23

헷갈릴만한 코틀린 문법을 정리해본다.

 

이전 글)

Kotlin 문법 - 입문편

Kotlin 문법 - 초보편

 

 


 

객체 선언

프로그램 전체에서 공유하는 하나뿐인 객체를 만들기 위해 Java에서는 싱글톤 패턴 코드를 작성하였다.

코틀린에서는 다음과 같이 간단하게 만들 수 있다.

object Person {
    var name: String = ""
    var age: Int = 0
    
    fun print() {
        println("$name, $age")
    }
}

fun main(args: Array<String>) {
    Person.name = "Sarah"
    Person.age = 42
    Person.print() //Sarah, 42
}

Person 식별자로 바로 객체에 접근한다.

Person은 타입 이름이기도 하여서 var person: Person = Person 와 같이 쓸 수도 있다.

 

 

동반자 객체 Companion Object

어떤 클래스의 모든 인스턴스가 공유하는 객체를 만들고 싶을 때 사용한다.

클래스 안에 포함되는 이름없는 객체이다.

class Person private constructor() {
    companion object {
        fun create(): Person {
            countCreated++
            return Person()
        }
        
        var countCreated = 0
            private set
    }
}

fun main(args: Array<String>) {
    val a = Person.create()
    println(Person.countCreated)
}

위 코드에서는 Person 인스턴스를 생성할 때, 지금까지 생성한 인스턴스의 개수를 세려고 직접적으로 생성자를 통하지 못하도록 강제하고 있다.

이제 인스턴스를 만들려면 Person.create() 함수를 호출해야 한다.

Java에서는 이런 의도를 구현하기 위해 static 키워드를 사용했으나 코틀린에는 static 키워드가 존재하지 않는다. static 효과를 얻고 싶으면 동반자 객체를 사용해야 한다.

 

동반자 객체는 자신이 속한 클래스의 이름으로 접근한다.

동반자 객체는 클래스당 한 개만 존재할 수 있다.

동반자 객체를 정의하면 Companion이라는 식별자가 자동으로 생겨, Person.create() 말고도 Person.Companion.create()으로도 호출할 수 있다.

 

 

inline 함수

함수를 호출하면 함수 속으로 실행 흐름이 점프하고, 함수가 끝나면 함수를 호출했던 지점으로 다시 점프한다. 이런 실행 흐름의 이동은 프로그램의 성능을 미세하게 저해한다. 

inline 함수를 사용하면 실행 흐름을 점프하지 않고 함수 호출문을 함수의 내용으로 바꿔넣기 때문에 성능을 조금이나마 개선할 수 있다.

inline fun hello() {
    println("Hello")
    println("World")
}

fun main(args: Array<String>) {
    hello()
    hello()
}

위 코드는 컴파일 될 때 아래와 같이 바뀐다.

fun main(args: Array<String>) {
    println("Hello")
    println("World")
    println("Hello")
    println("World")
}

그러나 성능을 개선시킨다고 모든 함수를 inline으로 바꾸는 것은 좋은 생각이 아니다.

문장이 많은 함수를 inline으로 바꾸면 프로그램의 크기가 기하급수적으로 늘어난다.

문장이 적고 빈번히 호출되는 함수만 inline으로 만들도록 한다.

 

또한, inline 함수는 재귀호출이 불가능하다.

함수 내용의 코드가 무한대로 늘어날 수 있기 때문이다.

 

 

const

val 변수 앞에 const 키워드를 붙이면 변수에 접근하는 코드를 변수에 저장된 값으로 대체시킨다. inline 함수와 비슷하다.

const val hello = "Hello" + "World"

object Foo {
    const val bar = "bar"
}

fun main(args: Array<String>) {
    println(hello) //Hello World
    println(Foo.bar) //bar
}

위 코드는 컴파일 될 때 아래와 같이 바뀐다.

fun main(args: Array<String>) {
    println("Hello World")
    println("bar")
}

val 변수는 런타임 시에 값이 할당되지만 const val 변수는 컴파일 시에 값이 할당된다.

즉,  const val 변수는 객체의 프로퍼티나 로컬 변수가 될 수 없다.

또한,  const val 변수는 오로지 리터럴로 이루어진 표현식만 저장이 가능하다. (문자열, 기본 자료형)

 

Java에서의 static final 상수와 같다고 생각하면 된다.

 

 

lateinit

클래스의 프로퍼티는 선언과 동시에 초기화하거나 init 블록 안에서 초기화되어야 한다. 하지만 그게 마땅치 않은 결우가 꼭 있다.

프로퍼티에 lateinit 키워드를 붙이면 바로 초기화하지 않아도 된다.

class Member {
	lateinit var firstName: String
    lateinit var lastName: String
    lateinit var age: Int
    
    val fullName: String
        get() = "$firstName $lastName"
}

fun main(args: Array<String>) {
    val member = Member()
    member.firstName = "Gildong"
    member.lastName = "Hong"
    
    println(member.fullName) //Gildong Hong
    println(member.age) //UninitializedPropertyAccessException
}

lateinit은 var 프로퍼티에만 붙일 수 있다.

프로퍼티에 값을 지정하지 않을 채 접근하면 예외가 발생한다.

프로퍼티가 초기화되었는지 확인하려면, 다음과 같이 한다.

if (member::age.isInitialized) {...}

 

 

Nullable 리시버

Nullable한 참조 변수에 null이 지정되어 있으면 함수 호출이 실행되지 않는다.

var member: Member? = null
member?.print() //무시됨

확장 함수를 응용하면 null이더라도 함수 호출이 가능하게 할 수 있다.

fun String?.isNumber() {
    if (this == null) {
        println("null...")
    }
}

fun main(args: Array<String>) {
    val empty: String? = null
    empty.isNumber() //null...
}

isNumber 확장 함수의 리시버 타입이 Nullable이기 때문에 표현식의 값이 null이어도 isNumber 확장 함수를 호출할 수 있다.

 

 

동반자 객체의 확장 함수

동반자 객체에도 확장 함수를 달 수 있다.

fun 클래스이름.Companion.함수이름 () {...}

동반자 객체는 클래스 이름만으로 접근할 수 있지만, 'fun 클래스이름.함수이름() {...}' 형태로 확장 함수를 추가하면 동반자 객체가 아닌 클래스에 멤버 함수가 추가되는 것이기에 Companion 식별자를 반드시 적어줘야 한다.

class Person { companion object }

Person.Companion.create() = Person()

fun main(args: Array<String>) = Person.create()

 

 

확장 함수의 리시버 타입이 상속 관계에 있을 때

open class AAA

class BBB : AAA()

fun AAA.hello() = println("AAA")
fun BBB.hello() = println("BBB")

fun main(args: Array<String>) {
    val a: AAA = BBB()
    a.hello() //AAA
}​

BBB.hello() 가 호출될 것 같지만 AAA.hello()가 호출된다.

확장 함수는 멤버 함수와 다르게 참조 변수가 실제로 가리키는 객체의 타입을 따르지 않고 참조 변수의 타입을 그대로 따른다.

 

 

추상 클래스 Abstract Class

여러 타입을 하나의 타입으로 묶기 위해 상속을 사용한다.

abstract class Employee {
    abstract fun getSalary(): Int
}

class SeniorDeveloper(var years: Int) : Employee() {
    override fun getSalary() = years * 20000
}

 

absract 키워드는 open 키워드를 포함한다.

추상 클래스는 프로퍼티도 가질 수 있다. (abstract var name: String)

 

 

인터페이스 Interface

interface Printable {
    fun print(): Unit
}

class AAA : Printable {
    override fun print() {
        println("Hello World")
    }
}

인터페이스는 멤버 함수, 추상 멤버 함수, 추상 프로퍼티를 가질 수 있다.

일반 프로퍼티와 생성자는 가질 수 없다.

 

 

인터페이스를 여러 개 상속할 때

interface Parent {
    fun hello(): Unit
}

interface Mother : Parent {
    override fun hello() = println("Hello Mother")
}

interface Father : Parent {
    override fun hello() = println("Hello Father")
}

class Child : Mother, Father {
    override fun hello() = super<Mother>.hello() //Hello Mother
}

super.hello()를 호출하면 Mother.hello()가 호출될지, Father.hello()가 호출될지 애매하다.

이럴 때 원하는 인터페이스의 super를 호출할 수가 있다. (super<Mother>.hello())

 

 

중첩 클래스 Nested Class

클래스 안의 또 다른 클래스를 의미한다.

밖과 안의 클래스가 중첩되어 있을 뿐, 완전히 별개의 클래스이다.

내부 클래스에서 바깥 클래스의 프로퍼티나 멤버 함수에 접근할 수 없다.

class Outer {
    class Nested {
        fun hello() = println("Hello World")
    }
}

fun main(args: Array<String>) {
    val a: Outer.Nested = Outer.Nested()
    a.hello() //Hello World
}

 

 

내부 클래스 Inner Class

내부 클래스는 인스턴스가 바깥 클래스의 인스턴스에 완전히 소속된다.

내부 클래스의 인스턴스를 생성하려면 '클래스이름.생성자()'가 아니라 '참조변수.생성자()'를 호출해야 한다.

내부 클래스는 바깥 클래스의 인스턴스로부터만 생성할 수 있다.

 

내부 클래스의 인스턴스는 자신이 속해있는 바깥 클래스의 인스턴스를 가리키는 참조변수를 가지고 있다. 바로 this@Outer이다.

class Outer(private val value: Int) {
    fun print() {
        println(this.value)
    }
    
    inner class Inner(private val innerValue: Int) {
        fun print() {
            this@Outer.print()
            println(this.innerValue + this@Outer.value)
        }
    }
}

fun main(args: Array<String>) {
    val obj: Outer = Outer(10)
    val innerObj: Outer.Inner = obj.Inner(20)
    innerObj.print()
    // 10
    // 30
}

 

 

데이터 클래스 Data Class

데이터 자체의 역할만 하는 클래스는 데이터 클래스로 선언할 수 있다.

data class Member(
    val name: String,
    val age: Int
)

fun main(args: Array<String>) {
    val a = Member("John", 30)
    val b = a.copy()
    val c = a.copy(age = 31)
    
    println(a == b) //true
    println(a == c) //false
}

데이터 클래스 규칙

  • 적어도 한 개의 프로퍼티를 가져야 한다.
  • 생성자 매개변수에 반드시 var 또는 val을 같이 써야 한다.
  • abstract, open, sealed, inner 키워드를 붙일 수 없다.
  • 인터페이스만 구현할 수 있다.
  • component, component2, ... 이름의 멤버 함수는 선언할 수 없다. (컴파일러가 사용하는 이름)

데이터 클래스 사용 장점

  • equals, hashCode, toString 멤버 함수가 자동으로 오버라이딩 된다.
  • equals 멤버 함수는 각 프로퍼티의 값이 서로 모두 같으면 true를 반환하게 오버라이딩 된다.
  • toString 멤버 함수는 "Member(name=..., age=...)" 형태로 문자열을 반환하게 오버라이딩 된다.
  • 객체를 복사하는 copy 함수가 자동으로 선언된다.
    a.copy(age = 31) 처럼 원하는 프로퍼티만 다른 값으로 지정하여 복사할 수 있다.

 

 

객체 분해하기

데이터 클래스의 인스턴스에 한해, 객체를 여러 개의 변수로 쪼갤 수 있다.

어떤 객체에서 필요한 부분만 변수로 추출할 때 사용하면 좋다.

data class Member(
    val name: String,
    val age: Int,
    val height: Int
)

fun main(args: Array<String>) {
    val (name, _, height) = Member("John", 22, 190)
    println(name) //John
    println(height) //190
}

사용되지 않는 변수의 이름은 어더스코어(_)로 지정하여 무시할 수 있다.

 

 

람다식 Lambda Expression

{매개변수 -> 반환 값} 형태의 람다식으로 함수를 더 간단하게 만들 수 있다.

fun main(args: Array<String>) {
    val f:(String) -> String
    f = {name: String -> 
        "Hello $name"
    }
    
    f("Hong") //Hello Hong
    f.invoke("Hong") //Hello Hong
}

(int) -> Unit 은 함수 타입이다. (함수를 저장할 수 있는 타입)

{name: String -> println("Hello $name")} 은 함수 리터럴이다. (함수를 나타내는 부분)

 

매개변수가 없는 함수를 만들고 싶으면 name: String -> 없이 함수의 내용만 나오면 된다.

 

함수 리터럴에는 return을 적지 않는다.

함수 내용의 맨 마지막 표현이 반환 값이 된다.

 

f("Hong")으로 함수를 호출할 수도 있지만, invoke 함수를 통해서도 호출할 수 있다.

변수가 nullable인 경우 invoke를 통해서 호출하는 편이 f?.invoke("hong") 와 같이 쓸 수 있어 널 처리에 더 편리하다.

 

 

익명 함수 Anonymous Function

함수 리터럴을 작성하는 또 다른 방법으로 익명 함수가 있다.

fun main(args: Array<String>) {
    val f:(String) -> Unit = f(name:String) : Unit {
        return "Hello $name"
    }
    
    f("Hong") //Hello Hong
    f.invoke("Hong") //Hello Hong
}

익명 함수는 람다식보다 조금 복잡하지만 return으로 반환 값을 직접 지정해줄 수 있어 버그를 일으킬 확률이 적다.

익명 함수에는 inline 같은 키워드를 붙일 수 없다.

 

 

it 식별자

람다식의 매개변수가 하나일 때는 매개변수 선언을 생략할 수 있다.

매개변수를 생략하면 it 이라는 특별한 식별자가 생성된다.

fun main(args: Array<String>) {
    val f:(String) -> String = {
    	"Hello $it"
    }
    
    f("Hong") //Hello Hong
}

 

 

함수 참조 Function Reference

fun minus(a: Int, b: Int) = println("minus1... $(a - b)")

object Object {
    fun minus(a: Int, b: Int) = println("minus2... $(a - b)")
}

class Class {
    fun minus(a: Int, b: Int) = println("minus3... $(a - b)")
}

fun main(args: Array<String>) {
    var f:(Int, Int) -> Unit
    f = ::minus
    f(20, 10) //minus1... 10
    
    f = Object::minus
    f(30, 10) //minus2... 20
    
    f = Class().minus
    f(40, 10) //minus3... 30
}

함수 이름 앞에 :: 를 붙이면, 표현식의 값은 그 함수의 참조값이 되고, 표현식의 타입은 그 함수의 시그니처에 맞는 함수 타입이 된다.

 

 

고차 함수 Higher-order Function

앞에서 살펴본 함수 리터럴은 보통 고차 함수를 위해 사용된다.

고차함수란, 인수를 함수로 받거나 함수를 반환하는 함수를 뜻한다.

fun decorate(task:() -> Unit) {
    println("---작업 시작---")
    task()
    println("---작업 완료---")
}

fun main(args: Array<String>) {
    decorate({
        println("Hello")
        // ---작업 시작---
        // Hello
        // ---작업 완료---
    })
    decorate({
        println("Hello")
        // ---작업 시작---
        // World
        // ---작업 완료---
    })
}

고차 함수의 마지막 매개변수 타입이 함수 타입이라면 함수 호출시 소괄호를 생략할 수 있다.

decorate({...}) 은 decorate {...} 로 쓸 수 있다.

인수가 여러 개일 경우 일반 인수만 소괄호로 감싸고, decorate(a, b, c) {...} 로 쓸 수 있다.

 

 

클로저 Closure

변수는 자신의 스코프를 가지고 있다. 로컬 변수는 함수 실행이 종료되면 소멸된다.

그러나 클로저를 이용하면 로컬 변수가 소멸하지 않는 것처럼 보이도록 할 수 있다.

fun print(num: Int): () -> Unit = {println("This is $num")}

fun main(args: Array<String>) {
    val f: () -> Unit = print(100)
    f() //This is 100
}

위 코드에서, print 함수는 () -> Unit 타입의 함수를 반환한다.

f가 호출되는 시점에 num 매개변수가 이미 사라지고 없지만, 함수 리터럴은 자신이 만들어질 때의 상황을 기억하고 있다.

즉, num 매개변수의 값을 복사해 갖고 있는다.

이렇게 함수가 만들어질 때 주변 상황을 기억하는 함수를 클로저라고 부른다.

 

 

리시버가 붙은 함수 리터럴

함수 리터럴을 확장 함수처럼 만들 수 있다.

fun main(args: Array<String>) {
    val max: Int.(other: Int) -> Int
    
    max = {other: Int -> 
        if (this > other) this
        else other
    }
    
    println(10.max(15)) //15
    println(max(10, 15)) //15
}

리시버가 붙은 함수 리터럴에는 리시버를 나타내는 this 키워드를 사용할 수 있다.

일반 함수 타입으로 호출할 때는 리시버를 첫 번째 인수로 전달하면 된다.

 

 

제네릭 Generic - 타입 매개변수

fun <T> toFunction(value: T) -> T = {value}

fun main(args: Array<String>) {
    val f: () -> Int = toFunction<Int>(101)
    println(f()) //101
}

 

 

여러 개의 타입 매개변수

fun <T, R> T.map(mapper: (T) -> R) : R {
    return mapper(this)
}

fun main(args: Array<String>) {
    val square: Int = 11.map<Int, Int> { it * it }
    println (square) //121
}

T.map 처럼 확장 함수의 리시버에도 타입 매개변수를 적용할 수 있다.

 

 

구체화된 Reified 타입 매개변수

타입 매개변수는 보통 일반 타입처럼 쓸 수 있지만, 어떤 상황에서는 그렇지 못하다.

fun <T> check() {
    val number = 0
    if (number is T) { //Error!
        println("T는 Int 타입입니다.")
    }
}

타입 매개변수는 is 연산자의 피연산자로 사용할 수 없다. 

코드가 컴파일되면 타입 정보가 제거되고 런타임에 T가 어떤 타입인지 알 수 없기에 애초에 컴파일 에러가 발생한다.

 

이렇게 사용하고 싶다면, 타입 매개변수에 reified를 붙이고 함수를 inline으로 선언해야 한다.

inline fun <reified T> check() {
    val number = 0
    if (number is T) {
        println("T는 Int 타입입니다.")
    }
}

fun main(args: Array<String>) {
    check<Int>()
}

reified 를 사용하면 런타임에 타입 정보를 알 수 있기에 classType: Class<T>와 같은 인자를 전달해주지 않아도 된다.

 

 

클래스와 인터페이스에서 제네릭 사용하기

class Pair<A, B>(val first: A, val second: B) {
    override fun toString() = "$first\n$second"
}

Pair<Int, Double>은 하나의 고유한 타입으로 취급되어 Pair<Int, Int>와는 서로 다른 타입이다.

 

 

제네릭이 적용된 클래스와 인터페이스 상속/구현하기

interface Plusable<T> {
    operator fun plus<other:T> : T
}

class Rectangle(val width: Int, val height: Int) : Plusable<Rectangle> {
    override fun plus(other: Rectangle) : Rectangle {
        return Rectangle(width + other.width, height + other.height)
    }
}

 

 

특정 타입을 상속/구현하는 타입만 인수로 받기

fun <T:Container> T.printValue() {
    println(this.getValue())
}

class AAA : Container {
    override fun getValue() : Int = 101
}

fun main(args: Array<String>) {
    AAA().printValue() //101
}

 

 

in / out 키워드

Int가 Any를 상속한다고 하더라도 ArrayList<Int> 타입과 ArrayList<Any> 타입은 다르다.

하지만 in / out 키워드를 사용하면 대입할 수 있다.

class AAA<out T>

class BBB<in T>

fun main(args: Array<String>) {
    val aaaSub = AAA<Int>()
    val aaaSuper: AAA<Any> = aaaSub
    
    val bbbSuper: BBB<Any>()
    val bbbSub: BBB<Int> = bbbSuper
    
    val star: AAA<*> = aaaSub
}

out T는 자바의 ? extends T 와 같다.

in T는 자바의 ? super T 와 같다.

(참고: Wildcards in Java)

 

타입 인수를 *로 지정하면 타입 인수가 무엇이든 상관없이 AAA 타입을 대입할 수 있다.

 

 

.. 연산자, Range Expression

.. 연산자는 범위를 표현한다.

fun main(args: Array<String>) {
    val oneToTen: IntRange = 1..10
    println(5 in oneToTem) //true
    
    val upperAtoZ: CharRange = 'A'..'Z'
    println('C' in upperAtoZ) //true
}

따라서 if (0 <= num && num <= 100) 코드는 더 잛고 읽기 쉬운 if (num in 0..100) 으로 쓸 수 있다.

 

.. 연산자는 operator fun rangeTo(other: 자유타입) : 자유타입 으로 오버로딩 할 수 있다.

 

클래스가 contains 연산자 멤버 함수를 갖고 있어야 in 연산자를 사용할 수 있다.

 

 

반복자 Iterator

반복자란, 특정 구간 속에 있는 원소를 하나씩 반복적으로 꺼내기 위한 인터페이스이다.

interface Iterator<out T> {
    operator fun next() : T
    operator fun hasNext() : Boolean
}
fun main(args: Array<String>) {
    val range: IntRange = 1..2
    val iter: Iterator<Int> = range.iterator()
    
    println(iter.hasNext()) //true
    println(iter.next()) //1
    println(iter.hasNext()) //true
    println(iter.next()) //2
    println(iter.hasNext()) //false
}

 

 

반복문 for

fun main(args: Array<String>) {
    for (i: Int in 1..5) {
        print("$i ") //1 2 3
        if (i > 3) {
            break
        }
    }
}

in 우측의 표현식은 다음과 같은 멤버 함수를 갖는 객체만 지정할 수 있다.

operator fun iterator(): Iterator<자유타입>

일반적은 in 연산자는 contains 연산자 멤버 함수가 있어야 하고,

for문 속의 in 연산자는 iterator 연산자 멤버 함수가 있어야 한다.

 

컴파일러는 for문을 최적화해주기 때문에 빈번한 객체 생성으로 인한 성능 저하를 걱정하지 않아도 된다.

 

 

배열 Array

val numbers: Array<Int> = arrayOf(10, 20, 30)

 

 

배열을 가변 인수로 활용하기

가변인수란 개수가 변하는 인자를 말한다.

자바는 ...을 이용해서 가변인수를 나타낸다.

코틀린에서는 가변인수 앞에 vararg를 붙인다.

배열을 가변인수로 사용하기 위해선 배열 앞에 *를 붙인다.

fun printSum(vararg numbers: Int) {
    var sum = 0;
    for (i: Int in numbers) {
        sum += i;
    }
    println(sum)
}

fun main(args: Array<String>) {
    val numbers: Array<Int> = arrayOf(1, 2, 3)
    printSum(*numbers) //6
}

 

 

 

vararg로 선언된 iterable 객체의 경우에 내부적으로 sum같은 메소드를 사용 할 수 있다.

fun printSum(vararg numbers: Int) {
    println(numbers.sum())
}

fun main(args: Array<String>) {
    val numbers: Array<Int> = arrayOf(1, 2, 3)
    printSum(*numbers) //6
}

 

 

열거 클래스 Enum Class

enum class Mode(val code: Int) {
    PEN(0), 
    SHAPE(1), 
    ERASER(2);
    
    fun printCode() {
        print("모드: $code")
    }
}

fun main(args: Array<String>) {
    val mode: Mode = Mode.PEN
    
    when (mode) {
        Mode.PEN -> println("펜")
        Mode.SHAPE -> println("도형")
        Mode.ERASER -> println("지우개")
    } //펜
    
    mode.printCode() //모드: 0
}

 

모든 열거 클래스는 Enum이라는 클래스를 상속한다. 

Enum 클래스에는 다음과 같은 멤버가 있다.

val name: String
val ordinal: Int //순서. 0부터 시작.

fun valueOf(value: String) : 열거클래스 //이름으로부터 열거 상수 찾음
fun values() : Array<열거클래스> //모든 상수 리스트

 

 

Sealed 클래스

Sealed 클래스는 자신의 Nested Class, Inner Class에만 상속을 허용하는 클래스이다.

sealed class Outer {
    class One : Outer()
    class Two : Outer()
    class Three : Outer()
}

//class Four: Outer() //Error!

fun main(args: Array<String) {
    val i: Outer = Outer.One()
    
    val text: String = when (i) {
        is Outer.One -> "1"
        is Outer.Two -> "2"
        is Outer.Three -> "3"
    }
    
    println(text) //1
}

1.1 버전 이후에는 Sealed 클래스와 같은 파일에 속해있으면 Sealed 클래스를 상속할 수 있다.

 

 

위임된 프로퍼티 Delegated Property

Int 타입의 프로퍼티에 음수가 저장되는 것을 방지하기 위해 Setter를 정의할 때가 자주 있다.

이때마다 모든 프로퍼티의 Setter를 반복적으로 정의하는 것을 방지하기 위해 Delegated Property 문법을 사용한다.

 

아래처럼 프로퍼티의 Getter/Setter 구현을 다른 객체에 맡길 수 있다.

class Sample {
    var number: Int by OnlyPositive()
}

class OnlyPositive {
    private var realValue: Int = 0
    
    operator fun getValue(thisRef: Any?, property: KProperty<*>) : Int {
        return realValue
    }
    
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        realVallue = if (value > 0) value else 0
    }
}

fun main(args: Array<String>) {
    val sample = Sample()
    
    sample.number = -50
    println(sample.number) //0
    
    sample.number = 50
    println(sample.number) //50
}

대리 객체는 getValue, setValue 멤버 함수를 가져야 한다.

 

 

클래스 위임 Class Delegation

위에서 살펴본대로 프로퍼티의 Getter/Setter의 구현을 다른 객체에 맡기는 것처럼,

인터페이스의 구현을 다른 클래스에 맡기는 문법도 있다.

반복적으로 똑같이 구현되는 인터페이스를 하나로 합치기 위해 사용한다.

interface Printable {
    operator fun print(text: String)
}

class ClassDelegator : Printable {
    override fun print(text: String) {
        println("$text...")
    }
}

class Sample : Printable by ClassDelegator()

fun main(args: Array<String>) {
    Sample().print("Hello World") //Hello World...
}

Sample의 print 멤버 함수를 호출하면 ClassDelegator의 print가 호출된다.

'Kotlin' 카테고리의 다른 글

Kotlin과 Java 함께 사용하기  (1) 2022.10.08
Kotlin 문법 - 표준 라이브러리편  (0) 2022.10.08
Kotlin 문법 - 초보편  (0) 2022.09.29
Kotlin 문법 - 입문편  (0) 2022.09.28
TypeScript 시작하기  (0) 2021.09.08