2022년 2월 5일 토요일

[코틀린] 1.6 변경사항

 

언어 문법

when절에서의 sealed

sealed 클래스를 이용한 when 절에서 sealed 클래스의 모든 서브클래스를 철저하게 확인하여 컴파일타임에 경고로 알려주는 기능이 정식으로 지원됩니다. 이러한 문법은 대수적(algebraic) 자료형으로 도메인을 설계해야 할 때 유용합니다. 예를들어, 연락 히스토리를 표현하는 모델을 다음과 같이 sealed 클래스 구조로 표현했다고 가정하겠습니다.

sealed class Contact {
   data class PhoneCall(val number: String) : Contact()
   data class TextMessage(val number: String) : Contact()
   data class InstantMessage(val type: IMType, val user: String) : Contact()
}

만약, 각기 다른 연락 히스토리 타입마다 다른 결과를 반환하는 코드를 작성한다면, 컴파일러는 개발자가 모든 타입을 확인하지 못한 경우에 에러로 표시해 줄 것입니다.

fun Rates.computeMessageCost(contact: Contact): Cost =
   when (contact) { // ERROR: 'when' expression must be exhaustive
       is Contact.PhoneCall -> phoneCallCost
       is Contact.TextMessage -> textMessageCost
   }

이는 개발자가 잊어버리고 확인하지 못할법한 실수를 방지하도록 도와줍니다. 1.6에서는 컴파일 수행시 Non-exhaustive 'when' statements on sealed class/interface will be prohibited in 1.7. Add an 'is InstantMessage' branch or 'else' branch instead. 라는 경고를 보여주고, 1.7부터는 아예 컴파일타임에 에러가 발생하도록 변경될 예정입니다.

슈퍼타입으로의 Suspending functions

기존 코틀린에서는 다음과 같이 함수를 클래스의 상위타입으로 지정할 수 있었습니다.

class MyCompletionHandler : () -> Unit {
   override fun invoke() { doSomething() }
}

코틀린 1.6부터는 suspend 함수도 클래스의 상위타입으로 지정할 수 있습니다.

class MyClickAction : suspend () -> Unit {
   override suspend fun invoke() { doSomething() }
}

suspend 변환

코틀린 1.6에서는 일반 함수 타입을 suspend 함수 타입으로 자동으로 변환하도록 지원합니다. 이제 suspend 함수타입의 인자를 넘겨야 하는 부분에서 일반 함수타입의 구문을 넘겨도 컴파일러가 자동으로 인지하여 바꿔줍니다. 이는 큰 변경사항은 아니지만, 일반 함수타입과 suspend 함수타입 사이의 성가신 불일치를 해결한 수정입니다.

예를들어 Flow.collect() 함수는 suspend 함수타입을 인자로 받는데, 1.6부터는 일반 함수타입을 인자로 넘길 수 있습니다.

fun processItem(item: Item) { /* ... */ }

flow.collect { processItem(it) }
flow.collect(::processItem)

재귀적 제네릭 타입에서의 타입 추론 개선

1.6부터 코틀린 컴파일러는 기본적으로 재귀적인 제네릭이라면, 타입 상한에 해당하는 타입파라미터를 기반으로 타입 매개변수를 유추할 수 있습니다. 이는 자바에서 빌더 API를 만들 때 종종 사용하는 것처럼, 재귀적인 제네릭을 이용한 다양한 패턴의 코드를 작성할 수 있도록 도와줍니다.

// Before 1.5.30
val containerA = PostgreSQLContainer<nothing>(DockerImageName.parse("postgres:13-alpine")).apply {
    withDatabaseName("db")
    withUsername("user")
    withPassword("password")
    withInitScript("sql/schema.sql")
}

// With compiler option in 1.5.30 or by default starting with 1.6.0
val containerB = PostgreSQLContainer(DockerImageName.parse("postgres:13-alpine"))
    .withDatabaseName("db")
    .withUsername("user")
    .withPassword("password")
    .withInitScript("sql/schema.sql")

Note 재귀적 제네릭 타입은 타입상한을 이용하여 해당 클래스/인터페이스로 제한하는 방법

interface Animal<T: Animal<T>> {
    val name: String
    fun compareName(other: T): Boolean {
        return name == other.name
    }
}

data class Dog(override val name: String): Animal<Dog>
data class Cat(override val name: String): Animal<Cat>

Builder inference 개선

코틀린 1.5.30에서 -Xunrestricted-builder-inference라는 컴파일러 옵션이 도입되었습니다. 이 옵션을 통해 빌더 호출에 대한 타입 정보를 빌더 람다의 내부로 가져오는게 가능해지도록 합니다. 이름 그대로, buildList() 내부에서 get()처럼 아직 유추되지 않은 유형의 인스턴스를 반환하는 호출을 만드는 기능을 제공하는 것입니다.

코틀린 1.6부터는 더이상 -Xunrestricted-builder-inference 컴파일 옵션을 명시할 필요가 없습니다. -Xenable-builder-inference 컴파일 옵션을 이용하면, @BuilderInterface 어노테이션을 사용하지 않고도 제네릭 빌더를 작성할 수 있고, 일반 타입 추론이 타입의 정보를 해결할 수 없다면 빌더 추론을 자동으로 활성화 할 수 있습니다.

표준 라이브러리

표준 입력을 위한 새로운 함수

새로 코틀린을 접하는 이들에게 더 나은 경험을 제공하기 위해, 코틀린 1.6에서는 표준입력으로 라인을 읽은 후에 !! 연산자로 null이 아님을 명시해주어야 했던 필요를 삭제했습니다. 다음과 같이 동작하는 새로운 함수를 도입했습니다.

  • readIn()은 EOF에 도달하면 예외를 발생시킵니다. readLine() 함수로 매 라인을 null체크하는 대신 readIn() 함수를 사용하길 권장합니다.
  • 기존의 readLine()함수와 동일한 동작을 하는 readInOrNull() 함수를 추가했습니다.

readln 함수의 네이밍 컨벤션은 println 으로부터 가져와서, 코틀린을 처음 접하는 이들도 친숙할 수 있습니다. 이 함수는 JVM, Native에서 사용가능합니다.

fun main() {
    println("Input two integer numbers each on a separate line")
    val num1 = readln().toInt()
    val num2 = readln().toInt()
    println("The sum of $num1 and $num2 is ${num1 + num2}")
}

Duration API 안정화

많은 분들의 피드백 덕분에 Duration API는 드디어 안정화되어 배포되었습니다. 1.5.30에서는 Duration.toString() 의 가독성을 향상시키고, 문자열로부터 Duration을 파싱하는 새로운 함수를 도입했었고, 이번 1.6에서의 변경사항은 다음과 같습니다.

  • toComponents() 함수의 days 인자의 타입이 Int에서 Long으로 변경되었습니다.
  • DurationUtil enum은 이제 타입엘리어스가 아닙니다. JVM에서 java.util.concurrent.TimeUnit을 위해 타입엘리어스로 사용한 경우는 없습니다.
  • Int.seconds 같은 확장 프로퍼티들을 다시 가져왔습니다. 이 프로퍼티의 적용을 제한하기 위해, Duration의 컴패니언 클래스에서만 사용가능합니다.

typeOf() 안정화

코틀린 1.6에서 typeOf() 함수가 안정화 되었습니다. 이 함수는 1.3.40에서부터 JVM환경에서 실험적 API로 사용가능했고, 1.6부터는 JVM뿐 아니라 어떤 코틀린 플랫폼에서도 사용가능하며, 플랫폼의 컴파일러에서 유추가능한 코틀린의 타입을 KType으로 반환합니다.

Collection 빌더 안정화

코틀린 1.6부터 buildMap()buildList()buildSet()이 안정화되었습니다. 빌더로부터 반환되는 Collection은 읽기전용 상태에서 serializable입니다.

정수의 bit 회전연산 안정화

코틀린 1.6에서 rotateLeft()rotateRight() 함수가 안정화되었습니다.

  • rotateLeft : 인자로 주어진 카운트만큼 bit를 왼쪽으로 shift연산하고, 왼쪽에서 넘치는 bit를 오른쪽으로 추가함
  • rotateRight : 인자로 주어진 카운트만큼 bit를 오른쪽으로 shift연산하고, 오른쪽에서 넘치는 bit를 왼쪽으로 추가함
val number: Short = 0b10001
println(number.rotateRight(2).toString(radix = 2)) // 100000000000100
println(number.rotateLeft(2).toString(radix = 2))  // 1000100

문자열을 시퀀스로 변환하는 정규식 함수 안정화

코틀린 1.6에서 문자열을 시퀀스로 변환해주는 splitToSequence() 함수가 안정화되었습니다.

val colorsText = "green, red , brown&blue, orange, pink&green"
val regex = "[,\\s]+".toRegex()
val mixedColor = regex.splitToSequence(colorsText)
    .onEach { println(it) }
    .firstOrNull { it.contains('&') }
println(mixedColor) // "brown&blue"

compareTo 함수에 중위연산(infix) 키워드 추가

Comparable.compareTo 함수에 infix 키워드를 추가하여 중위연산자로 사용할 수 있게 되었습니다.

class WrappedText(val text: String) : Comparable<WrappedText> {
    override fun compareTo(other: WrappedText): Int =
        this.text compareTo other.text
}

JVM, JS에서의 replace(), replaceFirst()의 동작 일원화

코틀린 1.6 전에는 group 레퍼런스를 포함한 문자열의 경우에 replace()와 replaceFirst() 정규식 함수가 JVM과 JS환경에서 서로 다르게 동작했었습니다. 1.6부터는 JS환경의 결과가 JVM과 동일해지도록 수정했습니다.

원문

https://blog.jetbrains.com/kotlin/2021/11/kotlin-1-6-0-is-released/

[코틀린] 1.5 변경사항

 

언어 문법

JVM records 지원

자바는 빠르게 발전하고 있고, 코틀린은 그 변화를 반영해주는 방향으로 함께 발전하고 있습니다. 이번 변경사항 중 하나는 자바의 record 지원입니다.

코틀린에서 일반적으로 클래스나 프로퍼티를 사용하는것처럼 자바의 record 클래스를 사용할 수 있습니다. data 클래스 에 @JvmRecord 어노테이션을 선언해주면 됩니다.

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

Note : record는 코틀린의 data클래스와 유사하게, 순수 데이터만을 담도록 선언할 수 있는 문법으로 자바 14에서 도입됨

Sealed 인터페이스

코틀린 인터페이스를 선언할 떄 sealed 키워드를 추가할 수 있게 되었습니다. 이 인터페이스는 모든 구현체들을 컴파일 타임에 알 수 있습니다. (sealed 클래스와 동일)

sealed interface Polygon

fun draw(polygon: Polygon) = when (polygon) {
   is Rectangle -> // ...
   is Triangle -> // …
   // else is not needed - all possible implementations are covered
}

추가적으로 sealed 인터페이스도 일반적인 인터페이스와 마찬가지로, 클래스가 둘 이상의 sealed 인터페이스를 상속할 수 있어서 보다 유연하게 클래스 계층구조를 제한할 수 있습니다.

Package-wide sealed 클래스 상속

예전에는 sealed 클래스를 상속하는 서브클래스는 반드시 같은 파일내에 있어야 했습니다. 이제는 sealed 클래스를 상속하는 서브클래스가 같은 모듈(Same compilation unit) 내에 있고, 같은 패키지인 경우라면 다른파일이더라도 허용되도록 변경되었습니다. 이 서브클래스는 파일에서의 최상위 클래스이든 중첩 클래스이든 상관없이 가능합니다.

ㅇ단, 이 서브클래스는 로컬 or 익명 object 여서는 안됩니다. 반드시 적절한 클래스 이름을 가지고 있어야 합니다.

inline 클래스

inline 클래스는 오로지 값만을 가지고있는 value-based 클래스의 하위 개념입니다. inline class를 이용하면 객체를 생성하기 위해 메모리를 할당하는 오버헤드를 줄일 수 있는 장점이 있습니다.

inline 클래스는 class 키워드 앞에 value 키워드를 추가하여 정의합니다. Kotlin/JVM외에 Kotlin/JS, Kotlin/Native와의 호환성이 필요하다면 @JvmInline 어노테이션을 추가해줍니다.

@JvmInline
value class Password(val s: String)

inline 키워드는 이제 경고와 함꼐 deprecate 되었습니다.

value class? : 위의 예시에서 선언된 Password 클래스처럼 Primitive 값 하나를 래핑한 클래스로, 컴파일시점에 Value 클래스를 프로퍼티로 변경하도록 동작합니다. value 클래스의 자세한 설명은 공식문서에서 확인할 수 있습니다.

표준 라이브러리

unsigned integer 타입 지원

부호없는 정수 타입 (UIntULongUByteUShort)이 지원됩니다. 이 부호없는 정수타입은 다른 타입들과 동일하게 연산자, 범위 등을 사용할 수 있습니다. 아직 부호없는 배열은 Beta 단계입니다.

지역상관없는 대소문자 text API 지원

텍스트의 대/소문자 관련하여 지역에 구애받지 않는 새로운 API가 출시되었습니다. 이 API들은 기존에 지역의 영향을 받는 toLowerCase()toUpperCase()capitalize()decapitalize() API들을 대체할 수 있습니다.

  • String 클래스
이전 버전1.5.0 이후
String.toUpperCase()String.uppercase()
String.toLowerCase()String.lowercase()
String.capitalize()String.replaceFirstChar { it.uppercase() }
String.decapitalize()String.replaceFirstChar { it.lowercase() }
  • Char 클래스
이전 버전1.5.0 이후
Char.toUpperCase()Char.uppercaseChar(): Char
Char.uppercase(): String
Char.toLowerCase()Char.lowercaseChar(): Char
Char.lowercase(): String
Char.toTitleCase()Char.titlecaseChar(): Char
Char.titlecase(): String

Note : Kotlin/JVM에서는 명시적으로 Locale 파라미터를 지정할 수 있도록 uppercase()lowercase()titlecase() 메서드마다 오버로드된 메서드들이 존재합니다.

Char - Integer 변환 API 지원

코틀린 1.5.0부터 Char - Code / Char - Digit 변환 API를 제공합니다. 기존에 존재하는 String - Int 변환 메서드는 종종 혼동의 여지가 있었기에, 새로운 API를 통하여 기존것을 대체합니다. 새로운 API는 code 라고 명명하여 좀 더 명확하게 의미를 드러내도록 했습니다.

  • 정수코드를 통해 문자를 얻거나, 문자에서 정수코드를 얻는 메서드
fun Char(code: Int): Char
fun Char(code: UShort): Char
val Char.code: Int
  • 문자를 숫자값으로 변환하는 메서드
fun Char.digitToInt(radix: Int): Int
fun Char.digitToIntOrNull(radix: Int): Int?
  • 음수가 아닌 10 미만의 정수를 문자로 바꿔주는 Int 확장메서드
fun Int.digitToChar(radix: Int): Char

Number.toChar()Int.toChar()Char.toInt() 등 이전의 변환 API들은 deprecate 됩니다.

Path API 지원

java.nio.file.Path의 확장함수로 지원되는 Path API가 정식으로 지원됩니다.

// construct path with the div (/) operator
val baseDir = Path("/base")
val subDir = baseDir / "subdirectory"

// list files in a directory
val kotlinFiles: List<Path> = Path("/home/user").listDirectoryEntries("*.kt")

버림 나눗셈 (Floored division)과 나머지 연산

표준 라이브러리에 나머지 연산을 위한 새로운 연산자가 추가되었습니다.

  • floorDiv() : 함수는 버림나눗셈의 결과를 반환합니다. 정수타입에서만 이용가능합니다.
  • mod() 함수는 버림나눗셈의 나머지값을 반환합니다. 모든 숫자타입에서 이용가능합니다.

추가된 연산자는 기존의 정수 나눗셈이나 rem() 함수 (% 연산자와 동일)와 매우 유사해보이지만, 음수를 연산할 때 다르게 동작합니다.

  • a / b는 0에 가까운 값을 결과로 주는 반면에, a.floorDiv(b)는 더 작은 정수의 방향으로 반올림한 결과를 줍니다.
  • a.mod(b)의 결과값은 a와 a.floorDiv(b) * b 사이의 차이입니다. 이 값은 0이거나 b와 동일한 부호를 가지게 됩니다. 그러나 a % b는 다른 값을 가집니다.
// 예시
println("Floored division -5/3: ${(-5).floorDiv(3)}")
println( "Modulus: ${(-5).mod(3)}")

println("Truncated division -5/3: ${-5 / 3}")
println( "Remainder: ${-5 % 3}")

// 결과
Floored division -5/3: -2
Modulus: 1
Truncated division -5/3: -1
Remainder: -2

Duration API 변경

시간의 차이를 나타내기 위해서 Duration 클래스를 실험적으로 도입했었습니다. 코틀린 1.5.0에서 Duration API는 다음의 변경점이 있습니다.

  • internal value는 Double대신 Long을 이용하도록 변경되었습니다.
  • Long 타입으로 변환하는 새로운 API가 추가되었고, 기존의 Double타입의 API들은 Deprecate 되었습니다. 예를들어 Duration.inWholeMinutes는 Long타입으로 값을 반환하고, Duration.inMinutes로 대체되었습니다.
  • 숫자로부터 Duration을 생성하는 Companion 메서드가 추가되었습니다. 예를들어 Duration.seconds(Int)는 인자로 받은 숫자의 초 만큼을 나타내는 Duration 객체를 생성합니다. Int.seconds 같은 예전의 확장프로퍼티들은 deprecate 되었습니다.

멀티플랫폼 코드를 위한 Char 카테고리 확인 API 지원

코틀린 1.5.0에서는 멀티플랫폼 프로젝트에서 유니코드에 따라 문자의 카테고리를 얻는 API를 도입했습니다.

문자인지 숫자인지를 확인하는 함수는 다음과 같습니다.

  • Char.isDigit()
  • Char.isLetter()
  • Char.isLetterOrDigit()
val chars = listOf('a', '1', '+')
val (letterOrDigitList, notLetterOrDigitList) = chars.partition { it.isLetterOrDigit() }
println(letterOrDigitList) // [a, 1]
println(notLetterOrDigitList) // [+]

// 결과
[a, 1]
[+]

문자의 Case를 확인하는 함수는 다음과 같습니다.

  • Char.isLowerCase()
  • Char.isUpperCase()
  • Char.isTitleCase()
val chars = listOf('Dž', 'Lj', 'Nj', 'Dz', '1', 'A', 'a', '+')
val (titleCases, notTitleCases) = chars.partition { it.isTitleCase() }
println(titleCases) // [Dž, Lj, Nj, Dz]
println(notTitleCases) // [1, A, a, +]

// 결과
[Dž, Lj, Nj, Dz]
[1, A, a, +]

그 외에 다음 함수들도 추가되었습니다.

  • Char.isDefined()
  • Char.isISOControl()

Char.category 프로퍼티는 해당 문자의 카테고리를 의미하는 CharCategory enum class를 반환합니다.

새로운 콜렉션 함수 firstNotNullOf()

새로 추가된 firstNotNullOf()와 firstNotNullOfOrNull() 함수는 mapNotNull()과 first() / firstOrNull() 메서드를 조합한 것입니다. 새로운 함수는 원본 콜렉션을 커스텀 셀렉터 함수와 매핑하고, 최초의 non-null 값을 반환합니다. 해당하는 값이 없다면 firstNotNullOf()는 예외를 던지고, firstNotNullOfOrNull()은 null을 반환합니다.

val data = listOf("Kotlin", "1.5")
println(data.firstNotNullOf(String::toDoubleOrNull))
println(data.firstNotNullOfOrNull(String::toIntOrNull))

// 결과
1.5
null

String?.toBoolean()의 Strict 버전

이미 존재하는 String?.toBoolean()의 대소문자를 구분하는 엄격한 버전의 API가 추가되었습니다.

  • String.toBooleanStrict() : truefalse 리터럴을 제외한 다른 문자열이 있다면 예외를 던집니다.
  • String.toBooleanStrictOrNull() : truefalse 리터럴을 제외한 다른 문자열이 있다면 null을 반환합니다.
println("true".toBooleanStrict())
println("1".toBooleanStrictOrNull())
// println("1".toBooleanStrict()) // Exception

// 결과
true
null

원문

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