Extension, Enum, Data class

유진·2024년 1월 27일
0
post-custom-banner

Extension

Extensions

코틀린은 클래스나 인터페이스를 상속하거나 Decorator와 같은 디자인 패턴을 사용하지 않고도 새로운 기능을 추가할 수 있는 방법을 제공합니다. 이를 확장(Extensions)이라고 부릅니다.

예를 들어, 수정할 수 없는 서드파티 라이브러리의 클래스나 인터페이스에 새로운 함수를 작성할 수 있습니다. 이러한 함수는 원래 클래스의 메소드처럼 평소처럼 호출할 수 있습니다. 이 메커니즘을 확장 함수라고 부릅니다. 또한, 기존 클래스에 새로운 속성을 정의할 수 있는 확장 속성도 있습니다.

➕해당 클래스의 밖에서 새로운 함수를 선언!

사용하는 것은 일반 함수랑 같이 쓰면 됨

Extension functions

확장 함수를 선언하려면 이름 앞에 수신자 타입을 붙여야 하는데, 이는 확장되는 타입을 가리킵니다. 예를 들면, MutableList<Int>swap 함수를 추가할 수 있습니다:

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this'는 리스트를 가리킵니다
    this[index1] = this[index2]
    this[index2] = tmp
}

확장 함수 내부의 this 키워드는 수신자 객체(점 앞에 전달되는 객체)를 가리킵니다. 이제 MutableList<Int>에 대해 이 함수를 호출할 수 있습니다:

val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'swap()' 내부의 'this'는 'list'의 값을 가집니다

이 함수는 모든 MutableList<T>에 대해 의미가 있으므로, 이를 제네릭으로 만들 수 있습니다:

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this'는 리스트를 가리킵니다
    this[index1] = this[index2]
    this[index2] = tmp
}

수신자 타입 표현식에서 제네릭 타입 매개변수를 사용하려면 함수 이름 앞에 제네릭 타입 매개변수를 선언해야 합니다.

Extensions are resolved statically

확장은 실제로 확장하는 클래스를 수정하지 않습니다. 확장을 정의함으로써 클래스에 새로운 멤버를 삽입하는 것이 아니라, 이 타입의 변수에 대해 새로운 함수를 점 표기법으로 호출 가능하게 만드는 것입니다.

확장 함수는 정적으로 디스패치됩니다. 즉, 어떤 확장 함수가 호출되는지는 컴파일 시간에 수신자 타입에 기반하여 이미 알려져 있습니다.

open class Shape
class Rectangle: Shape()

fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) {
    println(s.getName())
}

printClassName(Rectangle())

만약 클래스에 멤버 함수가 있고, 같은 수신자 타입, 같은 이름, 주어진 인수에 적용 가능한 확장 함수가 정의되어 있다면, 멤버는 항상 이긴다.

💡 같은 이름으로 된 함수가 클래스 안과 확장함수 둘 다 있다면 클래스 안에 있는 멤버 함수가 이김!
class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType() { println("Extension function") }

Example().printFunctionType()

확장은 같은 이름이지만 다른 시그니처를 가진 멤버 함수를 오버로드하는 것이 가능합니다.

class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType(i: Int) { println("Extension function #$i") }

Example().printFunctionType(1)

Nullable receiver

확장은 널 수신자 타입으로 정의될 수 있습니다. 이러한 확장은 객체 변수가 널인 경우에도 호출할 수 있습니다. 만약 수신자가 널이라면, this는 널입니다.

따라서 널 수신자 타입으로 확장을 정의할 때는 함수 본문 내에서 this == null 검사를 수행하는 것이 좋습니다.

Extension properties

코틀린은 확장 속성을 지원합니다. 이는 확장 함수를 지원하는 것과 유사하게 작동합니다.

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

하지만 확장은 실제로 클래스에 멤버를 삽입하지 않으므로, 확장 속성이 백업 필드를 가질 효율적인 방법이 없습니다. 그래서 initializers는 확장 속성에 허용되지 않습니다. 이들의 동작은 getter/setter를 명시적으로 제공함으로써만 정의될 수 있습니다.

💡 확장 속성은 아니어서 .으로 접근은 불가, getter/setter로 접근 가능
val House.number = 1 // error: initializers are not allowed for extension properties

Companion object extensions

클래스에 Companion object가 정의되어 있다면, Companion object에 대해서도 extension functions와 속성을 정의할 수 있습니다. Companion object의 일반 멤버와 마찬가지로, 이들은 클래스 이름만으로 호출할 수 있습니다.

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

fun MyClass.Companion.printCompanion() { println("companion") }

fun main() {
    MyClass.printCompanion()
}

Scope of extensions

확장은 대부분 패키지 바로 아래의 최상위 수준에서 정의합니다.

package org.example.declarations

fun List<String>.getLongestString() { /*...*/}

그 선언 패키지 바깥에서 확장을 사용하려면 호출 사이트에서 이를 가져와야 합니다.

package org.example.usage

import org.example.declarations.getLongestString

fun main() {
    val list = listOf("red", "green", "blue")
    list.getLongestString()
}

➕val 만 가능

Declaring extensions as members

확장을 멤버로 선언할 수도 있습니다.

이런 확장 내부에는 여러 implicit receivers가 있습니다. 이는 한 클래스 내부에서 다른 클래스에 대한 확장을 선언할 수 있다는 것을 의미합니다.
확장이 선언된 클래스의 인스턴스를 dispatch receiver라고 하고, 확장 메소드의 수신자 타입의 인스턴스를 extension receiver라고 합니다.

class Host(val hostname: String) {
    fun printHostname() { print(hostname) }
}

class Connection(val host: Host, val port: Int) {
    fun printPort() { print(port) }

    fun Host.printConnectionString() {
        printHostname()   // calls Host.printHostname()
        print(":")
        printPort()   // calls Connection.printPort()
    }

    fun connect() {
        /*...*/
        host.printConnectionString()   // calls the extension function
    }
}

fun main() {
    Connection(Host("kotl.in"), 443).connect()
    //Host("kotl.in").printConnectionString()  // error, the extension function is unavailable outside Connection
}

dispatch receiver와 extension receiver의 구성원 간에 이름이 충돌하는 경우에는 extension receive가 우선하며,dispatch receiver의 member를 참조하기 위해서는 적격한 this syntax을 사용하면 됩니다.

class Connection {
    fun Host.getConnectionString() {
        toString()         // calls Host.toString()
        this@Connection.toString()  // calls Connection.toString()
    }
}

Extensions이 멤버로 선언되면 open으로 선언되어 하위 클래스에서 오버라이드될 수 있습니다. 이는 이러한 함수의 dispatch가 dispatch receiver 타입에 대해 가상이지만 extension receiver 타입에 대해서는 static이라는 것을 의미합니다.

open class Base { }

class Derived : Base() { }

open class BaseCaller {
    open fun Base.printFunctionInfo() {
        println("Base extension function in BaseCaller")
    }

    open fun Derived.printFunctionInfo() {
        println("Derived extension function in BaseCaller")
    }

    fun call(b: Base) {
        b.printFunctionInfo()   // call the extension function
    }
}

class DerivedCaller: BaseCaller() {
    override fun Base.printFunctionInfo() {
        println("Base extension function in DerivedCaller")
    }

    override fun Derived.printFunctionInfo() {
        println("Derived extension function in DerivedCaller")
    }
}

fun main() {
    BaseCaller().call(Base())   // "Base extension function in BaseCaller"
    DerivedCaller().call(Base())  // "Base extension function in DerivedCaller" - dispatch receiver is resolved virtually
    DerivedCaller().call(Derived())  // "Base extension function in DerivedCaller" - extension receiver is resolved statically
}

Note on visibility

확장은 같은 범위에서 선언된 일반 함수와 동일한 visibility modifiers를 사용합니다. 예를 들면:

  • 파일의 최상위 수준에서 선언된 확장은 동일한 파일의 다른 최상위 private 선언에 액세스할 수 있습니다.
  • 수신자 타입 밖에서 선언된 확장은 수신자의 private 또는 protected 멤버에 액세스할 수 없습니다.

Extensions | Kotlin

Enum

Enum classes

enum 클래스의 가장 기본적인 사용 사례는 type-safe enums를 구현하는 것입니다:

각 enum 상수는 객체입니다.

enum 상수는 쉼표로 구분됩니다.

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

각 enum은 enum 클래스의 인스턴스이므로 다음과 같이 초기화할 수 있습니다:

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

익명 클래스

Enum 상수는 해당 메서드와 함께 기본 메서드를 오버라이드하는 자체 익명 클래스를 선언할 수 있습니다.

enum class ProtocolState {
    WAITING {
        override fun signal() = TALKING
    },

    TALKING {
        override fun signal() = WAITING
    };

    abstract fun signal(): ProtocolState
}

enum 클래스가 어떤 멤버를 정의하면 상수 정의와 멤버 정의를 세미콜론으로 구분합니다.

enum 클래스에서 인터페이스 구현

enum 클래스는 인터페이스를 구현할 수 있지만 (클래스에서 파생될 수는 없습니다), 모든 항목에 대한 인터페이스 멤버의 공통 구현을 제공하거나, 익명 클래스 내에서 각 항목에 대한 별도의 구현을 제공합니다. 이는 enum 클래스 선언에 구현하려는 인터페이스를 추가함으로써 수행됩니다:

enum class IntArithmetics : BinaryOperator<Int>, IntBinaryOperator {
    PLUS {
        override fun apply(t: Int, u: Int): Int = t + u
    },
    TIMES {
        override fun apply(t: Int, u: Int): Int = t * u
    };

    override fun applyAsInt(t: Int, u: Int) = apply(t, u)
}

모든 enum 클래스는 기본적으로 Comparable 인터페이스를 구현합니다. enum 클래스의 상수는 자연 순서로 정의됩니다. 더 자세한 정보는 Ordering을 참조하세요.

enum 상수 작업

코틀린의 enum 클래스는 정의된 enum 상수를 나열하고 이름으로 enum 상수를 가져오는 합성 메서드가 있습니다. 이 메서드들의 시그니처는 다음과 같습니다 (enum 클래스의 이름이 EnumClass라고 가정할 때):

EnumClass.valueOf(value: String): EnumClass
EnumClass.values(): Array<EnumClass>

이 메서드들이 동작하는 예는 다음과 같습니다:

enum class RGB { RED, GREEN, BLUE }

fun main() {
    for (color in RGB.values()) println(color.toString()) // prints RED, GREEN, BLUE
    println("The first color is: ${RGB.valueOf("RED")}") // prints "The first color is: RED"
}

valueOf() 메서드는 지정된 이름이 클래스에서 정의한 enum 상수와 일치하지 않을 경우 IllegalArgumentException을 던집니다.

코틀린 1.9.0에서 entries 속성이 values() 함수를 대체하는 것으로 도입되었습니다. entries 속성은 enum 상수의 미리 할당된 불변 리스트를 반환합니다. 이는 특히 컬렉션을 다룰 때 유용하며 성능 문제를 피하는 데 도움이 될 수 있습니다.

예를 들면:

enum class RGB { RED, GREEN, BLUE }

fun main() {
    for (color in RGB.entries) println(color.toString())
    // prints RED, GREEN, BLUE
}

모든 enum 상수에는 이름과 위치를 얻는 데 사용할 수 있는 nameordinal 속성이 있습니다. 위치는 enum 클래스 선언에서 시작하여 0부터 시작합니다:

println(RGB.RED.name) // prints RED
println(RGB.RED.ordinal) // prints 0

enumValues<T>()enumValueOf<T>() 함수를 사용하여 enum 클래스의 상수에 일반적인 방식으로 접근할 수 있습니다:

enum class RGB { RED, GREEN, BLUE }

inline fun <reified T : Enum<T>> printAllValues() {
    println(enumValues<T>().joinToString { it.name })
}

printAllValues<RGB>() // prints RED, GREEN, BLUE

인라인 함수와 실체화된 타입 매개변수에 대한 자세한 정보는 Inline functions를 참조하세요.

💡 코틀린 1.9.20에서 `enumEntries()`함수가 `enumValues()`함수를 대체할 미래의 함수로 도입되었습니다.

enumValues<T>() 함수는 여전히 지원되지만, enumEntries<T>() 함수를 사용하는 것이 좋습니다.

이는 enumValues<T>()를 호출할 때마다 새 배열이 생성되는 반면, enumEntries<T>()를 호출할 때마다 동일한 리스트가 반환되므로 훨씬 효율적이기 때문입니다.

예를 들면:

enum class RGB { RED, GREEN, BLUE }

@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T : Enum<T>> printAllValues() {
    println(enumEntries<T>().joinToString { it.name })
}

printAllValues<RGB>()
// RED, GREEN, BLUE

enumEntries<T>()함수는 실험적입니다. 이를 사용하려면 @OptIn(ExperimentalStdlibApi)를 사용하여 동의하고, 언어 버전을 최소 1.9로 설정해야 합니다.

Data class

Data Class

코틀린의 데이터 클래스는 주로 데이터를 보유하는 데 사용되는 클래스입니다. 데이터 클래스는 인스턴스를 읽기 쉬운 출력으로 출력하거나, 인스턴스를 비교하거나, 인스턴스를 복사하는 등의 추가 멤버 함수를 자동으로 제공합니다. 데이터 클래스는 data로 표시됩니다:

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

컴파일러는 data class일 때 아래와 같은 메소드를 자동으로 만들어준다.

  • .equals()
  • .hashCode()
    • 객체 그냥 출력하는거랑 toString 하는거랑 hash 값 다름!
    • 값 다르게 넣은 것 객체 두개의 hash값도 다르다!
  • .toString()
    • 객체 그냥 출력하는 거랑 toString 하는거랑 출력은 같음
  • .componentN()
    • 선언된 순서대로 속성을 가지고 옴
    • 데이터 클래스의 프로퍼티를 쉽게 가져오는 것을 돕는다. component1, component2 이런식으로 호출한다.
    • 이런식으로 파이썬식 다중할당? 편하다
      val (a,b) = listof(1,2)
      val (a, b, c) = A()
      auto [a, b] = pair(1, 2)
  • .copy()함수 (아래 참조)

생성된 코드의 일관성과 의미 있는 동작을 보장하기 위해, 데이터 클래스는 다음의 요구 사항을 충족해야 합니다:

  • 주 생성자는 최소한 하나의 매개변수를 가져야 합니다.
  • 모든 주 생성자 매개변수는 val 또는 var로 표시되어야 합니다.
  • 데이터 클래스는 abstract, open, sealed, 또는 inner class가 될 수 없습니다.

또한, 데이터 클래스 멤버의 생성은 멤버의 상속에 대한 이러한 규칙을 따릅니다:

  • 데이터 클래스 본문이나 상위 클래스에서 .equals(), .hashCode(), 또는 .toString()의 명시적 구현이 있는 경우, 이러한 함수들은 생성되지 않고, 기존 구현이 사용됩니다.
  • 상위 유형이 호환되는 타입을 반환하는 open .componentN() 함수를 가지고 있는 경우, 해당 함수들은 데이터 클래스에 대해 생성되고 상위 유형의 함수를 오버라이드합니다. 상위 유형의 함수를 오버라이드 할 수 없는 경우 (인식 불가능한 시그니처거나 final인 경우), 오류가 보고됩니다.
  • .componentN().copy() 함수에 대한 명시적인 구현을 제공하는 것은 허용되지 않습니다.

데이터 클래스는 다른 클래스를 확장할 수 있습니다 (예제는 Sealed class 참조).

JVM에서 생성된 클래스가 매개변수 없는 생성자를 가져야 하는 경우, 속성에 대한 기본 값이 지정되어야 합니다 (생성자 참조).

data class User(val name: String = "", val age: Int = 0)

클래스 본문에서 선언된 속성들

컴파일러는 자동으로 생성된 함수들 내에서 주 생성자 내부에 정의된 속성만 사용합니다. 생성된 구현에서 속성을 제외하려면, 클래스 본문 내부에서 그것을 선언합니다:

data class Person(val name: String) {
    var age: Int = 0
}

이 예제에서는 name 속성만이 .toString(), .equals(), .hashCode(), 그리고 .copy() 구현 내부에서 사용될 수 있으며, .component1() 함수만 존재합니다.

age 속성은 클래스 본문 내부에서 선언되었기 때문에 .toString(), .equals(), .hashCode(), 그리고 .copy() 구현 내부에서 사용할 수 없습니다.

만약 두 Person 객체가 다른 나이를 가지지만 같은 이름을 가진다면, 그들은 동일하게 취급됩니다. 이는 .equals() 함수가 name 속성의 동일성만 확인할 수 있기 때문입니다.

💡 data class에서 중요한 것은 파라미터로 들어가는 것들이다! 클래스 본문은 중요하지 않음!

복사하기

.copy() 함수를 사용하여 객체를 복사하고, 나머지는 그대로 유지하면서 일부 속성을 변경할 수 있습니다. 위의 User 클래스에 대한 이 함수의 구현은 다음과 같을 것입니다:

fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

그런 다음 다음과 같이 작성할 수 있습니다:

val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)

데이터 클래스와 구조 분해 선언

데이터 클래스에 대해 생성된 Component 함수는 구조 분해 선언에서 사용할 수 있습니다 (@유진 : 파이썬 스럽다..)

val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age")
// Jane, 35 years of age

표준 data class

표준 라이브러리는 Pair와 Triple 클래스를 제공합니다. 대부분의 경우, 그러나, 명명된 데이터 클래스가 더 나은 디자인 선택이 될 수 있습니다. 이는 속성에 대한 의미 있는 이름을 제공함으로써 코드를 더 쉽게 읽을 수 있게 만들기 때문입니다.

profile
안드로이드 학생 개발자 에디 / 유진입니다
post-custom-banner

0개의 댓글