2015年2月1日日曜日

AnyObjectの欠点を補うクラス(全体設計編+本編)

以前に行なったAnyObjectの仕様確認で、IntやFloatを入れたときにis演算子が区別できないと判明しました。そのことを含めて、AnyとAnyObjectの違いや使い分けも検討しました。やはりAnyObjectのほうが便利であり、is演算子によるInt判定の欠点さえ解消されれば、今のところ最高の汎用データ型になれるはずです。

というわけで、欠点を補うためのクラスを作りました。そのクラスと、考慮した点を紹介します。

 

前回の投稿では、1つのクラスを用意して、IntやFloatの値を入れる形を考えました。要望を満たせる方法ではありますが、よくよく考えるとベストな方法ではありません。もっと良い方法を思いつきました。

その方法とは、Intだけを入れるクラス、Floatだけを入れるクラス、Boolだけを入れるクラスと、3つのクラスを用意する方法です。この方法なら、どのデータ型を入れているのか判断する必要はありません。入っているデータ型の判定処理が不要になることで、使い勝手は格段に良くなるはずです。

3つのクラスで実現する方法を思いついたのは、そもそも別なことを考えていた結果です。Intをクラスに変換できれば、AnyObjectに入れた後でis演算子を使うとき、そのクラスの名前で判定できるのではと思ったからです。しかし、いろいろ探してみたものの、そのような方法は見付かりませんでした(もし知っていたら教えてください)。仕方がないので、自分で作ればと考えたとき、実はそれこそが一番良い解決方法だと気付いたわけです。このような回り道する思考過程は、良くありますね。

 

非常に簡単なクラスですが、いつもどおりに全体設計を考えます。具体的に考えたほうが理解しやすいので、とりあえずIntを例に。

このクラスは、iOSが提供する様々なAPIと組み合わせて使うことになります。その場合、最も親となるNSObjectを継承したクラスとして作るのがベストです。一部のAPIと組み合わせて使えないというトラブルが防げますから。

値の入ってない状態を作らないのも大事なことです。初期化の際に値を指定する形とします。他に、値を設定したり、値を取得する機能も必要です。

このクラスのインスタンスをAnyObjectに入れた状態で、全体をシリアライズする機会も多いでしょう。そのためには、このクラス自体もシリアライズに対応する必要があります。

また、デバッグ目的で使う際の使い勝手を考慮し、インスタンスの文字列化にも対応させましょう。以上をまとめると、次のようになります。

AnyObjectを補うクラス(Int編)の要件
・iOSの各種APIと組み合わせやすいように、NSObjectを継承
・初期化するときに値を指定
・値を変更したり取得する機能
・シリアライズ可能に
・文字列化にも対応

こうして整理した要件を、もう少し具体的な機能として変換すると、次のようになります。

AnyObjectを補うクラス(Int編)の具体的な要素
・クラスの親子関係:NSObjectを継承
・クラス内の変数:入れるデータ(Int)だけ
・初期化:値(Int)を指定して
・メソッド:値(Int)の取得
・メソッド:値(Int)の設定
・シリアライズ可能に:NSCodingプロトコル準拠
・文字列化:Printableプロトコルを上書き

以上の要素を満たすように、コーディングするだけです。

 

まずはクラスの入れ物から。NSObjectを継承して、NSCodingに準拠するので、次のようなSwiftコードになりました。

// AnyObjectを補うクラス(Int編)
class IntObj : NSObject, NSCoding {
    var aoInt : Int = -99999

    init(_ rInt:Int) {
        super.init()
        aoInt = rInt
    }
}

クラス変数はIntだけで、宣言するときにダミーの値を入れています。初期化の処理では、入れる値を受け取って、変数に設定するだけです。簡単ですね。ちなみにaoはAnyObjectの略で、特別な意味はありません。

クラス名は少し悩みましたけど、Intをオブジェクトにしたという意味で、IntObjとしてみました。どんなクラスなのか、何となく伝わると思います。

 

続いて、値の取得と設定のメソッドです。これも非常に簡単です。

// 値の設定と取得
func setValueF(rInt:Int) {
    aoInt = rInt
}
func getValueF() -> Int {
    return aoInt
}

あまりに簡単すぎて、機能については説明することがありません。ただし、メソッド名に関してですが、getIntFとはせずに、getValueFとしました。他の2つのクラスと、メソッド名を統一したかったからです。

 

次は、文字列化を実現するため、Printableプロトコルへの準拠です。親クラスのNSObjectgが既に準拠しているため、上書きする形になります。

// Printableプロトコルを上書き
override var description: String { return aoInt.description }

ここでは、descriptionを使わずにString(aoInt)とも書けます。しかし、実現する機能に近い書き方が良いと考え、descriptionを使いました。

 

最後は、シリアライズを可能にするNSCodingプロトコルへの準拠です。これも短く書け、次のようなSwiftコードになりました。

// NSCodingプロトコル準拠
func encodeWithCoder(rCoder:NSCoder) {
    rCoder.encodeInteger(self.aoInt, forKey:"aoInt")
}
required init(coder rDecoder:NSCoder) {
    self.aoInt = rDecoder.decodeIntegerForKey("aoInt")
}

整数型を扱うエンコードやデコードは、それぞれ複数ありました。Swiftには整数型が何種類もあるからでしょう。API資料で比べ、Int型を対象にしたものを選びました。

 

クラスのSwiftコードは、以上で終わりです。全体が短いソースコードなので、今回だけは全部含めた形で再掲します。なお、ソースコード内の並び順は、説明した順番とは違っています。

// AnyObjectを補うクラス(Int編)
class IntObj : NSObject, NSCoding {
    var aoInt : Int = -99999

    init(_ rInt:Int) {
        super.init()
        aoInt = rInt
    }
    // NSCodingプロトコル準拠
    func encodeWithCoder(rCoder:NSCoder) {
        rCoder.encodeInteger(self.aoInt, forKey:"aoInt")
    }
    required init(coder rDecoder:NSCoder) {
        self.aoInt = rDecoder.decodeIntegerForKey("aoInt")
    }
    // 値の設定と取得
    func setValueF(rInt:Int) {
        aoInt = rInt
    }
    func getValueF() -> Int {
        return aoInt
    }
    // Printableプロトコルを上書き
    override var description: String { return aoInt.description }
}

全体としても、かなり短いソースコードです。もともと機能の少ないクラスですし、対応したプロトコルも少しだけですから。

 

Float型のクラスの場合も、Int型とほぼ同じです。Swiftコードは、次のようになりました。

// AnyObjectを補うクラス(Float編)
class FloatObj : NSObject, NSCoding {
    var aoFloat : Float = -9999.9

    init(_ rFloat:Float) {
        super.init()
        aoFloat = rFloat
    }
    // NSCodingプロトコル準拠
    func encodeWithCoder(rCoder:NSCoder) {
        rCoder.encodeFloat(self.aoFloat, forKey:"aoFloat")
    }
    required init(coder rDecoder:NSCoder) {
        self.aoFloat = rDecoder.decodeFloatForKey("aoFloat")
    }
    // 値の設定と取得
    func setValueF(rFloat:Float) {
        aoFloat = rFloat
    }
    func getValueF() -> Float {
        return aoFloat
    }
    // Printableプロトコルを上書き
    override var description: String { return aoFloat.description }
}

見てのとおりInt型とほぼ同じなので、説明は不要でしょう。

Int型と同様に、NSCodingプロトコル準拠では、浮動小数点型を扱うエンコードとデコードがそれぞれ複数ありました。API資料を見て、Float型となっているメソッドを選んでいます。

 

続いて、Bool型のクラスです。次のようなSwiftコードになりました。

// AnyObjectを補うクラス(Bool編)
class BoolObj : NSObject, NSCoding {
    var aoBool : Bool = false
 
    init(_ rBool:Bool) {
        super.init()
        aoBool = rBool
    }
    // NSCodingプロトコル準拠
    func encodeWithCoder(rCoder:NSCoder) {
        rCoder.encodeBool(self.aoBool, forKey:"aoBool")
    }
    required init(coder rDecoder:NSCoder) {
        self.aoBool = rDecoder.decodeBoolForKey("aoBool")
    }
    // 値の設定と取得
    func setValueF(rBool:Bool) {
        aoBool = rBool
    }
    func getValueF() -> Bool {
        return aoBool
    }
    // Printableプロトコルを上書き
    override var description: String { return aoBool.description }
}

これもまた、Int型とほぼ同じです。

 

以上、3つのクラスを作りましたが、どれも最低限の機能しか持たせていません。いつものように、何か追加が必要になったら、そのときに追加すればよいと思います。

長くなったので、これらクラスの使用例は次回にて。

 

(使用開発ツール:Xcode 6.1.1, SDK iOS 8.1)

0 件のコメント:

コメントを投稿