※この記事はSwift by Sundellの内容を日本語に翻訳したものです。
概要
Swift の enum の実装は、間違いなく、この言語が提供しなければならない最も人気があり、強力な機能の1つです。Swift列挙型は、整数ベースの定数の単純な列挙型をはるかに超えており、関連する値や高度なパターンマッチングなどをサポートしているため、さまざまな種類の問題を解決するための優れた候補になります。
ただし、特定の種類の列挙型の場合は、避けたほうがよい場合があります。これは、トリッキーな状況に陥ったり、コードが意図したよりも「慣用的」でないと感じたりする可能性があるためです。いくつかのそのようなケースと、Swift の他の言語機能のいくつかを使用してそれらをリファクタリングする方法を見てみましょう。
価値がないことを表す
例として、ポッドキャスト アプリに取り組んでおり、列挙型を使用してアプリがサポートするさまざまなカテゴリを実装したとします。その列挙型には現在、各カテゴリのケースと、カテゴリをまったく持たないポッドキャストに使用される 2 つのやや特殊なケース (none)、およびすべてのカテゴリを一度に参照するために使用できるカテゴリ(all) が含まれています。
extension Podcast {
enum Category: String, Codable {
case none
case all
case entertainment
case technology
case news
...
}
}
次に、フィルタリングなどの機能を実装するときに、上記の列挙型を使用して、ユーザーが UI 内(Filter
モデル内にカプセル化されている)で選択した値Category
に対してパターンマッチングを実行できます。
extension Podcast {
func matches(filter: Filter) -> Bool {
switch filter.category {
case .all, category:
return name.contains(filter.string)
default:
return false
}
}
}
一見すると、上記の2つのコードは完全に正常に見えるかもしれません。しかし考えてみると、Swift にはオプショナルのような目的に合わせて作られた組み込みの言語機能があることを考えると、カテゴリの欠如を表す特定のケースnone
を現在追加しているという事実は、おそらく少し奇妙です。
したがって、代わりにPodcast
モデルのcategory
プロパティをオプショナルに変更すると、欠落しているカテゴリを全く問題なく表すことができるようになります。さらに、そのような欠落値を処理する場合、Swift のオプショナルがサポートするすべての機能 (if let
ステートメントなど) を利用できるようになりました。
struct Podcast {
var name: String
var category: Category?
...
}
上記の変更について本当に興味深いのは、Podcast.Category値で以前使用していた網羅的なswitch
ステートメントが、以前と同じように機能し続けることです。これは、オプショナル型自体も実際には値の欠如を表すためにnone
ケースを使用する列挙型であることが判明したためです。—つまり、次の関数のようなコードは完全に変更されないままである可能性があります(引数をオプショナルに変更することを除いて)。
func title(forCategory category: Podcast.Category?) -> String {
switch category {
case .none:
return "Uncategorized"
case .all:
return "All"
case .entertainment:
return "Entertainment"
case .technology:
return "Technology"
case .news:
return "News"
...
}
}
上記は、オプショナルがパターンマッチングコンテキスト(switch
ステートメントなど)で使用されるときにオプショナルを自動的にフラット化するSwiftコンパイラの魔法のおかげで機能します。これにより、オプション型自体のケースと、独自のPodcast.Category
列挙型内で定義されたケースの両方をすべて同じステートメント内で処理できます。
これらは上記のタイプの状況では機能的に同じであるため、必要に応じて
case nil
の代わりにcase .none
を使用することもできます。
ドメイン固有の列挙
次に、Podcast.Category
列挙型のall
の場合に注意を向けましょう。これも、考えてみると少し奇妙です。結局、ポッドキャストは同時にすべてのカテゴリに属することはできないため、そのall
ケースはフィルタリングのコンテキスト内でのみ意味があります。
したがって、そのケースをメインCategory
列挙型に含めるのではなく、フィルタリングのドメインに固有の専用の型を作成しましょう。そうすれば、関心の分離を非常にうまく行うことができ、ネストされた型を使用しているため、新しい列挙型に同じCategory
ネームを使用させることができますが、今回はFilter
モデル内でネストされます— 次のように:
extension Filter {
enum Category {
case any
case uncategorized
case specific(Podcast.Category)
}
}
ここでもオプショナルのアプローチを使用することを選択できたことは注目に値します。
nil
はany
またはuncategorized
のいずれかを表しますが、この場合は2つの候補が存在する可能性があるため、ここでは専用のケースを使用することで、間違いなく意図をより明確にしています。
上記のアプローチの本当に優れている点は、Swiftのパターンマッチング機能を使用してフィルタリングロジック全体を実装できることです。つまり、フィルタリングされたカテゴリをオンにしてから、where
句を使用して各ケースに追加のロジックをアタッチします。
extension Podcast {
func matches(filter: Filter) -> Bool {
switch filter.category {
case .any where category != nil,
.uncategorized where category == nil,
.specific(category):
return name.contains(filter.string)
default:
return false
}
}
}
上記のすべての変更が行われたので、メインのPodcast.Category
列挙型からnone
とall
ケースを削除できます。これにより、アプリがサポートする各カテゴリのはるかに簡単なリストが残ります。
extension Podcast {
enum Category: String, Codable {
case entertainment
case technology
case news
...
}
}
カスタムケースとカスタムタイプ
Podcast.Category
のような列挙型に関しては、(ある時点で)1回限りのケースを処理するために使用できる、または将来的にサーバーサイドに追加される可能性のあるケースを適切に処理することによって上位互換性を提供するために、ある種のカスタムケースを導入することは非常に一般的です。
これを実装する方法の1つは、値が関連付けられているケースを使用することです。この場合、次のように、カスタムカテゴリのRaw 値を表すString
を使用します。
extension Podcast {
enum Category: Codable {
case all
case entertainment
case technology
case news
...
case custom(String)
}
}
関連付けられた値は他のコンテキストでは非常に便利ですが、残念ながら、これは実際にはそれらの1つではありません。まず、このようなケースを追加することで、列挙型を String
でバックアップできなくなります。つまり、カスタムのエンコードとデコードのコード、およびインスタンスを Raw文字列との間で変換するためのロジックを作成する必要があります。
そこで、代わりにCategory
列挙型をRawRepresentable
構造体に変換することで、別のアプローチを検討しましょう。これにより、Swiftの組み込みロジックを利用して、このようなタイプの文字列変換をエンコード、デコード、および処理できます。
extension Podcast {
struct Category: RawRepresentable, Codable, Hashable {
var rawValue: String
}
}
必要なカスタム文字列からCategory
インスタンスを自由に作成できるようになったため、追加のコードを必要とせずに、カスタムカテゴリと将来のカテゴリの両方を簡単にサポートできます。ただし、コードの下位互換性を維持し、組み込みの現在知られているカテゴリを簡単に参照できるようにするために、これらすべてを実現する静的APIを使用して新しいタイプを拡張しましょう。
extension Podcast.Category {
static var entertainment: Self {
Self(rawValue: "entertainment")
}
static var technology: Self {
Self(rawValue: "technology")
}
static var news: Self {
Self(rawValue: "news")
}
...
static func custom(_ id: String) -> Self {
Self(rawValue: id)
}
}
上記の変更では、ある程度の追加コードを追加する必要がありましたが、最終的には、ほぼ完全に下位互換性のある、はるかに柔軟なセットアップになりました。実際、私たちが行う必要がある唯一の更新は、Category
値に対して徹底的な切り替えを実行するコードを行うことです。
たとえば、以前に調べた title
関数は、そのような値をオンにして、特定のカテゴリに一致するタイトルを返しました。コンパイル時に各 Category
値の完全なリストを取得できなくなったため、これらのタイトルを計算するために別のアプローチを使用する必要があります。この特定のケースでは、たとえば、これらの文字列を Localizable.strings
ファイルに移動し、次のようにタイトルを解決する絶好の機会と見なすことができます。
func title(forCategory category: Podcast.Category?) -> String {
guard let id = category?.rawValue else {
return NSLocalizedString("category-uncategorized", comment: "")
}
let key = "category-\(id)"
let string = NSLocalizedString(key, comment: "")
// Handling unknown cases by returning a capitalized version
// of their key as a fallback title:
guard string != key else {
return key.capitalized
}
return string
}
もう1つのオプションは、Category
タイプ自体の中でローカライズされたタイトルを解決することです。また、オプションの`titleプロパティを追加して、アプリがまだネイティブにサポートしていないカスタムカテゴリのローカライズされたタイトルをサーバーが送信できるようにすることもできます。
自動命名静的プロパティ
簡単なボーナスのヒントとして、上記の構造体ベースのアプローチの1つの欠点は、静的プロパティごとに基になる文字列のRaw値を手動で定義する必要があることですが、これはSwiftの #function
キーワードを使用して解決できます。そのキーワードは、カプセル化関数が呼び出されている関数(またはこの場合はプロパティ)の名前に自動的に置き換えられるため、列挙型を使用する場合と同じ自動Raw値マッピングが得られます。
extension Podcast.Category {
static func autoNamed(_ rawValue: StaticString = #function) -> Self {
Self(rawValue: "\(rawValue)")
}
}
上記のユーティリティを使用すると、組み込みの各カテゴリAPI内でautoNamed()
を呼び出すだけで、SwiftがこれらのRaw値を自動的に入力します。
extension Podcast.Category {
static var entertainment: Self { autoNamed() }
static var technology: Self { autoNamed() }
static var news: Self { autoNamed() }
...
static func custom(_ id: String) -> Self {
Self(rawValue: id)
}
}
ただし、#function
ベースの手法を使用する場合は、上記の静的プロパティの名前を変更しないように少し注意する必要があります。名前を変更すると、そのプロパティのCategory
の基になるRaw値も変更されるためです。ただし、これは列挙型を使用する場合にも当てはまります。逆に、各 Raw文字列を手動で定義するときに発生する可能性のあるタイプミスやその他の間違いも防止できるようになります。
終わりに
Swift列挙型は素晴らしいです(実際、そのトピックだけで15以上の記事を書いています)が、構築しようとしているものに対して別の言語メカニズムの方が適している場合もあります。プロジェクトが成長し進化するにつれて、いくつかの異なるメカニズムとアプローチを切り替える必要があるかもしれません。
元の記事
Avoiding problematic cases when using Swift enums Swift by Sundell