Enumeration
Enumeration (열거형)
enumeration은 이름 붙은 값들의 집합으로 이루어진 타입을 정의할 때 사용해요.
본문
enum Color:
case Red, Green, Blue
이 코드는 새 sealed 클래스 Color를 정의하는데, 여기에는 Color.Red, Color.Green, Color.Blue라는 세 값이 있어요. 색상 값들은 Color의 컴패니언 객체의 멤버예요.
파라미터화된 enum
enum은 파라미터화할 수 있어요.
enum Color(val rgb: Int):
case Red extends Color(0xFF0000)
case Green extends Color(0x00FF00)
case Blue extends Color(0x0000FF)
예시에서 볼 수 있듯이, 명시적인 extends 절을 사용해서 파라미터 값을 정의할 수 있어요.
enum을 위해 정의되는 메서드
enum의 값들은 고유한 정수와 대응돼요. enum 값과 연관된 정수는 ordinal 메서드가 돌려줘요.
enum Color(val rgb: Int):
case Red extends Color(0xFF0000)
case Green extends Color(0x00FF00)
case Blue extends Color(0x0000FF)
val red = Color.Red
val ord = red.ordinal
assert(ord == 0)
enum의 컴패니언 객체는 유틸리티 메서드 세 개도 정의해요. valueOf 메서드는 이름으로 enum 값을 얻고, values 메서드는 enumeration에 정의된 모든 enum 값을 Array로 돌려줘요. fromOrdinal 메서드는 ordinal(Int) 값으로부터 enum 값을 얻어요.
enum Color(val rgb: Int):
case Red extends Color(0xFF0000)
case Green extends Color(0x00FF00)
case Blue extends Color(0x0000FF)
val blue = Color.valueOf("Blue")
// blue: Color = Blue
val values = Color.values
// values: Array[Color] = Array(Red, Green, Blue)
val red = Color.fromOrdinal(0)
// red: Color = Red
enum의 사용자 정의 멤버
enum에 자신만의 정의를 추가할 수도 있어요. 예를 들어볼게요.
enum Planet(mass: Double, radius: Double):
private final val G = 6.67300E-11
def surfaceGravity = G * mass / (radius * radius)
def surfaceWeight(otherMass: Double) = otherMass * surfaceGravity
case Mercury extends Planet(3.303e+23, 2.4397e6)
case Venus extends Planet(4.869e+24, 6.0518e6)
case Earth extends Planet(5.976e+24, 6.37814e6)
case Mars extends Planet(6.421e+23, 3.3972e6)
case Jupiter extends Planet(1.9e+27, 7.1492e7)
case Saturn extends Planet(5.688e+26, 6.0268e7)
case Uranus extends Planet(8.686e+25, 2.5559e7)
case Neptune extends Planet(1.024e+26, 2.4746e7)
end Planet
enum의 사용자 정의 컴패니언 객체
enum을 위해 명시적인 컴패니언 객체를 정의하는 것도 가능해요.
enum Planet(mass: Double, radius: Double):
private final val G = 6.67300E-11
def surfaceGravity = G * mass / (radius * radius)
def surfaceWeight(otherMass: Double) = otherMass * surfaceGravity
case Mercury extends Planet(3.303e+23, 2.4397e6)
case Venus extends Planet(4.869e+24, 6.0518e6)
case Earth extends Planet(5.976e+24, 6.37814e6)
case Mars extends Planet(6.421e+23, 3.3972e6)
case Jupiter extends Planet(1.9e+27, 7.1492e7)
case Saturn extends Planet(5.688e+26, 6.0268e7)
case Uranus extends Planet(8.686e+25, 2.5559e7)
case Neptune extends Planet(1.024e+26, 2.4746e7)
end Planet
object Planet:
def main(args: Array[String]) =
val earthWeight = args(0).toDouble
val mass = earthWeight / Earth.surfaceGravity
for p <- values do
println(s"Your weight on $p is ${p.surfaceWeight(mass)}")
end Planet
Enum case에 대한 제약
Enum case 선언은 이차 생성자(secondary constructor)와 비슷해요. enum 템플릿 안에 선언되지만 스코프는 템플릿 밖으로 잡히죠. 즉 enum case 선언은 enum 클래스의 내부 멤버에 접근할 수 없어요.
마찬가지로 enum case 선언은 enum의 컴패니언 객체의 멤버를 직접 참조할 수도 없어요. (직접 import했거나 이름을 바꿔 import했더라도 마찬가지예요.) 예를 들어:
import Planet.*
enum Planet(mass: Double, radius: Double):
private final val (mercuryMass, mercuryRadius) = (3.303e+23, 2.4397e6)
case Mercury extends Planet(mercuryMass, mercuryRadius) // Not found
case Venus extends Planet(venusMass, venusRadius) // illegal reference
case Earth extends Planet(Planet.earthMass, Planet.earthRadius) // ok
object Planet:
private final val (venusMass, venusRadius) = (4.869e+24, 6.0518e6)
private final val (earthMass, earthRadius) = (5.976e+24, 6.37814e6)
end Planet
Mercury가 참조하는 필드는 보이지 않고, Venus가 참조하는 필드도 (import Planet.*를 썼더라도) 직접 참조할 수 없어요. Earth에서 보여준 것처럼 간접적인 참조를 써야 해요.
참고로 enum case를 위한 컴패니언 객체는 enum 클래스 템플릿 안에서도, enum 클래스 컴패니언 객체 안에서도 작성할 수 없어요.
enum Planet(mass: Double, radius: Double):
case Earth extends Planet(5.976e+24, 6.37814e6)
object Earth:
// a near-Earth object, but not a companion and not an enum value
assert(this ne Planet.Earth)
def report = assert(Planet.this.Earth eq Planet.Earth, s"I thought $this was Earth!")
end Planet
Enum case는 접근 수식자(access modifier)만 받아요. Enum 클래스는 접근 수식자와 함께 into나 infix도 받아요.
Enum case의 폐기(deprecation)
라이브러리 작성자라면 enum case가 더 이상 쓰일 의도가 아님을 알리고 싶을 수 있어요. 하지만 폐기된 case를 특별히 처리하는 것처럼, 공개 API에서 case를 제거해도 우아하게 처리하고 싶을 수도 있죠.
예를 들어 설명할게요. Planet enum에 원래 case가 하나 더 있었다고 해볼게요.
enum Planet(mass: Double, radius: Double):
...
case Neptune extends Planet(1.024e+26, 2.4746e7)
+ case Pluto extends Planet(1.309e+22, 1.1883e3)
end Planet
이제 Pluto case를 폐기하고 싶다고 해볼게요. 먼저 Pluto에 scala.deprecated 어노테이션을 추가해요.
enum Planet(mass: Double, radius: Double):
...
case Neptune extends Planet(1.024e+26, 2.4746e7)
- case Pluto extends Planet(1.309e+22, 1.1883e3)
+
+ @deprecated("refer to IAU definition of planet")
+ case Pluto extends Planet(1.309e+22, 1.1883e3)
end Planet
enum Planet이나 object Planet의 어휘적 스코프 밖에서 Planet.Pluto를 참조하면 폐기 경고가 나와요. 하지만 그 스코프 안에서는 여전히 참조할 수 있어서, 폐기된 case에 대한 인트로스펙션을 구현할 수 있어요.
trait Deprecations[T <: reflect.Enum]:
extension (t: T) def isDeprecatedCase: Boolean
object Planet:
given Deprecations[Planet]:
extension (p: Planet)
def isDeprecatedCase = p == Pluto
라이브러리가 타입 클래스 유도(type class derivation)를 이용해서 Deprecations의 인스턴스를 자동으로 제공하는 상황을 상상해볼 수 있어요.
Java Enum과의 호환성
Scala로 정의한 enum을 Java enum처럼 쓰고 싶다면, 기본으로 import되는 java.lang.Enum 클래스를 다음과 같이 상속하면 돼요.
enum Color extends Enum[Color]:
case Red, Green, Blue
타입 파라미터는 Java enum 정의에서 오는 것으로, enum의 타입과 같아야 해요. java.lang.Enum을 상속할 때 생성자 인자(Java API 문서에 정의된 것)를 제공할 필요는 없어요——컴파일러가 자동으로 생성해 주거든요.
Color를 그렇게 정의하면, Java enum처럼 사용할 수 있어요.
enum Color extends Enum[Color]:
case Red, Green, Blue
val cmp = Color.Red.compareTo(Color.Green)
assert(cmp == -1)
Java에서 Scala 3 enum을 사용하는 더 깊은 예시는 이 테스트에서 볼 수 있어요. 그 테스트에서는 enum이 MainScala.scala 파일에 정의되어 있고 Java 소스 Test.java에서 사용돼요.
구현
enum은 scala.reflect.Enum 트레이트를 상속하는 sealed 클래스로 표현돼요. 이 트레이트는 공개 메서드 하나 ordinal을 정의해요.
package scala.reflect
/** A base trait of all Scala enum definitions */
transparent trait Enum extends Any, Product, Serializable:
/** A number uniquely identifying a case of an enum */
def ordinal: Int
extends 절을 가진 enum 값은 익명 클래스 인스턴스로 확장돼요. 예를 들어 위의 Venus 값은 다음과 같이 정의될 거예요.
val Venus: Planet = new Planet(4.869E24, 6051800.0):
def ordinal: Int = 1
override def productPrefix: String = "Venus"
override def toString: String = "Venus"
extends 절이 없는 enum 값들은 모두 단일 구현을 공유하는데, 이는 태그와 이름을 인자로 받는 private 메서드로 인스턴스화할 수 있어요. 예를 들어 위의 Color.Red 값의 첫 정의는 다음과 같이 확장될 거예요.
val Red: Color = $new(0, "Red")
참고 자료
더 자세한 내용은 Issue #1970과 PR #4003을 참고하세요.