enum と struct、どっちを選ぶべき?
この記事は 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つに集約されます。
- 状態遷移を明確にしたいとき
- Associated Value が異なる型を持つとき
- 選択肢が限定されるとき
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
Swift は書いたことがないので流儀はよくわからないのですが、enum と struct の合わせ技みたいなのは一般的ではないのでしょうか。
(ディスパッチがひとつ余計に入るのが嫌われる可能性はありそうです。あと impl より data と名付けた方が筋がいい?)
Swiftではそのような型を検討する場合、enumにAssociated valueを持たせることによってenumが値を保持するようにする方が一般的です。