안드로이드 개발자 노트
[코틀린 완벽 가이드] 6장 : 특별한 클래스 사용하기 본문
1. 이넘 클래스
이넘 클래스는 미리 정의해둔 상수들로 이뤄진 제한된 집합을 표현하는 클래스이다.
enum class WeekDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
fun WeekDay.isWorkDay() =
this == WeekDay.SATURDAY || this == WeekDay.SUNDAY
fun main() {
println(WeekDay.MONDAY.isWorkDay()) // false
println(WeekDay.SATURDAY.isWorkDay()) // true
}
1. 빠뜨린 부분이 없는 when 식
when 식에서 모든 이넘 상수를 다룬 경우에는 else 가지를 생략할 수 있다.
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
fun rotateClockWise(direction: Direction) = when (direction) {
Direction.NORTH -> Direction.EAST
Direction.EAST -> Direction.SOUTH
Direction.SOUTH -> Direction.WEST
Direction.WEST -> Direction.NORTH
}
새 이넘 값을 추가하는 경우에, 빠뜨린 부분이 없는 when을 사용하지 않으면 새로운 값을 호출했을때 예외가 발생할 수 있다. 하지만 빠뜨린 부분이 없는 when을 사용하게 되면 컴파일 시점에서 알 수 있기 때문에 이런 오류를 방지할 수 있다.
자바의 switch문의 case절에는 이넘 값을 간단한 이름으로 사용할 수 있지만, 코틀린의 when은 그렇지 않다. rotateClockWise() 함수와 비슷한 역할을 하는 자바 메서드를 비교해보면,
public Direction rotateClockWise(Direction d) {
switch (d) {
case NORTH: return Direction.EAST;
case EAST: return Direction.SOUTH;
case SOUTH: return Direction.WEST;
case WEST: return Direction.NORTH;
}
throw new IllegalArgumentException("Unknown value: " + d);
}
이넘 상수를 임포트하면 한정시키지 않은 짧은 이름으로 이넘 상수를 사용할 수 있다.
import Direction.*
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
fun rotateClockWise(direction: Direction) = when (direction) {
NORTH -> Direction.EAST
EAST -> Direction.SOUTH
SOUTH -> Direction.WEST
WEST -> Direction.NORTH
}
2. 커스텀 멤버가 있는 이넘 정의하기
이넘 클래스도 멤버를 포함할 수 있으며, 확장 함수나 프로퍼티를 붙일 수도 있다.
enum class WeekDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
val lowerCaseName get() = name.lowercase()
fun isWorkDay() = this == SATURDAY || this == SUNDAY
}
fun main() {
println(WeekDay.MONDAY.isWorkDay()) // false
println(WeekDay.WEDNESDAY.lowerCaseName) // wednesday
}
이넘 클래스에 생성자가 있으면 각 이넘 상수의 정의 뒤에도 적절한 생성자 호출을 추가해야 한다.
enum class RainbowColor(val isColor: Boolean) {
RED(false), ORANGE(false), YELLOW(false),
GREEN(true), BLUE(true), INDIGO(true), VIOLET(true);
val isWarm get() = !isColor
}
fun main() {
println(RainbowColor.BLUE.isColor) // true
println(RainbowColor.RED.isWarm) // true
}
이넘 상수에서도 자신만의 정의가 들어있는 본문이 포함될 수 있다. 하지만 이런 상수에 의해 생겨나는 익명 타입은 코드 밖으로 노출되지 않는다. 따라서 이넘 상수의 바깥 부분에서는 접근할 수는 없다.
enum class WeekDay {
MONDAY{ fun startWork() = println("Work week started") },
TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}
fun main() = WeekDay.MONDAY.startWork() // Error
3. 이넘 클래스의 공통 멤버 사용하기
모든 이넘 값에는 ordinal과 name이라는 한 쌍의 프로퍼티가 들어있다.
ordinal은 이넘 클래스 안에서 정의된 이넘 값의 순서에 따른 인덱스이고, name은 이넘 값의 이름이다. 또한 특정 이넘 값을 정의의 위치에 따라 서로 비교할 수 있다.
enum class Direction {
NORTH, SOUTH, WEST, EAST;
}
fun main() {
println(Direction.WEST.name) // WEST
println(Direction.WEST.ordinal) // 2
println(Direction.WEST == Direction.NORTH) // false
println(Direction.WEST != Direction.EAST) // true
println(Direction.EAST < Direction.NORTH) // false
println(Direction.SOUTH >= Direction.NORTH)// true
}
이넘 값에 대한 비교는 ordinal 프로퍼티가 돌려주는 인덱스 값에 정의된다.
이넘 클래스는 동반 객체의 멤버처럼 호출할 수 있는 암시적인 메서드들을 제공한다. valueOf() 메서드는 이넘 값의 이름을 문자열로 넘기면 그에 해당하는 이넘 값을 돌려주거나 이름이 잘못된 경우 예외를 던진다.
enum class Direction {
NORTH, SOUTH, WEST, EAST;
}
fun main() {
println(Direction.valueOf("NORTH")) // NORTH
println(Direction.valueOf("NORTH_EAST")) // java.lang.IllegalArgumentException
}
values() 메서드는 정의된 순서대로 모든 이넘 값이 들어있는 배열을 돌려준다.
이 메서드를 호출하면 새 배열이 생기는 것에 유의해야한다.
enum class WeekDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
private val weekDay = WeekDay.values()
val WeekDay.nextDay get() = weekDay[(ordinal + 1) % weekDay.size]
values()나 valueOf() 대신에 제너릭 최상위 메서드인 enumValues()와 enumValueOf()를 사용할 수도 있다.
fun main() {
val weekDays = enumValues<WeekDay>()
println(weekDays[2]) // WEDNESDAY
println(enumValueOf<WeekDay>("THURSDAY")) // THURSDAY
}
2. 데이터 클래스
1. 데이터 클래스와 데이터 클래스에 대한 연산
class Person(val firstName: String, val familyName: String, val age: Int)
이 클래스의 두 인스턴스가 동등한지 비교하려면, 정체성이 같은 지를 비교하면 된다. 두 객체가 메모리에서 같은 위치에 있는 객체라면 같다.
fun main() {
val person1 = Person("John", "Doe", 25)
val person2 = Person("John", "Doe", 25)
val person3 = person1
println(person1 == person2) // false, 서로 다른 정체성
println(person1 == person3) // true, 서로 같은 정체성
}
클래스에 커스텀 동등성 비교가 필요하면 equals()나 hashCode()를 구현한다.
코틀린의 데이터 클래스는 이런 메서드를 자동으로 생성해준다.
data class Person(val firstName: String, val familyName: String, val age: Int)
fun main() {
val person1 = Person("John", "Doe", 25)
val person2 = Person("John", "Doe", 25)
val person3 = person1
println(person1 == person2) // true
println(person1 == person3) // true
}
컴파일러가 주 생성자에 정의된 프로퍼티의 값을 서로 비교하는 동등성 비교 연산을 자동으로 생성해주기 때문에 두 비교 모두 true를 반환한다.
프로퍼티 값의 비교도 equals() 메서드를 사용한다. 깊은 동등성 비교가 이뤄지는지 여부는 프로퍼티의 타입으로 어떤 타입이 들어있느냐에 따라 달라진다.
data class Person(val firstName: String, val familyName: String, val age: Int)
data class Mailbox(val address: String, val person: Person)
fun main() {
val box1 = Mailbox("Unknown", Person("John", "Doe", 25))
val box2 = Mailbox("Unknown", Person("John", "Doe", 25))
println(box1 == box2) // true
}
String, Person, Mailbox는 모두 내용을 바탕으로 동등성을 구현하기 때문에 Mailbox 인스턴스 비교는 address 프로퍼티 비교와 person에 들어있는 Person 클래스의 동등성 비교에 따라 결정된다. 하지만 데이터 클래스가 아니면, Person 프로퍼티 비교가 객체의 정체성에 따라 결정되기 때문에 결과가 바뀐다.
class Person(val firstName: String, val familyName: String, val age: Int)
data class Mailbox(val address: String, val person: Person)
fun main() {
val box1 = Mailbox("Unknown", Person("John", "Doe", 25))
val box2 = Mailbox("Unknown", Person("John", "Doe", 25))
println(box1 == box2) // false: 두 Person 인스턴스의 정체성이 다름
}
데이터 클래스는 toString() 메서드 구현도 생성해준다.
data class Person(val firstName: String, val familyName: String, val age: Int)
fun main() {
val person = Person("John", "Doe", 25)
println(person) // Person(firstName=John, familyName=Doe, age=25)
}
주 생성자의 파라미터에서 선언한 프로퍼티만 equals() / hashCode() / toString() 메서드 구현에 쓰인다. 다른 프로퍼티들은 이런 함수들의 생성에 영향을 못미친다.
data class Person(val firstName: String, val familyName: String) {
var age = 0
}
fun main() {
val person1 = Person("John", "Doe").apply { age = 25 }
val person2 = Person("John", "Doe").apply { age = 26 }
println(person1 == person2) // true
}
모든 데이터 클래스는 copy() 함수를 제공한다. 이 함수를 사용하면 인스턴스를 복사하면서 몇몇 프로퍼티를 변경할 수 있다.
copy()는 불변 데이터 구조를 더 쉽게 사용하도록 해준다.
data class Person(val firstName: String, val familyName: String, val age: Int)
fun Person.show() = println("$firstName $familyName: $age")
fun main() {
val person = Person("John","Doe",25)
person.show() // John Doe: 25
person.copy().show() // John Doe: 25
person.copy(familyName = "Smith").show() // John Smith: 25
person.copy(age = 30, firstName = "Jane").show() // Jane Doe: 30
}
코틀린은 두 가지 범용 데이터 클래스가 있다. 두 값(한 쌍: pair)이나 세 값(트리플렛: triple)을 저장할 수 있는 데이터 클래스이다.
fun main() {
val pair = Pair(1, "two")
println(pair.first + 1) // 2
println("${pair.second}!") // two!
val triple = Triple("one", 2, false)
println("${triple.first}!") // one!
println(triple.second - 1) // 1
println(!triple.third) // true
}
중위 연산자를 사용해 Pair 객체를 만들 수도 있다.
val pair = 1 to "two"
println(pair.first + 1) // 2
println("${pair.second}!") // two!
2. 구조 분해 선언
데이터 클래스를 각각의 프로퍼티에 대응하는 지역 변수를 정의하는 간결한 구문을 사용할 수 있다.
data class Person(val firstName: String,
val familyName: String,
val age: Int)
val (firstName, familyName, age) = Person("John", "Doe", 25)
이런 선언을 구조 분해 선언이라고 하며, 변수 이름을 하나만 사용하는 대신 괄호로 감싼 식별자 목록으로 여러 변수를 한꺼번에 정의할 수 있게 해주는 지역 변수 선언 구문이다. 이름은 변수 선언과 같고, = 다음에 적은 데이터 클래스 인스턴스의 프로퍼티에 해당한다.
여기서 각 변수에 어떤 프로퍼티가 매핑되는지는 데이터 클래스의 생성자에 있는 각 프로퍼티의 위치에 따라 결정되며, 선언하는 변수의 이름에 의해 결정되지 않는다.
val (familyName, firstName, age) = Person("John", "Doe", 25)
println("$firstName $familyName: $age") // Doe John: 25
필요하면 구조 분해를 이루는 각 컴포넌트 변수에 타입을 표기할 수 있으며 기본적으로는 필요하지 않다.
val (firstName, familyName: String, age) = Person("John", "Doe", 25)
데이터 클래스의 프로퍼티 수보다 적은 수의 변수가 들어갈 수도 있다. 이 경우 뒷 부분에 선언된 프로퍼티는 추출되지 않는다.
val (firstName, familyName) = Person("John", "Doe", 25)
println("$firstName $familyName") // John Doe
val (name) = Person("John", "Doe", 25)
println(name) // John
시작 부분이나 중간 부분에서 몇 가지 프로퍼티를 생략해야 한다면 _로 대신할 수 있다.
val (_, familyName) = Person("John", "Doe", 25)
println(familyName)
var로 선언하면 가변 변수를 얻을 수 있다.
val/var 지정은 모든 부분에 적용되며, 구조 분해 선언의 모든 변수를 가변 변수로 정의하거나 불변 변수로만 정의해야만 한다.
var (firstName, familyName) = Person("John", "Doe", 25)
firstName = firstName.lowercase()
familyName = familyName.lowercase()
println("$firstName $familyName") // john doe
for 루프에서도 구조 분해를 사용 할 수 있다.
val pair = arrayOf(1 to "one", 2 to "two", 3 to "three")
for ((number, name) in pair) {
println("$number: $name")
}
람다 파라미터에 대해 구조 분해를 사용할 수 있다.
일반 람다 파라미터 목록과 달리 구조 분해를 사용하는 파라미터를 괄호로 둘러싸야만 한다.
fun combine(person1: Person, person2: Person, folder: ((String, Person) -> String)): String {
return folder(folder("", person1), person2)
}
fun main() {
val p1 = Person("John", "Doe", 25)
val p2 = Person("Jane", "Doe", 26)
// 구조 분해를 쓰지 않음
println(combine(p1, p2) { text, person -> "$text ${person.age}"}) // 25 26
// 구조 분해를 씀
println(combine(p1, p2) { text, (firstName) -> "$text $firstName"}) // John Jane
println(combine(p1, p2) { text, (_, familyName) -> "$text $familyName"}) // Doe Doe
}
3. 인라인 클래스(값 클래스)
1. 인라인 클래스 정의하기
인라인 클래스를 정의하려면 value class를 클래스 이름 앞에 붙여야 한다. 또한, JVM 백엔드를 사용하는 경우에는 @JvmInlin을 value class 앞에 붙여줘야 한다.
@JvmInline
value class Dollar(val amount: Int) // amount의 단위는 센트
@JvmInline
value class Euro(val amount: Int) // amount의 단위는 센트
인라인 클래스의 주생성자에는 불변 프로퍼티를 하나만 선언해야 한다. 런타임에 클래스 인스턴스는 별도의 래퍼 객체를 생성하지 않고 이 프로퍼티의 값으로 표현된다. 호출 지점을 함수 본문으로 인라인하는 인라인 함수처럼 인라인 클래스 객체를 사용하는 위치 대신 인라인 클래스에 들어있는 값이 들어간다.
@JvmInline
value class Dollar(val amount: Int) {
fun add(d: Dollar) = Dollar(amount + d.amount)
val isDebt get() = amount < 0
}
fun main() {
println(Dollar(15).add(Dollar(20)).amount) // 35
println(Dollar(-100).isDebt) // true
}
2. 부호 없는 정수
부호 없는 정수 타입의 이름은 부호 있는 정수 타입의 이름 앞에 U를 덧붙인 것이다.
타입 | 크기(바이트) | 범위 |
UByte | 1 | 0 ~ 255 |
UShort | 2 | 0 ~ 65535 |
UInt | 4 | 0 ~ 2^32-1 |
ULong | 8 | 0 ~ 2^64-1 |
부호 없는 값은 정수 리터럴 뒤에 u/U 접두사를 붙여 표현한다. 변수 타입을 지정하지 않은 경우에는 크기에 따라 UInt나 ULong 중 하나로 결정된다.
val uByte: UByte = 1u // 명시적으로 UByte
val uShort: UShort = 100u // 명시적으로 UShort
val uInt = 1000u // 자동으로 UInt로 추론
val uLong: ULong = 1000u // 명시적으로 ULong
val ULong2 = 1000uL // L 접미사가 붙었기 때문에 명시적으로 ULong
부호있는 타입과 부호없는 타입은 서로 호환되지 않는다. 부호없는 타입의 값을 부호있는 타입의 변수)에 저장할 수 없다. (그 반대 방향도)양 타입의 값을 toXXX()를 통해 반대쪽 타입으로 변환할 수 있다.
val long: Long = 1000ul // Error
println(1.toUByte()) // 1, Int -> UByte
println((-100).toUShort()) // 65436, Int -> UShort
연산자(+, -, *, /, %)는 부호없는 타입의 값을 모든 가능한 방식으로 짝을 지어 사용할 수 있다. 하지만 부호없는 타입의 값과 부호있는 타입의 값을 섞어서 사용할 수는 없다.
println(1u + 2u) // 3
println(1u + 2) // error
당연하지만, 부호없는 정수는 단항 부호 반전(-) 연산자를 지원하지 않는다.
println(-1u) // error
부호없는 값에 대해 증/감/복합 대입 연산자를 쓸 수 있으며, 비트반전, AND, OR, XOR과 같은 비트 연산을 지원한다.
var uInt: UInt = 1u
++uInt
uInt -= 3u
val ua: UByte = 67u // 01000011
val ub: UByte = 139u // 10001011
println(ua.inv()) // 10111100: 188
println(ua or ub) // 11001011: 203
println(ua xor ub) // 11001000: 200
println(ua and ub) // 00000011: 3
UInt와 ULong은 왼쪽과 오른쪽 비트 시프트를 지원한다. 여기서 비트 수는 UInt가 아닌 Int 타입의 값으로 지정한다.
부호없는 시프트 연산인 ushr/ushl 을 제공하지 않는다. 또한 비교 연산도 지원한다.
val ua = 67u // 0..0001000011
println(ua shr 2) // 0..0000010000: 16
println(ua shl 2) // 0..0100001100: 268
println(1u < 2u) // true
println(2u >= 3u) // false
println(2u + 2u == 1u + 3u) // true
부호없는 정수 배열도 지원한다.
val uBytes = ubyteArrayOf(1u, 2u, 3u)
val squares = UIntArray(10) { it.toUInt()*it.toUInt() }
'Kotlin > 코틀린 완벽 가이드' 카테고리의 다른 글
[코틀린 완벽 가이드] 7-1장 : 컬렉션 (0) | 2022.10.23 |
---|---|
[코틀린 완벽 가이드] 5장 : 고급 함수와 함수형 프로그래밍 활용하기 (0) | 2022.10.04 |
[코틀린 완벽 가이드] 4장 : 클래스와 객체 다루기 (1) | 2022.09.29 |