Can Associated Values and Raw Values Coexist in Swift Enumeration

Difference between associated and raw values in swift enumerations

Raw values are for when every case in the enumeration is represented by a compile-time-set value. The are akin to constants, i.e.

let A = 0
let B = 1

is similar to:

enum E: Int {
case A // if you don't specify, IntegerLiteralConvertible-based enums start at 0
case B
}

So, A has a fixed raw value of 0, B of 1 etc set at compile time. They all have to be the same type (the type of the raw value is for the whole enum, not each individual case). They can only be literal-convertible strings, characters or numbers. And they all have to be distinct (no two enums can have the same raw value).

Associated values are more like variables, associated with one of the enumeration cases:

enum E {
case A(Int)
case B
case C(String)
}

Here, A now has an associated Int that can hold any integer value. B on the other hand, has no associated value. And C has an associated String. Associated types can be of any type, not just strings or numbers.

Any given value of type E will only ever hold one of the associated types, i.e. either an Int if the enum is an A, or a String if the enum is a C. It only needs enough space for the bigger of the two. Types like this are sometimes referred to as "discriminated unions" – a union being a variable that can hold multiple different types, but you know (from the enum case) which one it is holding.

They can even be generic. The most common example of which is Optional, which is defined like this:

enum Optional<T> {
case .Some(T)
case .None
}

How to archive enum with an associated value?

The main problem for your issue is that you cannot pass Swift enums to encode(_:forKey:).

This article shown by Paulw11 will help you solve this part. If the enum can easily have rawValue, it's not too difficult.

But, as you see, Enum with raw type cannot have cases with arguments.

Simple enums can easily have rawValue like this:

    enum UnicornColor: Int {
case yellow, pink, white
}

But enums with associate values, cannot have rawValue in this way. You may need to manage by yourself.

For example, with having inner enum's rawValue as Int :

enum Creature: Equatable {
enum UnicornColor: Int {
case yellow, pink, white
}

case unicorn(UnicornColor)
case crusty
case shark
case dragon

static func == (lhs: Creature, rhs: Creature) -> Bool {
//...
}
}

You can write an extension for Dream.Creature as:

extension Dream.Creature: RawRepresentable {
var rawValue: Int {
switch self {
case .unicorn(let color):
return 0x0001_0000 + color.rawValue
case .crusty:
return 0x0002_0000
case .shark:
return 0x0003_0000
case .dragon:
return 0x0004_0000
}
}

init?(rawValue: Int) {
switch rawValue {
case 0x0001_0000...0x0001_FFFF:
if let color = UnicornColor(rawValue: rawValue & 0xFFFF) {
self = .unicorn(color)
} else {
return nil
}
case 0x0002_0000:
self = .crusty
case 0x0003_0000:
self = .shark
case 0x0004_0000:
self = .dragon
default:
return nil
}
}
}

(In fact, it is not an actual rawValue and you'd better rename it for a more appropriate name.)

With a definition like shown above, you can utilize the code shown in the link above.

How can I use sealed classes to describe a finite set of cases with associated values, and a smaller set of such values?

Is this how I should approach this problem?

IMHO, yes, but with some semantic changes, listed after.

How can I best make the properties of the associated value accessible from the sealed class?

You can generate extensions or instance functions for each subclass.

e.g.

val ContentType.Case2.id: String get() = value.id

In this way, you can successfully call:

is ContentType.Case2 -> println(content.id)

How can I reduce the app size while preserving the semantic of another case?

You can do it generating only one class for all the cases which need the same types as parameters and using Kotlin contracts to handle them.

Taking your example, you could generate:

sealed class ContentType {
class Case1(val value: Foo) : ContentType()
class Case2_3(val value: Bar, val caseSuffix: Int) : ContentType()
}

As you can see, the classes Case2 and Case3 are now only one class and caseSuffix identifies which one of them it is.

You can now generate the following extensions (one for each case):

@OptIn(ExperimentalContracts::class)
fun ContentType.isCase1(): Boolean {
contract {
returns(true) implies (this@isCase1 is ContentType.Case1)
}
return this is ContentType.Case1
}

@OptIn(ExperimentalContracts::class)
fun ContentType.isCase2(): Boolean {
contract {
returns(true) implies (this@isCase2 is ContentType.Case2_3)
}
return this is ContentType.Case2_3 && caseSuffix == 2
}

@OptIn(ExperimentalContracts::class)
fun ContentType.isCase3(): Boolean {
contract {
returns(true) implies (this@isCase3 is ContentType.Case2_3)
}
return this is ContentType.Case2_3 && caseSuffix == 3
}

Since you are using contracts the client can now use them with:

when {
content.isCase1() -> println(content.title)
content.isCase2() -> println(content.id)
content.isCase3() -> println(content.id)
}

As you can see, a further optimization could be removing the property caseSuffix for cases with only one suffix to avoid unnecessary properties.



Related Topics



Leave a reply



Submit