Coding Log


Kotlin

본 카테고리는 2017년 Android 공식 언어로 채택된 Kotlin에 관하여 다룬다.

Kotlin을 이용해 개발하는 Android는 추후 따로 다루기로 하고 언어 자체에만 집중한다.

참고 kotlin 공식 사이트

Class - 확장(Extensions)

Kotlin은 C#이나 Gosu처럼 클래스의 상속이나 디자인패턴의 적용없이도 새로운 기능을 클래스에 확장할 수 있는 기능을 제공한다.

참고 Gosu Programming Language

이는 extension이라는 선언을 통해 가능하며 Kotlin은 extension 선언에 대해 함수와 프로퍼티를 추가적으로 제공한다.

확장 함수(Extension Functions)

확장 함수를 선언하려면 receiver 타입을 접두사로 사용해야 한다.

아래의 예제 코드를 보자.

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

위의 코드에서 swap 함수를 MutableList<Int>에 추가된 것을 볼 수 있다.

this 키워드가 대응하는 리시버 객체가 바로 MutableList<Int>이다.

만약 아래와 같이 작성한다고 가정해보자.

val l = mutableListOf(1, 2, 3)
l.swap(0, 2) // 'this' inside 'swap()' will hold the value of 'l'

여기서 swap함수의 this l을 가리키게 된다.

이러한 확장 함수는 MutableList<T>에도 동일하게 적용되므로 아래와 같이 제네릭으로 선언할 수도 있다.

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

확장의 정적 결정(Extensions are resolved statically)

확장은 클래스를 변경하는 개념이 아니다.

확장은 클래스에 새로운 멤버를 추가적으로 넣는 것이 아니라, 단지 해당 클래스 타입에 점(.)을 통해 호출할 함수를 정의하는 것 뿐이다.

따라서 확장 함수는 정적으로 전달되는 것을 중점적으로 알아야 한다.

즉 리시버 타입에 따라 런타임에 동적으로 확장 함수가 결정되는 것이 아닌, 함수가 호출되는 표현식의 유형에 따라 정적으로 결정된다.

아래의 예시를 보자.

open class C

class D: C()

fun C.foo() = "c"

fun D.foo() = "d"

fun printFoo(c: C) {
    println(c.foo())
}

printFoo(D())

printFoo(D())를 호출하면 C를 출력한다.

파라미터로 넘긴 값은 D()이지만, 파라미터 타입의 정의는 C이므로 D.foo() 대신 C.foo()를 호출하게 된다.

아래의 또 다른 예시를 보자.

class C {
    fun foo() { println("member") }
}

fun C.foo() { println("extension") }

위의 코드를 보면 C클래스의 멤버함수 foo()도 있고, 확장 함수 C.foo()도 있는 것을 볼 수 있다.

이 때 클래스 C의 객체은 c에서 c.foo()를 호출하면 무조건 member를 호출하게 된다.

즉, 멤버함수와 확장함수가 동일한 이름과 파라미터를 가지고 있다면 항상 멤버함수를 호출한다.

멤버 함수와 이름이 같더라도 다른 타입의 형태를 오버로딩을 하면 별도로 사용할 수 있다.

아래의 예시를 보자

class C {
    fun foo() { println("member") }
}

fun C.foo(i : Int) { println("extension") }

이때 C().foo(1) extension을 출력한다.

Nullable Receiver

확장이 Nullable한 리시버를 가질 수 있도록 정의할 수도 있다.

이때 확장은 객체 변수가 null인 경우에도 호출할 수 있으며, this == null을 통해 검증할 수 있다.

아래 예제를 보자.

fun Any?.toString(): String {
    if (this == null) return "null"
    // after the null check, 'this' is autocast to a non-null type, so the toString() below
    // resolves to the member function of the Any class
    return toString()
}

만약 위와 같이 작성한다면 모든 객체의 toString()을 호출할 때 모든 클래스의 멤버함수로 처리되도록 할 것이다.

확장 프로퍼티(Extension Properties)

함수에도 확장이 있는 것처럼 프로퍼티에도 확장이 있다.

val <T> List<T>.lastIndex: Int
    get() = size - 1

위에서 서술했듯 확장은 클래스의 변경없이 진행되므로 지원 필드를 가진 확장 프로퍼티를 위한 효율적인 방법은 존재하지 않는다.

즉, 클래스의 변경이 없으므로 클래스에 멤버 변수를 추가하듯이 필드값을 추가할 수 없다는 뜻이다.

참고 따라서 확장 프로퍼티는 초기화를 허용하지 않는다.

따라서 확장 프로퍼티는 아무런 상태값도 가질 수 없으며, 아래의 코드처럼 작성하게 되면 오류가 발생한다.

val Foo.bar = 1 // error: initializers are not allowed for extension properties

짝 객체 확장(Companion Object Extensions)

클래스가 짝 객체를 정의하고 있다면, 해당 짝 객체를 위한 확장 함수와 확장 프로퍼티를 정의할 수 있다.

class MyClass {
    companion object { }  // will be called "Companion"
}

fun MyClass.Companion.foo() {
    // ...
}

호출할 때는 일반 멤버 함수처럼 호출하면 된다.

MyClass.foo()

확장의 범위(Scope of Extensions)

확장은 대부분 패키지와 같은 최상위 수준에 정의한다.

package foo.bar

fun Baz.goo() { ... }

특정 확장 함수를 선언한 패키지가 아닌 외부에서 해당 확장 함수를 사용할 경우엔 직접 함수 경로까지 import 시킨다.

package com.example.usage

import foo.bar.goo // importing all extensions by name "goo"
                   // or
import foo.bar.*   // importing everything from "foo.bar"

fun usage(baz: Baz) {
    baz.goo()
}

멤버로서의 확장 선언(Declaring Extensions as Members)

클래스 내부에 다른 클래스를 위한 확장을 선언할 수도 있다.

class D {
    fun bar() { ... }
}

class C {
    fun baz() { ... }

    fun D.foo() {
        bar()   // calls D.bar
        baz()   // calls C.baz
    }

    fun caller(d: D) {
        d.foo()   // call the extension function
    }
}

위의 클래스 C에서 클래스 D에 대한 확장 함수를 선언한 것을 볼 수 있다.

이 경우 클래스 D에는 묵시적으로 리시버 객체 멤버가 존재하게 된다.

확장 함수를 선언하고 있는 클래스의 인스턴스를 dispatch receiver라 하며, 확장 함수의 리시버 타입 객체를 extension receiver라고 부른다.

dispatch receiver extension receiver간의 이름이 동일할 경우 extension receiver가 더 우선순위에 있다.

class C {
    fun D.foo() {
        toString()         // calls D.toString()
        this@C.toString()  // calls C.toString()
    }

추가적으로 확장을 멤버로 가진 클래스에 open 키워드를 사용하여 하위 클래스에서 오버라이딩하도록 할 수 있다.

open class D {
}

class D1 : D() {
}

open class C {
    open fun D.foo() {
        println("D.foo in C")
    }

    open fun D1.foo() {
        println("D1.foo in C")
    }

    fun caller(d: D) {
        d.foo()   // call the extension function
    }
}

class C1 : C() {
    override fun D.foo() {
        println("D.foo in C1")
    }

    override fun D1.foo() {
        println("D1.foo in C1")
    }
}

C().caller(D())   // prints "D.foo in C"
C1().caller(D())  // prints "D.foo in C1" - dispatch receiver is resolved virtually
C().caller(D1())  // prints "D.foo in C" - extension receiver is resolved statically


DISQUS 로드 중…
댓글 로드 중…

트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다