読者です 読者をやめる 読者になる 読者になる

SwiftのenumがObjective Cで欲しいときはどうするか

Swiftではenumという種類のデータ型を定義できます。Cのenumと違って、引数を持てるようになっています。これが便利なのは

  • ある状態では有効だけど、別の状態では有効ではない値を定義するとき

です(すごい雑な説明なので、他の使い方も調べておきましょう)。

Swiftenumとは

例えばあなたが最強のレジアプリを開発しているとしましょう。販売する商品の価格には次の二通りがあります。

  • 単価と税率が決まっているもの(単価¥100、税8%)
  • 全体の金額からの割合であるようなもの(10%割引)

enumを使わないで普通のクラスにしようとすると、こんな感じになるはずです。

class MenuItem {
  let isPercentage: Bool
  let unitPrice: NSDecimalNumber!
  let vatPercentage: NSDecimalNumber!
  let percentage: Int!
}

isPercentageが偽のときには、unitPricevatPercentageが設定されて、percentagenilになります。isPercentageが真のときはその逆。optionalがたくさんあって嫌な感じがしますし、いつか普通に間違えそうです。うっかりisPercentageな商品のunitPriceにアクセスしてエラーになるかもしれませんし、それを見た新人がisPercentageなのにunitPriceに0を設定するみたいな間違いを犯すかもしれません。少しでも状況を改善するためにはvalidationのコードを書くこともできますが、退屈なのでできるだけやりたくないですね。

こういうことを防ぐにはenumを使います。

enum MenuItemPrice {
  case Unit(price: NSDecimalNumber, vatPercentage: NSDecimalNumber)
  case Percentage(percentage: Int)
}

class MenuItem {
  let price: MenuItemPrice
}

optionalがなくなってやばそうな感じはなくなり、割引の項目の価格にはpriceがないので間違えてアクセスしようとするとコンパイラが教えてくれます。安心。priceの中身にアクセスしようとすると、必ずswitchが必要になって少し面倒ですが、諦めましょう。

switch price {
  case Unit(let p):
    ...
  case Percentage(let p):
    ...
}

ちなみにswitchを使うと、caseが網羅されているかもコンパイラがチェックしてくれるので、後で価格の種類が増えたときにも安心です。

Objective Cでどうするか

残った問題は一つだけで、2010年から開発されているあなたの最強のレジアプリはObjective Cで書かれているので、こういう良い感じのenumSwiftで定義してもそれをアプリケーションから使うことは出来ないということです。関連するコードだけでもSwiftで書き直せれば良いのですが、いろいろな事情でそうもいかないことは多いでしょう。

仕方がないのでこうします。

@interface MenuItemPrice : NSObject
@end

@interface UnitPrice : MenuItemPrice

@property (nonatomic) NSDecimalNumber *price;
@property (nonatomic) NSDecimalNumber *vatPercentage;

@end

@interface PercentagePrice : MenuItemPrice

@property (nonatomic) NSInteger percentage;

@end

@interface MenuItem

@property (nonatomic) MenuItemPrice *price;

@end

これで「単価が決まっている価格なのにうっかりpercentageにアクセスしてしまう」ことは防げるようになりました。

こうやって定義した値を使う方法はいくつかあります。

  1. MenuItemPriceメソッドを追加してサブクラスでオーバーライドする
  2. isKindOfClassして場合分けする

必要なメソッドがわかっている場合は、1.の方法が良いでしょう。例えば「価格を文字列の表現に変換する」みたいな操作は、それでいいと思います。私の場合は、さっさと諦めて、isKindOfClassで場合分けすることが多いですね。

if ([menuItem.price isKindOfClass:[UnitPrice class]]) {
  UnitPrice *price = (UnitPrice *)menuItem.price;
  ...
}

isKindOfClassのような操作はオブジェクト指向プログラミングでは避けるべきとされていますが、ここでは別にOOしたいわけではないので良いことにします。キャストが少し面倒なので、ヘルパーを足すことがあります。

@interface MenuItemPrice

- (void)asUnitPrice:(void (^)(UnitPrice *))block;
- (void)asPercentagedPrice:(void (^)(PercentagedPrice *))block;
- (void)switchWithUnitPriceCase:(void (^)(UnitPrice *))unitCase percentagedPriceCase:(void (^)(PercentagedPrice *))percentagedCase;

@end

もしも価格の種類が増えた場合にコンパイルエラーになるようにするため、switchWithUnitPriceCase:percentagedPriceCase:も入れてみました。

まとめ

Swiftenumって要するに代数的データ型 (Algebraic data type / ADT) なので、Objective Cに欲しい場合は、どうしてもOOPLでADTしたい場合のパターンで逃げることができます。