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

Obj-CでJavaっぽいEnumを実現するためのINMEnumというライブラリを作った

Objective-Cenumが本当にintを列挙するだけのenumで、サーバークライアントのアプリケーションを書くときにサーバから文字列で送られてくる列挙型の値をクライアントで保持する書き方が苦痛だった。この辺りの問題はいろいろやりかたはありそうでこれが正解みたいなのはよくわからない。しかし、JavaにおいてはEnumが非常に柔軟にクラスのように使うことができて真似したくなった。

ainame/INMEnum

Cocoapodsでインストールできるはず。 使い方は簡単で・・・と言いたいところだけど、Objective-Cがもともとできないことをやろうとしているのでちょいちょい制約がある。 まず、INMEnumが提供するEnumの値はクラスのシングルトンのオブジェクトである。なので、特別な構文とかが提供されるわけでもなく、 普通にクラス定義から始める。

  • INMEnum ・・・ Enumの値を定義していくためのクラス
  • INMEnumCollection ・・・ Enumの値を束ねて扱うためのクラスでvalueForNameとかswitch:case:とか持ってる

こんなかんじでEnumを定義していく

@interface Sushi : INMEnum
@end;
@implementation Sushi
@end

@interface Tuna : Sushi
@end
@implementation Tuna
@end

@interface Egg : Sushi
@end
@implementation Egg
@end

@interface Shrimp : Sushi
@end
@implementation Shrimp
@end

@interface SushiGoRound : INMEnumCollection
@end
@implementation SushiGoRound
+ (NSArray *) values
{
    return @[
        [Tuna defineEnum:0 name:@"tuna" description:@":sushi:"],
        [Egg defineEnum:1 name:@"egg" description:@":egg:"],
        [Shrimp defineEnum:2 name:@"shrimp" description:@":fried_shrimp:"],
    ];
}
@end

最初にINEnumを継承したSushiクラスを作るのがキモで、さらにこれを継承したTunaやEggやShirmpはSushiクラスのサブクラスとなり、Sushiクラスの変数へ代入できるようになる。 また、SushiGoRound(回転寿司)というクラスのvaluesメソッドではTunaやEggやShrimpを実際に定義している。defineEnum:name:dscriptionというクラスメソッドが、各Enumのシングルトンオブジェクトを生成するためのDSLとなっている。なので、valuesが一度でも呼ばれると、TunaやEggやShrimpがシングルトンとして存在していくこととなる。

これらの値は以下のように使う。このINENumは、Objective-Cのランタイム上で生成されるただのオブジェクトなので、 初期化処理(上記の場合だとSushiGoRoundのvalues)を自分で実行しないと値が決定しない。 なので、AppDelegateやmain.mなどで一番最初に[INMEnumInitializer initializeAllEnumerateObjects]を必ず実行しておく。実装は読めばわかるけど全クラスをなめてINMEnumCollectionのサブクラスを探して初期化処理を読んでるだけ。

以下は、各クラスのクラスメソッドとしてenumObjectというメソッドを呼ぶとシングルトンのインスタンスがとれたり、 INEnumクラスのswitch:case:というメソッドが利用できたりする。swtich:case:の実装もやたらと手こずって実装したけど、 可変長引数の終端を取得することが難しくて、普通のswtich-case文におけるdefault節のようなものを用意し、 これを末尾に呼ぶことを強制することで実現している。

[INMEnumInitializer initializeAllEnumerateObjects]; // コードを利用するために絶対に呼ばないといけない。

Sushi *sushi = [Tuna enumObject];
sushi.ordinal; //=> 0
sushi.name; //=> @"tuna"
sushi.description; //=> @":sushi:"

sushi = [sushi valueForName:@"egg"] // => Egg object;
[SushiGoRound values]               // => Tuna, Egg and Shrimp instances as NSArray;

// INMEnum's swtich case syntax
[SushiGoRound switch:sushi
               cases:[Tuna then:^{ NSLog(@"awesome!"); }],
                     [Egg then:^{ NSLog(@"yummy!!"); }],
                     [Shrimp then:^{ NSLog(@"delicious!!!"); }],
                     [INMEnumCaseDefault then:^{ NSLog(@"WTF!"); }]]; // must set at last

SushiGoRoundというのは面白狙いでつけただけの名前なので、実際に使うならSushisみたいにEnumの型の複数形をつけてあげると具合が良さそう。 TODOとしては、swtich-caseの時にEnumが列挙しきれてない時にWarn出してあげるとかそういうの実装したら便利そう。