Development record of developer who study hard everyday.

레이블이 코틀린인 게시물을 표시합니다. 모든 게시물 표시
레이블이 코틀린인 게시물을 표시합니다. 모든 게시물 표시
, , , ,

코틀린(Kotlin) 제네릭(Generic) 파헤치기

 코틀린(Kotlin) 제네릭(Generic) 파헤치기

안드로이드 블로그

코틀린 공식문서를 번역하면서 공부 중이다...

오늘은 Generic에 대해서 공부하려고한다.


1. Generics : in, out, where

Kotlin에서 클래스들은 JAVA에서처럼 타입 파라미터를 가질 수 있다.


class Box<T>(t: T) {
var value = t
}


Box 객체를 만들기위해서 아래처럼 타입 매개변수를 부여하자.

val box: Box<Int> = Box<Int>(1)


이때, 파라미터의 타입이 예측될 수 있어서 타입을 생략할 수 있다.

val box: Box = Box(1)


2. 변성(Variance)

자바에서 가장 혼란스러운 부분이 wildcard(와일드카드) 타입이다.

코틀린은 wildcard가 존재하지 않는다.

대신에, 코틀린은 declaration-site variance와 type projection이 존재한다.


왜 자바는 wildcard가 필요할까?

Effective JAVA 3rd Edition을 보면, API의 유연성을 높이기위해 wildcard를 사용하라고한다.

Java에서 제네릭 타입은 invariant(불변)이다.

예를 들어, List가 invariant(불변)이 아니라면, 자바의 array보다 나을게 없다.

왜냐하면 아래 코드가 컴파일 되더라도 런타임에서 exception을 던질 것이기 때문이다.

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! A compile-time error here saves us from a runtime exception later.
objs.add(1); // Put an Integer into a list of Strings
String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String

Java는 런타임시의 안전성을 위해 위와 같은 코드진행을 못하게한다.

하지만 이것이 암시하는 바가 있다.

예를 들어, Collection interface의 addAll()을 생각해보자.

이 메소드의 매개변수가 무엇일까?

// Java
interface Collection<E> ... {
void addAll(Collection<E> items);
}


하지만 이때, 아래와 같이 할 수 없다.

// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from);
// !!! Would not compile with the naive declaration of addAll:
// Collection<String> is not a subtype of Collection<Object>
}


따라서, addAll() 메소드의 실제 매개변수는 아래와 같다.

// Java
interface Collection<E> ... {
void addAll(Collection<? extends E> items);
}


? extends E로 표현되는 wildcard 타입은 이 메소드가 E 뿐만아니라 E객체의 collection 또는 E를 상속하는 타입까지 받을 수 있다는 것을 말해준다.

다시 말해, Collection에서 E 타입을 안전하게 읽을 수 있다는 것이다.

하지만, 어떤 객체가 E의 하위항목인지 모르기때문에 값을 write할 수 는 없다.

이러한 제한때문에 Collection<String>이 Collection<? extends Object>의 하위항목이였으면 하는 생각이 든다.

즉, extend로 이루어진 wildcard가 covariant(공변성)을 이루었으면 한다.


공변성의 핵심은 간단하다.

collection에서 값을 가져왔으면, Object 타입으로 읽어와서  String 타입의 Collection을 사용하라.

반대로, collection에 값을 넣을 수 있다면, Object 타입의 collection으로 받아서 String 타입의 값을 넣어줘라. 

예를 들어, Java에는 List<? super String>이라는 List<Object>의 supertype이 있다.


후자를 반변성이라고 한다.

당신은 오직 List<? super String>의 매개변수인 String 타입의 값을 취하는 메소드를 호출할 수 있다.

ex) add(String), set(int, String)

만약 List<T>에서 T 타입을 리턴하는 무언가를 호출한다면, 그것은 String 타입이 아니라 Object 타입을 받을 것이다.


Joshua Bloch는 오로직 읽어들이는 객체에 Producers라는 이름을 붙였고

오로지 쓰는(write) 객체에 Consumers라는 이름을 붙였다

"For maximum flexibility, use wildcard types on input parameters that represent producers or consumers", and proposes the following mnemonic:

PECS stands for Producer-Extends, Consumer-Super.


Declaration-site variance(선언부 변성)

Source<T>라는 제네릭 인터페이스가 있다고하자.

이 인터페이스는 T를 매개변수로 취하는 어떤 메소드도 갖지 않는다.

오로지 T를 리턴하는 메소드만 존재한다.

// Java
interface Source<T> {
T nextT();
}


이때, Source<Object> 타입의 변수에 Source<String>타입의 객체를 저장하기에 완벽하다.

여기에 consumer는 없다.

하지만 자바는 이것을 알지못하고 하지못하게한다.


// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!! Not allowed in Java
// ...
}


이를 해결하기 위해서, 우리는 Source<? extends Object>를 선언해야만한다.

이렇게하는 것은 의미가 없다.

왜냐하면  Source<? extends Object>를 선언하기 이전에 모두 같은 메소드를 호출할 수 있기 때문이다.

그래서 더 복잡한 타입에의해 추가된 값은 없다.

하지만 컴파일러는 그것을 알지 못한다.(= Source<Object>로 Source<String>을 받을 수 없다.)


Kotlin에서는 컴파일러에게 이렇게 설명한다.

이게 바로 declaration-site variance 이다.

매개변수 Source<T>는 오로지 Source<T>의 하위객체로 리턴된(produced된) 것이고 consume은 절대 하지 않는다는 것을 표현할 수 있다.

"out" 한정자를 사용하자.

interface Source<out T> {
fun nextT(): T
}

fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // This is OK, since T is an out-parameter
// ...
}


일반적인 규칙은 이렇다.

C<out T> => it may occur only in the out-position in the members of C, but in return C<Base> can safely be a supertype of C<Derived>

[ 😥너무 어렵다;;

C클래스의 멤버에서 out 포지션에서만 발생한다. 하지만 그 결과, C<Base>는 안전하게 C<Derived>의 supertype이 될 수 있다...??

out이 Java에서 <? extends T>와 같으니까 ?에 어떤값이 와도 T로 받을 수 있다 라는 말 아닐까? ]

다른 말로, C 클래스는 파라미터 T에 covariant(공변성)하다. 또는 T는 covariant한 타입 파라미터다.

C 클래스를 T의 producer로 생각할 수 있다. 물론 consumer는 아니다.


Kotlin은 추가적으로 variance annotation을 제공한다. 바로 in이다.

in은 타입 파라미터를 반변성으로 만든다.

타입 파라미터는 오로지 consume되고 절대 produce 될 수 없다.

좋은 예가 바로 Comparable interface이다.

interface Comparable<in T> {
operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
// Thus, you can assign x to a variable of type Comparable<Double>
val y: Comparable<Double> = x // OK!
}


3. 타입 프로젝션(Type projections)

타입 프로젝션은 Use-site variance이다

타입 파라미터 T를 out으로 선언하는 것은 쉽고 타입 파라미터를 사용하는 부분에서 여러 문제를 피할 수 있다.

하지만 몇몇 클래스들은 오직 T로 리턴하게끔 제한될 수 없다.

class Array<T>(val size: Int) {
operator fun get(index: Int): T { ... }
operator fun set(index: Int, value: T) { ... }
}

☝ 위 클래스는 공변성도 반변성도 없다.

따라서 여러 문제들이 발생된다.


fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}

☝ 위 메소드는 한 Array를 다른 Array로 내용을 복사하는 역할을 한다.

한 번 실행해보자


val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any)
// ^ type is Array<Int> but Array<Any> was expected
Array<T>는 T에 대해서 불변성을 가지고있다.

그리고 Array<Int>와 Array<Any>는 둘다 어떤 객체의 하위타입이 아니다.

copy 메소드는 예를들어 from 배열로 String을 쓰게끔 시도할 수 있고 이럴 때 ClassCastException이 발생할 것이다.

이러한 상황을 막기위해 아래와 같이 수정한다.

fun copy(from: Array<out Any>, to: Array<Any>) { ... }

☝ 이것이 type projection이다.

from array는 단순한 배열이 아니고 제한된(projected) 배열이다.

당신은 오로지 타입 파라미터 T를 반환하는 메소드를 호출할 수 있다.

이 경우에, 오로지 get() 메소드만 부를 수 있다.

이것이 use-site variance이다.

이것은 Java의 Array<? extends Object>와 같다.


또한, in을 가지고 타입을 project할 수 있다.

fun fill(dest: Array<in String>, value: String) { ... }

Array<in String>은 Java의 Array<? super String>과 같다.

이것은 dest 배열에 CharSequence나 Object를 fill()할 수 있다는 뜻이다.


스타 프로젝션(Star-projection)

때때로 타입 매개변수에대해 아는게 없는데 안정적으로 코드를 진행하고 싶을 때가 있다.

그럴 때 제네릭 타입의 projection을 정의하는 방식이 있다.

제네릭 타입의 모든 객체화가 projection의 하위타입이 될 것이다.

이름하여 스타 프로젝션(star-projection)이라 부른다.

- Foo<out T : TUpper>, 즉 T가 TUpper에 공변성이 있을 때, 

Foo<*>은 Foo<out TUpper>과 같다.

따라서 T를 알지 못할 때도 Foo<*>에서 TUpper의 값을 안정적으로 읽을 수 있다.


- Foo<in T>, 즉 T가 반변성일 때,

Foo<*> 는 Foo<in Nothing>과 같다.

따라서 T를 알지 못할 때, Foo<*>에 어떤 값도 쓸 수 없다.


- Foo<T : TUpper>, 즉 타입 파라미터 T가 TUpper일 때, Foo<*>는 값을 읽을 때는 Foo<out TUpper>와 같고 값을 쓸 때는 Foo<in Nothing>과 같다.


제네릭 타입이 여러 타입 파라미터를 가진다면, 각각은 독립적으로 project된다.

예를 들어, interface Function<in T, out U>라면, 스타 프로젝션을 아래와 같이 사용할 수 있다.

- Function<*, String>은 Function<in Nothing, String>이다.

- Function<Int, *>은 Function<Int, out Any?>이다.

- Function<*, *>은 Function<in Nothing, out Any?>이다.


4. 제네릭 함수(Generic functions)

클래스만 타입 파라미터를 가지는 것이 아니다.

함수도 타입 파라미터를 가질 수 있다.

함수에서 타입 파라미터는 함수 이름 바로 앞에 붙는다.

fun <T> singletonList(item: T): List<T> {
// ...
}

fun <T> T.basicToString(): String { // extension function
// ...
}


제네릭 함수를 호출할 때ㅔ, 함수 이름 뒤에 타입 파라미터를 명시해준다.

val l = singletonList<Int>(1)


타입 파라미터가 예측될 때는 생략 가능하다.

val l = singletonList(1)


5. Generic 제한(Generic constraints)

타입 파라미터를 대체할 수 있는 모든 가능성있는 타입의 입력은 generic constraints 에 의해 제한될 지도 모른다.


가장 흔한 제한은 upper bound이다.

이것은 Java의 extends 키워드와 같다.

fun <T : Comparable<T>> sort(list: List<T>) {  ... }

: 이후에 나온 타입이 upper bound이다.

오로지 Comparable<T>의 하위타입만이 T를 대체할 수 있다는 뜻이다.

sort(listOf(1, 2, 3)) // OK. Int is a subtype of Comparable<Int>
sort(listOf(HashMap<Int, String>())) // Error: HashMap<Int, String> is not a subtype of Comparable<HashMap<Int, String>>


upper bound의 기본값은 Any?이다.

오로지 하나의 upper bound만이 <>에 명시될 수 있다.

만약 하나 이상의 upper bound가 필요하다면 where 구문을 사용해야한다.

fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}

전달된 T는 where 구문의 모든 조건을 충족해야한다.

위의 코드는 T는 반드시 CharSequence와 Comparable을 구현해야만한다.


6. 타입 제거(Type erasure)

Kotlin이 제네릭 선언을 사용하는 타입 안정성체크는 컴파일 타임에 이루어진다.

런타임시, 제네릭 타입의 객체는 실제 타입 매개변수에 대한 어떤 정보도 갖지않는다.

타입에 대한 정보는 지워진다는 것이다.

예를 들어, Foo<Bar>와 Foo<Baz?>는 Foo<*>으로 지워진다.


제네릭 타입체크와 형변환(Generics type checks and casts)

타입 제거(Type erasure) 때문에 런타임시에 제네릭 타입이 어떤 타입으로 만들어졌는지 확인할 수 있는 방법이 없다.

그리고 컴파일러는 is의 사용을 막는다.

예를들어, ints is List<Int>나 list is T 는 불가능하다.

그러나 star-projected 타입을 사용하여 객체의 타입을 체크할 수 있다.

if (something is List<*>) {
something.forEach { println(it) } // The items are typed as `Any?`
}


이와 비슷하게, 이미 타입이 확인된 객체를 가지고있다면, is 사용도 가능하고 제네릭 타입이 아닌 타입으로 형변환도 가능하다.

아래 예제코드를 살펴보자. <>에 타입이 생략된 것을 주목하라.

fun handleStrings(list: MutableList<String>) {
if (list is ArrayList) {
// `list` is smart-cast to `ArrayList<String>`
}
}

타입 매개변수가 생략된 문법이 형변환에도 사용될 수 있다. (list as ArrayList)


제네릭함수 호출에서 타입 매개변수는 컴파일 타임에 체크된다.

함수 본체 안에서, 타입 매개변수는 타입 체크를 사용할 수 없고 타입 매개변수로의 형변환(foo as T)도 확인될 수 없다.

유일한 예외는 reified 타입 매개변수이다.

reified는 호출부에 인라인된 실제 타입 매개변수를 가진다.

그래서 타입 체크와 형변환이 가능하다.

그러나 위에서 언급한 제한은 여전히 타입 체크나 형변환에 사용된 제네릭 타입의 객체를 적용한다.

예를들어, arg is T 라는 타입체크 상황에서, arg가 제네릭 타입 그 자체라면, 타입 매개변수는 지워진다.

inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {
if (first !is A || second !is B) return null
return first as A to second as B
}

val somePair: Pair<Any?, Any?> = "items" to listOf(1, 2, 3)


val stringToSomething = somePair.asPairOf<String, Any>()
val stringToInt = somePair.asPairOf<String, Int>()
val stringToList = somePair.asPairOf<String, List<*>>()
val stringToStringList = somePair.asPairOf<String, List<String>>() // Compiles but breaks type safety!
// Expand the sample for more details


확인되지않은 형변환(Unchecked cast)

제네릭타입에서 확실한 타입으로의 형변환(foo as List<String>)은 런타임시 확인될 수 없다.

이런 확인되지않은 형변환(Unchecked cast)는 타입 안전성이 추정되는 높은 수준의 프로그램에서 사용 가능하다.

하지만 컴파일러에 의해 직접적으로 이루어지지는 않는다.

아래 코드를 살펴보자.


fun readDictionary(file: File): Map<String, *> = file.inputStream().use {
TODO("Read a mapping of strings to arbitrary elements.")
}

// We saved a map with `Int`s into this file
val intsFile = File("ints.dictionary")

// Warning: Unchecked cast: `Map<String, *>` to `Map<String, Int>`
val intsDictionary: Map<String, Int> = readDictionary(intsFile) as Map<String, Int>

마지막 줄에 경고가 나타난다.

컴파일러는 런타임시에 완전히 타입 체크를 할 수 없고 Map 객체 안의 값이 Int라는 것을 보장할 수 없다.

unchecked cast를 피하기위해서는, 프로그램 구조를 다시 짜야한다.

위의 예제코드처럼, 타입 안전성을 위해서 DictionaryReader<T>와 DictionaryWriter<T> 인터페이스를 사용할 수 있다.

unchecked cast를 호출부에서 implmentation으로 옮긺으로서 합리적인 추상화를 도입할 수 있다.

arg의 타입이 지워진 타입이 아니라면 제네릭 함수에서 reified 타입 매개변수를 사용하는것이 arg as T 같은 형변환을 가능케한다.

@Suppress("UNCHECKED_CAST")를 사용하면 unchecked cast 경고가 무시된다.

inline fun <reified T> List<*>.asListOfType(): List<T>? =
if (all { it is T })
@Suppress("UNCHECKED_CAST")
this as List<T> else
null


7. Underscore 연산자

Underscore(_)는 타입 매개변수를 위해 사용될 수 있다.

다른 타입이 명시적으로 정해져있을 때 타입 매개밴수를 자동으로 추론하기위해 사용한다.

abstract class SomeClass<T> {
abstract fun execute() : T
}

class SomeImplementation : SomeClass<String>() {
override fun execute(): String = "Test"
}

class OtherImplementation : SomeClass<Int>() {
override fun execute(): Int = 42
}

object Runner {
inline fun <reified S: SomeClass<T>, T> run() : T {
return S::class.java.getDeclaredConstructor().newInstance().execute()
}
}

fun main() {
// T is inferred as String because SomeImplementation derives from SomeClass<String>
val s = Runner.run<SomeImplementation, _>()
assert(s == "Test")

// T is inferred as Int because OtherImplementation derives from SomeClass<Int>
val n = Runner.run<OtherImplementation, _>()
assert(n == 42)
}




Share:
Read More
, , , , , ,

코틀린 Inline class에 대하여 알아보자(번역)

 Kotlin Inline classes 공식문서 번역


오늘은 코틀린 인라인 클래스(Inline class)에 대해서 알아보자.

개발공부는 항상 공식문서를 기준으로 시작한다.

코틀린 공식홈페이지에서 문서를 번역한 것을 기본으로 한다.

                                              

때때로 변수 타입의 wrapper 클래스를 만들 때가 있다. 하지만 이것은 런타임에 과부하를 가져온다. 게다가, 원시형 타입으로 wrapper 클래스를 만들게되면, 성능은 더 좋지 않아진다.

이런 문제를 해결하기위해, 코틀린은 inline class라는 특별한 클래스를 제공한다. inline class는 value-based classes 중 하위항목이다. inline class는 id값을 가지지 않고 오직 값만 가질 수 있다.

1. 정의

inline class를 선언하기 위해서는, value라는 modifier를 사용한다.

JVM에 선언하기 위해서는 @JvmInline 어노테이션을 사용한다.

@JvmInline
value class Password(private val s: String)

✋ inline class에서 inline 한정자는 deprecated 되었다.


inline class는 주 생성자로 초기화된 하나의 프로퍼티만 가져야한다.

// No actual instantiation of class 'Password' happens
// At runtime 'securePassword' contains just 'String'
val securePassword = Password("Don't try this in production")


2. 멤버

Inline class는 일반 클래스의 기능을 제공한다.

예를 들어, 프로퍼티와 메소드를 선언할 수 있고 init 블록도 제공한다.

@JvmInline
value class Name(val s: String) {
init {
require(s.length > 0) { }
}

val length: Int
get() = s.length

fun greet() {
println("Hello, $s")
}
}

fun main() {
val name = Name("Kotlin")
name.greet() // method `greet` is called as a static method
println(name.length) // property getter is called as a static method
}

☝ Inline class의 프로퍼티는 backing field를 가지지 않는다. 그저 간단한 계산가능한 프로퍼티들을 가질 뿐이다.


3. 상속

Inline class는 interface로부터 상속받을 수 있다.

interface Printable {
fun prettyPrint(): String
}

@JvmInline
value class Name(val s: String) : Printable {
override fun prettyPrint(): String = "Let's $s!"
}

fun main() {
val name = Name("Kotlin")
println(name.prettyPrint()) // Still called as a static method
}

☝ Inline class는 class 상속이 불가능하다.

다시 말해, inline class는 항상 final이다.


4. 표현

generated code에서, 코틀린 컴파일러는 Inline class에 대한 wrapper 클래스를 유지한다.

Inline class는 런타임시 wrapper class나 원래의 자료형으로 표현된다. 이것은 Int가 원시형 타입 int나 wrapper class인 Integer로 표현되는 것과 같다.

Inline class는 다른 자료형으로 사용될 때마다 wrapper class로 감싸진다.

interface I

@JvmInline
value class Foo(val i: Int) : I

fun asInline(f: Foo) {}
fun <T> asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}

fun <T> id(x: T): T = x

fun main() {
val f = Foo(42)

asInline(f) // unboxed: used as Foo itself
asGeneric(f) // boxed: used as generic type T
asInterface(f) // boxed: used as type I
asNullable(f) // boxed: used as Foo?, which is different from Foo

// below, 'f' first is boxed (while being passed to 'id') and then unboxed (when returned from 'id')
// In the end, 'c' contains unboxed representation (just '42'), as 'f'
val c = id(f)
}

Inline class는 원래의 자료형과 wrapper class 둘 다 표현가능하기 때문에 

referential equality(===)가 의미없다.


Inline class는 generic 타입의 매개변수를 가질 수 있다. 이 경우 컴파일러는 generic을 Any?나 상위 타입의 매개변수로 매핑시킨다.


@JvmInline
value class UserId<T>(val value: T)

fun compute(s: UserId<String>) {} // compiler generates fun compute-<hashcode>(s: Any?)


5. Mangling

Inline class는 원래의 타입으로 컴파일 되기 때문에 불명확한 에러를 발생시킨다.

예를들어, 아래와 같이 매개변수 충돌을 일으킬 수 있다.

@JvmInline
value class UInt(val x: Int)

// 'public final void compute(int x)'로 JVM상에서 컴파일됨
fun compute(x: Int) { }

// 마찬가지로 'public final void compute(int x)' JVM상에서 컴파일됨;;
fun compute(x: UInt) { }

이러한 현상을 막기위해, Inline class를 사용하는 함수들은 해시코드를 함수 이름에 더하는 방식으로 mangle된다.

따라서 fun compute(x : UInt)는 public final void compute-<hashcode>(int x) 로 표현된다.


자바에서도 Inline class를 사용할 수 있다.

이때, @JvmName을 추가하여 mangling을 지워야만한다.

@JvmInline
value class UInt(val x: Int)

fun compute(x: Int) { }

@JvmName("computeUInt")
fun compute(x: UInt) { }


6. Inline class와 type alias

Inline class는 type alias와 매우 비슷해보인다. 실제로, 두 개념 모두 새로운 타입을 도입하고 런타임 시에 원래의 타입으로 표현된다.

하지만, 원래의 타입과 assignment-compatible이 결정적인 차이다.

Inline class는 원래의 타입과 assignment-compatible이 성립하지 않는다.

(assignment-compatible을 뭐라고 해석해야지?? 할당호환성?? ㅋㅋ)


다시말해, Inline class는 완전히 새로운 타입이다. 반면에, type alias는 원래 존재하는 타입의 다른 이름일 뿐이다.

typealias NameTypeAlias = String    //type alias 선언

@JvmInline
value class NameInlineClass(val s: String)    //Inline class 선언

fun acceptString(s: String) {}
fun acceptNameTypeAlias(n: NameTypeAlias) {}
fun acceptNameInlineClass(p: NameInlineClass) {}

fun main() {
val nameAlias: NameTypeAlias = ""
val nameInlineClass: NameInlineClass = NameInlineClass("")
val string: String = ""

acceptString(nameAlias) // OK: String 타입 대신에 type alias 가능
acceptString(nameInlineClass) // not OK: String 타입 대신에 Inline class 불가능

// 반대로 해보자
acceptNameTypeAlias(string) // OK: type alias 대신에 String 타입 가능
acceptNameInlineClass(string) // Not OK: Inline class 대신에 String 타입 불가능
}


Share:
Read More
, ,

코틀린 enum class 개념 정리

 코틀린 enum class 개념 정리

안드로이드 개발 블로그

지금 회사에 입사하기 전에 코틀린을 아예 몰랐다....

자바로만 안드로이드 개발을 공부하다가 뒤늦게 취업을 해야겠다 싶어서 지금 회사에 들어왔다.

그래서 들어오자마자 코틀린 공부에 열을 올렸다.

코틀린을 공부하면서 enum 클래스에 대한 개념은 자바와 똑같기 때문에 이해하고 있지만 막상 쓰려고하면 망설임이 있었다.

그래서 이번에 블로그에 기록하면서 제대로 공부해보려고한다.


1. 정의 

enum은 열거형 클래스라고 한다.

주로 클래스의 타입을 구분하기 위해 사용한다.

자바에서는 enum을 사용하는데 코틀린에서는 enum class를 사용한다.

선언하는 방식은 아래와 같다.

enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

각 enum 상수는 객체이다.

enum 상수는 ,(콤마)로 구분된다.

각 enum은 enum class의 객체이기때문에 초기화될 수 있다.

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


2. 프로퍼니와 메서드 정의하기

enum은 단순히 값만 열거하는 존재가 아니다.

enum 클래스 안에도 프로퍼티나 메서드를 정의할 수 있다.

enum class Color(val r: Int, val g:Int, val b: Int) {
RED(255, 0, 0), ORANGE(255, 165, 0), //생성자 파라미터 형태로 프로퍼티 값 지정
YELLOW(255, 255, 0), GREEN(0, 255, 0),
BLUE(0, 0, 255), INDIGO(75, 0, 130),
VIOLET(238, 130, 238);    //여기 반드시 세미콜론 사용

fun rgb() = (r * 256 + g) * 256 + b    //메서드 정의
}

enu에서도 일반적인 클래스와 마찬가지로 생성자와 프로퍼티를 생성한다.

enum 상수를 정의할 때 그 상수에 해당하는 프로퍼티 값을 지정해야만 한다.

enum 클래스 안에 프로퍼티나 메서드를 정의하는 경우 반드시 enum 상수 목록과 메서드 정의 사이에 세미콜론을 넣어야한다.


3. 익명 클래스

enum 상수들은 익명객체를 선언할 수 있다.

그리고 메소드를 상속거나 구현할 수도 있다.

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

},
TALKING {
override fun signal(): ProtocolState = WAITING
};

abstract fun signal() : ProtocolState
}


4. interface 구현하기

enum class는 interface를 구현할 수 있다.(하지만 클래스는 상속할 수 없다.)

한번에 공통의 인터페이스 멤버를 구현할 수도 있고(applyAsInt()) 익명 클래스를 만들어서 각각 구현할 수도 있다.(apply()) 

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): Int = apply(t, u)    //공통의 인터페이스 멤버 구현
}


5. enum 상수 활용하기

enum class는 enum 상수 목록을 보여주는 메서드와 이름으로 enum 상수를 가져오는 메서드를 제공한다.

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


6. ordinal 과 name

모든 enum 상수들은 ordinal과 name 프로퍼티를 가진다.

ordinal을 사용하면 해당 타입이 enum에서 몇 번째 타입인지 알 수 있다.

단, 인덱스가 0부터 시작한다.

val type : Color = Color.ORANGE
println("Orange의 순서는 ${type.ordinal}번째 입니다")  

위 코드를 실행하면,

"Orange의 순서는 1번째 입니다." 라고 나온다.


Share:
Read More