[Swift] enum のすすめ Vol.2 (associatedValue の扱い)

     

TAGS:

⌛️ 2 min.
Swift の enum は 拡張されて associatedValue と呼ばれる値を持つことができるようになっています。associatedValue の使い方を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 12.4
  • Xcode 13.3.1
  • iOS 15.4

associatedValue を使うシーン

前回の記事で、enum は、有限個の状態のいずれかであることを表現する と説明しました。

[Swift] enum のすすめ

以降では、タイマーアプリを想定して、そのアプリの状態を表現するための enum を考えていきます。

タイマーアプリの状態として考えられるのが、以下の2つです。
・タイマー動作中
・タイマー停止中

単純化のために 1つのタイマーのみを扱うアプリとすると、上の2つの状態をいずれかを持つことになります。上記2つの状態以外を取ることはないものとします。

enum で定義すると以下のようになります。


enum AppState {
    case TimerRunning
    case TimerStopped
}

この enum を 管理することで、アプリの状態を自然に管理することができます。

この AppState を持つ変数 が .TimerRunning であれば タイマー動作中であり、それに準じた定期処理を行うと想定されますし、.TimerStopped であれば、タイマーは停止しているので、タイマースタートボタンは、押下可能な状態になっていることでしょう。

このタイマーアプリの実装を進めていくと、タイマーが完了した時にも特別な動作を行いたくなります。例えば、ユーザーに通知したり、画面がフラッシュしたり ということです。

タイマーの完了がいつなのか という情報を 先の enum と “別の変数” として持つようにするとコードとしては以下のようなコードが考えられます。(変数名は timerExpire としています。)


if appState == .TimerRunning {
    if timerExpire < Date() {
       // timer 完了処理
    }
} else if appState == .TimerStopped {
    // ....
}

この timerExpire という変数は、.TimerRunning の時にしか使いません。
なぜなら タイマーが動作していない時に、タイマーが完了することはないからです。

言い換えると、timerExpire という変数は、状態の一部であり、TimerRunning の状態の時のみ意味がある情報ということです。

このような 特定の状態のみが持つ付随する情報を表す時には Swift の enum では associatedValue というものを使用して保持することができます。

associatedValueを持つ enum の定義

以下は、TimerRunning という case で、いつタイマーが完了するのかという情報も併せて保持するような enum を定義しています。


enum AppState {
     case TimerRunning(timerExpire: Date)
     case TimerStopped
}

TimerRunning の時には、 timerExpire という Date 型の情報を付与しています。
しかし、TimerStopped にはそのような情報は付与されていません。

このように 定義されている case 毎に異なる associatedValue を持つことができます。

このようにすることで、”特定の状態の時のみ意味のある情報” を きれいに 定義することができます。

associatedValue を指定して enum をインスタンス化する

AppState をインスタンス化する時を見てみます。

以下のように インスタンス化することが可能です。
associatedValue を持つ case では associatedValue も指定して インスタンス化することが必要です。


var appState = AppState.TimerStopped

// 60秒後に expire するタイマーを開始した状態
appState = .TimerRunning(timerExpire: Date().advanced(by: 60))

特定の case に付随している associatedValue は、変数が書き換えられると失われます。
例えば、以下のコードでは、2回目の .TimerStopped を代入された時点で、直前に与えられている timerExpire の情報は失われています。


var appState = AppState.TimerStopped

// 60秒後に expire するタイマーを開始した状態
appState = .TimerRunning(timerExpire: Date().advanced(by: 60))

// expire!
appState = .TimerStopped

// .TimerRunning の時の timerExpire 情報は失われている

あくまで associatedValue も含めた case が enum の値であり、上書きされるとそれ以前の情報は失われてしまいます。

enum から associatedValue を取得する

associatedValue を持つ enum を定義して、インスタンス化しましたので、次は、associatedValue を取得する方法を説明します。

associatedValue を受け取る方法として、switch 文で受け取る方法と case let 文で受け取る方法があります。

おさらい: associatedValue を持たない enum の扱い

(associatedValue を持たない) enum の case は、以下のように switch 文で使ったり、比較したりすることができます。



enum AppState {
    case TimerRunning
    case TimerStopped
}
var appState = AppState.TimerStopped


// switch 文で enum をチェック
switch (appState) {
case .TimerStopped:
    print("timer is stopped")
case .TimerRunning:
    print("timer is running")
}

// == を使って、enum が同値であるかチェック
if appState == .TimerRunning {
    print("timer is running")
}

switch 文を使用して取得

associatedValue を持つ enum を対象とする時は、以下のように、switch の case 文に、associatedValue を指定することで、その値を受け取ることができます。case に associatedValue が定義されている時は、その associatedValue を無視することはできず、必ず指定する必要があります。


enum AppState {
    case TimerRunning(timerExpire: Date)
    case TimerStopped
}

var appState = AppState.TimerStopped

// 60秒後に expire するタイマーを開始した状態
appState = .TimerRunning(timerExpire: Date().advanced(by: 60))

switch (appState) {
case .TimerStopped:
    print("timer is stopped")
case .TimerRunning(let timerExpire): // - associatedValue を受け取る
    print("timer is running, will expire at \(timerExpire)")
}
// print-out
timer is running, will expire at 2022-06-10 13:35:47 +0000

受け取る必要がない時には、_ を使用して受け取らないことを明示しないといけません。


enum AppState {
    case TimerRunning(timerExpire: Date)
    case TimerStopped
}


let check = AppState.TimerStopped

print(check)

var appState = AppState.TimerStopped

// 60秒後に expire するタイマーを開始した状態
appState = .TimerRunning(timerExpire: Date().advanced(by: 60))

// expire!
//appState = .TimerStopped

switch (appState) {
case .TimerStopped:
    print("timer is stopped")
case .TimerRunning(_): // _ を使用することで、associatedValue を無視している
    print("timer is running, will expire soon?")
}

case let を使用して取得

switch 文は、enum で定義されている状態を抜け漏れなく処理する時に便利ですが、特定の状態であるかどうかをチェックして処理したい時もあります。

そのような時に case let (case condition と呼ばれます) を使って処理することもできます。以下は、.TimerRunning である時のみ associatedValue を使った処理(print文) を行うコードです。


enum AppState {
    case TimerRunning(timerExpire: Date)
    case TimerStopped
}

var appState = AppState.TimerStopped

// 60秒後に expire するタイマーを開始した状態
appState = .TimerRunning(timerExpire: Date().advanced(by: 60))

if case AppState.TimerRunning(let timerExpire) = appState {
    print("timer is running, it will expire at \(timerExpire)")
}
// print-out
timer is running, it will expire at 2022-06-10 13:39:04 +0000

まとめ

enum が持つことのできる associatedValue の使い方を説明しました。

enum の概要と struct/class との使い分け
  • enum は、case ごとに、指定した型の値を持つことができる(associatedValue)
  • enum の case から associatedValue を取得する時は case と一緒に取得する

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です