enum と struct、どっちを選ぶべき?

2024/12/09に公開
2

この記事は Swift Advent Calendar 2024 の 10 日目の記事です。
昨日は @lemo-nade-room さんの Server-Side Swift Vapor を Cloud Run にデプロイし、PostgreSQL に証明書認証で接続する でした。証明書の話がゴリゴリ書いてあって、とても勉強になりました。

Swift の enum は便利だ

Swift の enum は有能です。Associated Value を持つことができ、強力なパターンマッチを備えています。名前空間としても使われています。
どこにでも使いたくなってしまう魅力があります。
基本的に enum を使っていて問題はおこりませんが、次のようなコードがあるとしましょう。

enum Size {
  case small
  case medium
  case large
}

この enum は、Size.small という形で利用でき、Size 型を取ることがわかっている場合には .small という形で利用することができます。

この enum を拡張して、ボタンのフォントやサイズを定義することにします。

extension Size {
  var fontSize: CGFloat {
    switch self {
    case .small:
      return 12
    case .medium:
      return 16
    case .large:
      return 20
    }
  }
}

これを利用すると、次のようにかけます。

Button("Hello")
  .font(.system(size: .small.fontSize))

いいでしょう。この調子でボタンのプロパティを色々と追加していきましょう。

var color: Color {
  switch self {
  case .small:
    return .red
  case .medium:
    return .green
  case .large:
    return .blue
  }
}

var cornerRadius: CGFloat {
  switch self {
  case .small:
    return 4
  case .medium:
    return 8
  case .large:
    return 12
  }
}

var padding: CGFloat {
  switch self {
  case .small:
    return 4
  case .medium:
    return 8
  case .large:
    return 12
  }
}

var fontWeight: Font.Weight {
  switch self {
  case .small:
    return .light
  case .medium:
    return .regular
  case .large:
    return .bold
  }
}

まとめるとこうなります。

enum Size {
  case small
  case medium
  case large

  var fontSize: CGFloat {
    switch self {
    case .small:
      return 12
    case .medium:
      return 16
    case .large:
      return 20
    }
  }

  var color: Color {
  switch self {
  case .small:
    return .red
  case .medium:
    return .green
  case .large:
    return .blue
  }

  var cornerRadius: CGFloat {
    switch self {
    case .small:
      return 4
    case .medium:
      return 8
    case .large:
      return 12
    }
  }

  var padding: CGFloat {
    switch self {
    case .small:
      return 4
    case .medium:
      return 8
    case .large:
      return 12
    }
  }

  var fontWeight: Font.Weight {
    switch self {
    case .small:
      return .light
    case .medium:
      return .regular
    case .large:
      return .bold
    }
  }
}

ここに 60 行程度の enum が誕生しました。 enum なので網羅されていていいですね。
さて、これは本当にうれしいコードでしょうか?

struct で書いてみる

このコードを struct で書いてみましょう。

struct Size {
  let fontSize: CGFloat
  let color: Color
  let cornerRadius: CGFloat
  let padding: CGFloat
  let fontWeight: Font.Weight

  private init(fontSize: CGFloat, color: Color, cornerRadius: CGFloat, padding: CGFloat, fontWeight: Font.Weight) {
    self.fontSize = fontSize
    self.color = color
    self.cornerRadius = cornerRadius
    self.padding = padding
    self.fontWeight = fontWeight
  }

  static let small = Size(fontSize: 12, color: .red, cornerRadius: 4, padding: 4, fontWeight: .light)
  static let medium = Size(fontSize: 16, color: .green, cornerRadius: 8, padding: 8, fontWeight: .regular)
  static let large = Size(fontSize: 20, color: .blue, cornerRadius: 12, padding: 12, fontWeight: .bold)
}

この例はとても見通しがいいし、 enum と比べても機能的には問題ありません。むしろ、 struct の方が柔軟性があります。
例えば、 xLarge というケースを追加することを想像してみてください。 enum だと全てのプロパティに case を追加する必要がありますが、 struct だと xLarge だけを追加するだけでいいのです。

enum で定義したものが実は排他性のあるものではなく、ある struct のインスタンスではないかと想像すると、struct の方が適していることがあります。

こうした使い方は、Apple の SDK にもよくみられます。

enum はどこで使うべきか

次の3つに集約されます。

  1. 状態遷移を明確にしたいとき
  2. Associated Value が異なる型を持つとき
  3. 選択肢が限定されるとき

1.状態遷移を明確にしたいとき

特定の状態を表現する際に使うと、予期せぬ状態になることを防ぐことができます。

enum State {
  case idle
  case loading
  case loaded
  case error(Error)
}

2. Associated Value が異なる型を持つとき

異なる型のデータを1つの型として扱うときに便利です。 Swift では  case let によって Associated Value を取り出すことができます。

enum Result<Value, Error> {
  case success(Value)
  case failure(Error)
}

3. 選択肢が限定されるとき

これが一般的に使われるケースです。冒頭に例示したケースもここに該当します。

まとめ

今回例示したケースは struct の方がコードがスッキリするというだけで、パターンを網羅するような場合は enum の方が適しています。
とりあえず enum を利用して長いコードを書いているケースはよく現場で見かけるので、お手元の enum を見直してみるといいかもしれません。

Discussion

yuizumiyuizumi

Swift は書いたことがないので流儀はよくわからないのですが、enum と struct の合わせ技みたいなのは一般的ではないのでしょうか。

enum Size {
  case small
  case medium
  case large

  private var impl: SizeImpl {
    switch self {
      case .small:
        return SizeImpl.small
      case .medium:
        return SizeImpl.medium
      case .large:
        return SizeImpl.large
    }
  }

  var fontSize { return impl.fontSize }
  // ほかも同様
}

struct SizeImpl {
  // 型名を除いて本文中の struct Size に同じ
}

(ディスパッチがひとつ余計に入るのが嫌われる可能性はありそうです。あと impl より data と名付けた方が筋がいい?)

Daiki MatsudateDaiki Matsudate

Swiftではそのような型を検討する場合、enumにAssociated valueを持たせることによってenumが値を保持するようにする方が一般的です。

enum Size {
  case small(Config)
  ...
}

struct Config {
  let fontsize: CGFloat
}