본문 바로가기
Kotlin/What's new

[Kotlin 번역] What's new in Kotlin 1.1

by 노력남자 2023. 9. 12.
반응형

2016년 2월 15일

 

Table of contents

 

  • 코루틴
  • 다른 언어 기능
  • 표준 라이브러리
  • JVM 백엔드
  • JavaScript 백엔드


JavaScript


Kotlin 1.1부터 JavaScript 대상은 더 이상 실험적으로 간주되지 않습니다. 모든 언어 기능이 지원되며, 프론트엔드 개발 환경과의 통합을 위한 많은 새로운 도구가 있습니다. 아래에서 자세한 변경 목록을 확인하세요.

 

코루틴 (실험 중)


Kotlin 1.1의 주요 새로운 기능은 코루틴(coroutines)입니다. 코루틴은 async/await, yield 및 유사한 프로그래밍 패턴을 지원합니다. Kotlin의 설계 핵심은 코루틴 실행의 구현이 언어가 아닌 라이브러리의 일부로 되어 있으므로 특정한 프로그래밍 패러다임이나 동시성 라이브러리에 얽매이지 않는다는 것입니다.

코루틴은 사실상 중단 및 재개할 수 있는 경량 스레드입니다. 코루틴은 중단 함수를 통해 지원되며, 이러한 함수 호출은 코루틴을 중단시킬 수 있으며, 일반적으로 익명 중단 함수(중단 람다)를 사용하여 새로운 코루틴을 시작합니다.

외부 라이브러리인 kotlinx.coroutines에서 구현된 async/await를 살펴보겠습니다.

 

// 백그라운드 쓰레드 풀에서 돌린 코드
fun asyncOverlay() = async(CommonPool) {
    // 두 개의 비동기 작업 시작
    val original = asyncLoadImage("original")
    val overlay = asyncLoadImage("overlay")
    // 그리고 나서 두 결과에 오버레이 적용
    applyOverlay(original.await(), overlay.await())
}

// UI 컨텍스트에서 새로운 코루틴을 시작합니다.
launch(UI) {
    // 비동기 오버레이가 완료될 때까지 기다립니다.
    val image = asyncOverlay().await()
    // 그리고 나서 UI에서 이미지를 표시합니다.
    showImage(image)
}

 

여기서 async { ... }는 코루틴을 시작하며, await()를 사용하면 기다리는 동안 코루틴의 실행이 중단되고 기다리는 작업이 완료되면 (가능하면 다른 스레드에서) 다시 시작됩니다.

표준 라이브러리는 yield 및 yieldAll 함수를 사용하여 게으르게 생성된 시퀀스를 지원하기 위해 코루틴을 사용합니다. 이러한 시퀀스에서는 시퀀스 요소를 반환하는 코드 블록이 각 요소를 가져온 후 중단되고 다음 요소가 요청될 때 다시 시작됩니다. 다음은 예제입니다:

 

import kotlin.coroutines.experimental.*

fun main(args: Array<String>) {
    val seq = buildSequence {
        for (i in 1..5) {
            // i의 제곱 값을 yield합니다.
            yield(i * i)
        }
        // 범위를 yield합니다.
        yieldAll(26..28)
    }

    // 시퀀스를 출력합니다.
    println(seq.toList())
}

 


위의 코드를 실행하여 결과를 확인할 수 있습니다. 필요에 따라 편집하고 다시 실행하십시오!

더 많은 정보는 코루틴 문서와 튜토리얼을 참조하십시오.

코루틴은 현재 실험적인 기능으로 간주되므로 Kotlin 팀은 최종 1.1 릴리스 이후에도 이 기능의 역방향 호환성을 지원하기로 확약하지 않는다는 점을 유의하십시오.

 

다른 언어 기능


타입 별칭


타입 별칭을 사용하면 기존 타입에 대한 대체 이름을 정의할 수 있습니다. 이것은 주로 컬렉션과 같은 제네릭 타입 및 함수 타입에 유용합니다. 다음은 예시입니다:

 

typealias OscarWinners = Map<String, String>

fun countLaLaLand(oscarWinners: OscarWinners) =
    oscarWinners.count { it.value.contains("La La Land") }

// 주의: 타입 이름 (원래 이름 및 타입 별칭)은 서로 교환 가능합니다.
fun checkLaLaLandIsTheBestMovie(oscarWinners: Map<String, String>) =
    oscarWinners["Best picture"] == "La La Land"

 

자세한 내용은 타입 별칭 문서KEEP을 참고하세요.


바인딩된 호출 참조


이제 :: 연산자를 사용하여 특정 객체 인스턴스의 메서드 또는 프로퍼티를 가리키는 멤버 참조를 얻을 수 있습니다. 이전에는 람다로만 표현할 수 있었습니다. 다음은 예시입니다:

val numberRegex = "\\d+".toRegex()
val numbers = listOf("abc", "123", "456").filter(numberRegex::matches)

 

자세한 내용은 reflection 문서KEEP을 참고하세요.

 

Sealed 및 데이터 클래스


Kotlin 1.1은 Kotlin 1.0에서 존재한 일부 제한을 제거하여 sealed 및 데이터 클래스의 사용을 향상시켰습니다. 이제 최상위 sealed 클래스의 하위 클래스를 동일한 파일의 최상위로 정의할 수 있으며 sealed 클래스의 중첩 클래스로만 정의할 필요가 없습니다. 데이터 클래스는 이제 다른 클래스를 확장할 수 있습니다. 이를 통해 식 클래스의 계층 구조를 깔끔하게 정의할 수 있습니다:

 

sealed class Expr

data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

fun eval(expr: Expr): Double = when (expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
}

val e = eval(Sum(Const(1.0), Const(2.0)))

 

자세한 내용은 sealed classes 문서 및 KEEP의 sealed classdata class를 참고바랍니다.


람다 내에서 해체


이제 람다로 전달된 인수를 언패킹하기 위해 해체 선언 구문을 사용할 수 있습니다. 다음은 예시입니다:

 

val map = mapOf(1 to "one", 2 to "two")

// 이전
println(map.mapValues { entry ->
    val (key, value) = entry
    "$key -> $value!"
})

// 현재
println(map.mapValues { (key, value) -> "$key -> $value!" })

 

자세한 내용은 해체 선언 문서KEEP을 참고바랍니다.

 

사용되지 않는 매개변수에 대한 밑줄


여러 매개변수를 갖는 람다의 경우 사용하지 않는 매개변수의 이름을 대체로 _ 문자로 사용할 수 있습니다:

 

map.forEach { _, value -> println("$value!") }


이것은 해체 선언에서도 동작합니다:

 

val (_, status) = getResult()

 

자세한 내용은 KEEP을 참고바랍니다.


숫자 리터럴에서 밑줄 사용


Java 8과 마찬가지로 Kotlin에서는 숫자 리터럴에서 숫자 그룹을 구분하기 위해 밑줄을 사용할 수 있습니다:

 

val oneMillion = 1_000_000
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010

 

 

자세한 내용은 KEEP을 참고바랍니다.

 

프로퍼티에 대한 더 짧은 구문


getter가 표현 본문으로 정의된 프로퍼티의 경우 이제 프로퍼티 타입을 생략할 수 있습니다:

 

data class Person(val name: String, val age: Int) {
    val isAdult get() = age >= 20 // 프로퍼티 타입은 'Boolean'으로 추론됩니다.
}


인라인 프로퍼티 접근자


백킹 필드가 없는 프로퍼티 접근자에 inline 수정자를 지정할 수 있습니다. 이러한 접근자는 인라인 함수와 동일한 방식으로 컴파일됩니다.

 

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


또한 프로퍼티 전체를 인라인으로 표시할 수도 있으며, 그런 경우 수정자가 두 접근자에 모두 적용됩니다.

 

자세한 내용은 inline 함수 문서KEEP을 참고바랍니다.

 

지역 위임 프로퍼티


이제 지역 변수와 함께 위임된 프로퍼티 구문을 사용할 수 있습니다. 이를 사용하여 지연 평가되는 지역 변수를 정의하는 것이 가능합니다:

 

val answer by lazy {
    println("정답 계산 중...")
    42
}
if (needAnswer()) {                     
    println("답은 $answer입니다.")   
} else {
    println("가끔 답이 없는 것이 답입니다...")
}

 

자세한 내용은 KEEP을 참고바랍니다.

 

위임된 속성 바인딩의 가로채기


위임된 프로퍼티 바인딩 중 위임 프로퍼티 바인딩을 가로채는 것이 가능합니다. 예를 들어, 속성 이름을 바인딩하기 전에 확인하려면 다음과 같이 작성할 수 있습니다:

 

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(thisRef: MyUI, prop: KProperty<*>): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        ... // 속성 생성
    }

    private fun checkProperty(thisRef: MyUI, name: String) { ... }
}

fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

class MyUI {
    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}


provideDelegate 메서드는 MyUI 인스턴스를 만들 때마다 각 속성에 대해 호출되며 필요한 검증을 즉시 수행할 수 있습니다.

 

자세한 내용은 위임된 속성 문서를 참고바랍니다.


일반적인 열거형 값 액세스


이제 열거형 클래스의 값을 일반적인 방식으로 나열할 수 있습니다.

 

enum class RGB { RED, GREEN, BLUE }

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


DSL에서 암시적 수신자의 범위 제어


@DslMarker 주석을 사용하여 DSL 컨텍스트에서 외부 범위의 수신자 사용을 제한할 수 있습니다. 전형적인 HTML 빌더 예제를 고려해보세요:

 

table {
    tr {
        td { + "Text" }
    }
}


Kotlin 1.0에서 td에 전달된 람다 내의 코드는 세 개의 암시적 수신자에 액세스합니다. 이것은 컨텍스트에서 의미가 없는 메서드를 호출할 수 있게 합니다. 예를 들어 td 내에서 tr을 호출하고 <td> 태그에 <tr> 태그를 넣을 수 있습니다.

Kotlin 1.1에서는 td에 전달된 람다 내에서 td의 암시적 수신자에 정의된 메서드만 사용할 수 있도록 제한할 수 있습니다. 이를 위해 @DslMarker 메타 주석이 지정된 주석을 정의하고 해당 주석을 태그 클래스의 기본 클래스에 적용합니다.

 

자세한 내용은 안전한 타입 빌더 문서KEEP을 참고바랍니다.

 

rem 연산자


mod 연산자는 현재 사용이 중단되었으며, 대신 rem이 사용됩니다. 동기를 보려면 이 이슈를 참조하십시오.

 

표준 라이브러리


문자열에서 숫자로의 변환


String 클래스에는 이제 문자열을 숫자로 변환하는 데 사용할 수 있는 새로운 확장 함수가 있습니다. 이 함수들은 잘못된 숫자에 대한 예외를 throw하지 않고 작동합니다. 예를들어, `String.toIntOrNull(): Int?`, `String.toDoubleOrNull(): Double?` 등이 있습니다.

 

val port = System.getenv("PORT")?.toIntOrNull() ?: 80


또한, Int.toString(), String.toInt(), String.toIntOrNull()과 같은 정수 변환 함수도 radix 매개변수를 사용할 수 있는 오버로드를 제공합니다. 이를 통해 변환의 기본(base)을 지정할 수 있습니다(2부터 36까지).


onEach()


onEach는 컬렉션과 시퀀스에 사용할 수 있는 작은 확장 함수로, 컬렉션 또는 시퀀스의 각 요소에 대해 일련의 작업을 수행할 수 있습니다. 반복 가능한(iterable) 객체에서는 forEach와 유사하게 동작하지만 반복 가능한 객체 자체를 반환합니다. 시퀀스에서는 요소가 반복되는 동안 주어진 작업을 지연 실행하는 래핑 시퀀스를 반환합니다.

 

inputDir.walk()
        .filter { it.isFile && it.name.endsWith(".txt") }
        .onEach { println("Moving $it to $outputDir") }
        .forEach { moveFile(it, File(outputDir, it.toRelativeString(inputDir))) }


also(), takeIf(), 그리고 takeUnless()


이 세 가지 일반적인 용도의 확장 함수는 모든 수신자(receiver)에 적용할 수 있습니다.

also는 apply와 유사합니다. 수신자를 가져와서 그에 대한 작업을 수행한 다음 해당 수신자를 반환합니다. 차이점은 apply 내부의 블록에서 수신자는 this로 사용할 수 있고, also 내부의 블록에서는 it으로 사용할 수 있으며 원한다면 다른 이름을 부여할 수 있다는 것입니다. 이것은 외부 범위의 this를 가려야 할 때 유용합니다.

 

fun Block.copy() = Block().also {
    it.content = this.content
}


takeIf는 단일 값에 대한 필터와 유사합니다. 수신자를 주어진 조건과 비교하고 조건을 만족하면 수신자를 반환하고 그렇지 않으면 null을 반환합니다. 엘비스 연산자(?:)와 조기 반환을 사용하여 다음과 같은 구조를 작성할 수 있게 합니다:

val outDirFile = File(outputDir.path).takeIf { it.exists() } ?: return false
// 기존의 outDirFile을 처리하는 작업 수행

val index = input.indexOf(keyword).takeIf { it >= 0 } ?: error("keyword not found")
// 입력 문자열에서 키워드의 인덱스를 찾은 경우 처리 작업 수행


takeUnless는 takeIf와 동일하지만 반대의 조건을 사용합니다. 조건을 만족하지 않으면 수신자를 반환하고, 그렇지 않으면 null을 반환합니다. 따라서 위의 예제 중 하나는 takeUnless를 사용하여 다음과 같이 다시 작성할 수 있습니다:

val index = input.indexOf(keyword).takeUnless { it < 0 } ?: error("keyword not found")


람다 대신 호출 가능한 참조를 사용하는 경우 편리합니다.

val result = string.takeUnless(String::isEmpty)


groupingBy()


이 API는 컬렉션을 키(key)로 그룹화하고 각 그룹을 동시에 접어(fold) 줄 수 있는 데 사용할 수 있습니다. 예를 들어, 각 문자로 시작하는 단어의 수를 세는 데 사용할 수 있습니다:

val frequencies = words.groupingBy { it.first() }.eachCount()


Map.toMap()와 Map.toMutableMap()


이러한 함수는 맵을 쉽게 복사할 때 사용할 수 있습니다.

class ImmutablePropertyBag(map: Map<String, Any>) {
    private val mapCopy = map.toMap()
}


Map.minus(key)


연산자 plus는 읽기 전용 맵에 키-값 쌍을 추가하는 방법을 제공합니다. 새로운 맵을 생성합니다. 그러나 맵에서 키를 제거하는 간단한 방법이 없었습니다. 이제 연산자 minus가 이 빈 곳을 채웁니다. 

4개의 오버로드가 사용 가능합니다. 단일 키, 키 컬렉션, 키 시퀀스 및 키 배열을 제거하기 위한 오버로드가 있습니다.

val map = mapOf("key" to 42)
val emptyMap = map - "key"


minOf()와 maxOf()


이러한 함수는 주어진 값 중 가장 낮은 값과 가장 큰 값을 찾는 데 사용할 수 있습니다. 값은 기본 숫자나 비교 가능한 객체여야 합니다. 객체를 비교할 때 Comparator 인스턴스를 추가로 전달하려면 각 함수에 대한 추가 오버로드도 있습니다.

val list1 = listOf("a", "b")
val list2 = listOf("x", "y", "z")
val minSize = minOf(list1.size, list2.size)
val longestList = maxOf(list1, list2, compareBy { it.size })


배열과 유사한 List 인스턴스 생성 함수


Array 생성자와 유사한 함수가 이제 List 및 MutableList 인스턴스를 생성하고 각 요소를 람다를 호출하여 초기화할 수 있습니다.

val squares = List(10) { index -> index * index }
val mutable = MutableList(10) { 0 }


Map.getValue()


Map에 대한 이 확장 함수는 주어진 키에 해당하는 기존 값 또는 키를 찾지 못했음을 나타내는 예외를 throw합니다. 맵이 withDefault로 생성된 경우 이 함수는 예외를 throw하는 대신 기본값을 반환합니다.

val map = mapOf("key" to 42)
// 42를 반환합니다.
val value: Int = map.getValue("key")

val mapWithDefault = map.withDefault { k -> k.length }
// 4를 반환합니다.
val value2 = mapWithDefault.getValue("key2")

// map.getValue("anotherKey") // <- 이것은 NoSuchElementException을 throw합니다.


추상 컬렉션


이러한 추상 클래스는 Kotlin 컬렉션 클래스를 구현할 때 기본 클래스로 사용할 수 있습니다. 읽기 전용 컬렉션을 구현하는 경우 AbstractCollection, AbstractList, AbstractSet 및 AbstractMap이 있으며, 가변 컬렉션을 구현하는 경우 AbstractMutableCollection, AbstractMutableList, AbstractMutableSet 및 AbstractMutableMap이 있습니다. JVM에서는 이러한 추상 가변 컬렉션은 대부분 JDK의 추상 컬렉션에서 상속받습니다.


배열 조작 함수


표준 라이브러리는 이제 배열에 대한 요소별 작업을 수행하는 일련의 함수를 제공합니다. 비교 (contentEquals 및 contentDeepEquals), 해시 코드 계산 (contentHashCode 및 contentDeepHashCode), 그리고 문자열로 변환 (contentToString 및 contentDeepToString)을 위한 함수들입니다. 이러한 함수들은 JVM과 JS 모두에서 지원되며, JVM에서는 java.util.Arrays의 해당 함수에 별칭으로 작동합니다.

val array = arrayOf("a", "b", "c")
println(array.toString())  // JVM 구현: 형식 및 해시 값
println(array.contentToString())  // 목록으로 깔끔하게 포맷

 

JVM 백엔드


자바 8 바이트코드 지원


Kotlin은 이제 자바 8 바이트코드를 생성하는 옵션을 가지고 있습니다 (-jvm-target 1.8 명령 줄 옵션 또는 Ant/Maven/Gradle에서 해당 옵션). 현재로서는 바이트코드의 의미를 변경하지 않습니다 (특히, 인터페이스의 기본 메서드와 람다는 Kotlin 1.0과 정확히 동일하게 생성됩니다), 하지만 나중에 이를 더 활용할 계획입니다.


자바 8 표준 라이브러리 지원


자바 7과 8에서 추가된 새로운 JDK API를 지원하는 표준 라이브러리의 별도 버전이 있습니다. 새로운 API에 액세스해야 하는 경우 표준 kotlin-stdlib 대신 kotlin-stdlib-jre7 및 kotlin-stdlib-jre8 메이븐 아티팩트를 사용하십시오. 이러한 아티팩트는 kotlin-stdlib 위에 작은 확장 기능을 제공하며 이를 프로젝트에 전이적 종속성으로 가져옵니다.


바이트코드에 매개변수 이름 저장


Kotlin은 이제 바이트코드에 매개변수 이름을 저장하는 기능을 지원합니다. 이 기능은 -java-parameters 명령 줄 옵션을 사용하여 활성화할 수 있습니다.


상수 인라인


컴파일러는 이제 const val 속성의 값을 사용되는 위치에 인라인합니다.


가변 클로저 변수


람다에서 가변 클로저 변수를 캡처하기 위해 사용되는 상자 클래스에는 더 이상 volatile 필드가 없습니다. 이 변경 사항은 성능을 향상시키지만 일부 특수한 사용 시나리오에서 새로운 경합 조건을 발생시킬 수 있습니다. 이에 영향을 받는 경우 변수에 액세스하기 위한 자체 동기화를 제공해야 합니다.


javax.script 지원


Kotlin은 이제 javax.script API (JSR-223)와 통합됩니다. 이 API를 사용하면 런타임에서 코드 스니펫을 평가할 수 있습니다:

 

val engine = ScriptEngineManager().getEngineByExtension("kts")!!
engine.eval("val x = 3")
println(engine.eval("x + 2")) // 5를 출력합니다.


kotlin.reflect.full


Java 9 지원을 위해 kotlin-reflect.jar 라이브러리에 있는 확장 함수와 속성이 kotlin.reflect.full 패키지로 이동되었습니다. 이전 패키지의 이름 (kotlin.reflect)은 폐기되며 Kotlin 1.2에서 제거될 예정입니다. 중요한 점은 핵심 리플렉션 인터페이스 (예: KClass)가 kotlin-reflect이 아니라 Kotlin 표준 라이브러리의 일부이며 이동에 영향을 받지 않습니다.

 

자바스크립트 백엔드


통합 표준 라이브러리


Kotlin 표준 라이브러리의 큰 부분이 이제 JavaScript로 컴파일된 코드에서 사용할 수 있습니다. 특히, 컬렉션 (ArrayList, HashMap 등), 예외 (IllegalArgumentException 등) 및 StringBuilder, Comparator와 같은 주요 클래스들은 이제 kotlin 패키지 아래에서 정의됩니다. JVM에서는 이름이 해당 JDK 클래스의 타입 별칭이며, JS에서는 이러한 클래스들이 Kotlin 표준 라이브러리에 구현되어 있습니다.


더 나은 코드 생성


JavaScript 백엔드는 이제 정적으로 검사 가능한 코드를 더 많이 생성하며, 이는 minifier, optimizer, linter 등과 같은 JS 코드 처리 도구와 더 친화적입니다.


external 수식어


JavaScript에서 Kotlin으로 구현된 클래스에 안전한 방식으로 액세스해야 할 경우 external 수식어를 사용하여 Kotlin 선언을 작성할 수 있습니다. (Kotlin 1.0에서는 @native 주석이 대신 사용되었습니다.) JVM 대상과 달리 JS 대상에서는 클래스 및 속성에 external 수식어를 사용할 수 있습니다. 예를 들어 DOM Node 클래스를 선언하는 방법은 다음과 같습니다:

 

external class Node {
    val firstChild: Node

    fun appendChild(child: Node): Node

    fun removeChild(child: Node): Node

    // 기타
}

 

개선된 import 처리


이제 JavaScript 모듈에서 가져와야 하는 선언을 더 정확하게 설명할 수 있습니다. external 선언에 @JsModule("<모듈-이름>") 주석을 추가하면 컴파일 중에 모듈 시스템 (CommonJS 또는 AMD)에 올바르게 가져올 것입니다. 예를 들어, CommonJS에서는 선언이 require(...) 함수를 통해 가져올 것입니다. 또한 선언을 모듈로 가져올지 아니면 전역 JavaScript 객체로 가져올지를 결정하려면 @JsNonModule 주석을 사용할 수 있습니다.

예를 들어, Kotlin 모듈에 JQuery를 가져오는 방법은 다음과 같습니다:

 

external interface JQuery {
    fun toggle(duration: Int = definedExternally): JQuery
    fun click(handler: (Event) -> Unit): JQuery
}

@JsModule("jquery")
@JsNonModule
@JsName("$")
external fun jquery(selector: String): JQuery


이 경우, JQuery는 jquery라는 모듈로 가져오게 될 것입니다. 또는 Kotlin 컴파일러가 구성된 모듈 시스템에 따라 $-객체로 사용될 수도 있습니다.

이러한 선언을 다음과 같이 애플리케이션에서 사용할 수 있습니다:

fun main(args: Array<String>) {
    jquery(".toggle-button").click {
        jquery(".toggle-panel").toggle(300)
    }
}

 

원문

 

https://kotlinlang.org/docs/whatsnew11.html

반응형