코틀린은 클래스나 인터페이스를 상속하거나 Decorator와 같은 디자인 패턴을 사용하지 않고도 새로운 기능을 추가할 수 있는 방법을 제공합니다. 이를 확장(Extensions)이라고 부릅니다.
예를 들어, 수정할 수 없는 서드파티 라이브러리의 클래스나 인터페이스에 새로운 함수를 작성할 수 있습니다. 이러한 함수는 원래 클래스의 메소드처럼 평소처럼 호출할 수 있습니다. 이 메커니즘을 확장 함수라고 부릅니다. 또한, 기존 클래스에 새로운 속성을 정의할 수 있는 확장 속성도 있습니다.
➕해당 클래스의 밖에서 새로운 함수를 선언!
사용하는 것은 일반 함수랑 같이 쓰면 됨
확장 함수를 선언하려면 이름 앞에 수신자 타입을 붙여야 하는데, 이는 확장되는 타입을 가리킵니다. 예를 들면, 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
}
수신자 타입 표현식에서 제네릭 타입 매개변수를 사용하려면 함수 이름 앞에 제네릭 타입 매개변수를 선언해야 합니다.
확장은 실제로 확장하는 클래스를 수정하지 않습니다. 확장을 정의함으로써 클래스에 새로운 멤버를 삽입하는 것이 아니라, 이 타입의 변수에 대해 새로운 함수를 점 표기법으로 호출 가능하게 만드는 것입니다.
확장 함수는 정적으로 디스패치됩니다. 즉, 어떤 확장 함수가 호출되는지는 컴파일 시간에 수신자 타입에 기반하여 이미 알려져 있습니다.
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)
확장은 널 수신자 타입으로 정의될 수 있습니다. 이러한 확장은 객체 변수가 널인 경우에도 호출할 수 있습니다. 만약 수신자가 널이라면, this
는 널입니다.
따라서 널 수신자 타입으로 확장을 정의할 때는 함수 본문 내에서 this == null
검사를 수행하는 것이 좋습니다.
코틀린은 확장 속성
을 지원합니다. 이는 확장 함수를 지원하는 것과 유사하게 작동합니다.
val <T> List<T>.lastIndex: Int
get() = size - 1
하지만 확장은 실제로 클래스에 멤버를 삽입하지 않으므로, 확장 속성이 백업 필드를 가질 효율적인 방법이 없습니다. 그래서 initializers는 확장 속성에 허용되지 않습니다. 이들의 동작은 getter/setter
를 명시적으로 제공함으로써만 정의될 수 있습니다.
val House.number = 1 // error: initializers are not allowed for extension properties
클래스에 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()
}
확장은 대부분 패키지 바로 아래의 최상위 수준에서 정의합니다.
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 만 가능
확장을 멤버로 선언할 수도 있습니다.
이런 확장 내부에는 여러 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
}
확장은 같은 범위에서 선언된 일반 함수와 동일한 visibility modifiers를 사용합니다. 예를 들면:
private
선언에 액세스할 수 있습니다.private
또는 protected
멤버에 액세스할 수 없습니다.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 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 클래스의 이름이 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 상수에는 이름과 위치를 얻는 데 사용할 수 있는 name
과 ordinal
속성이 있습니다. 위치는 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
로 표시됩니다:
data class User(val name: String, val age: Int)
컴파일러는 data class일 때 아래와 같은 메소드를 자동으로 만들어준다.
.equals()
.hashCode()
.toString()
.componentN()
val (a, b, c) = A()
auto [a, b] = pair(1, 2)
.copy()
함수 (아래 참조)생성된 코드의 일관성과 의미 있는 동작을 보장하기 위해, 데이터 클래스는 다음의 요구 사항을 충족해야 합니다:
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
표준 라이브러리는 Pair와 Triple 클래스를 제공합니다. 대부분의 경우, 그러나, 명명된 데이터 클래스가 더 나은 디자인 선택이 될 수 있습니다. 이는 속성에 대한 의미 있는 이름을 제공함으로써 코드를 더 쉽게 읽을 수 있게 만들기 때문입니다.