2015年2月26日木曜日

言語やAPIの仕様確認がかなり重要

ここまでの何回かの投稿で、Swift言語やiOS APIの仕様確認の話を書きました。良い機会なので、仕様確認に関する話を少し突っ込んで書いてみます。

 

まず、仕様確認という用語から。残念ながら、仕様確認という呼び方は、一般的ではないと思います。おそらく、私が勝手に使っているだけではないでしょうか。つまり、私の個人的な呼び名です。

わざわざ特別な名前をつけて呼んでいる理由ですが、何か名前をつけないと説明しづらいので、とりあえず付けたという感じです。勝手に付けた名前ではありますが、意味は上手に伝わると思っています。

肝心の「仕様確認」の意味を説明しなくてはダメですね。公開されている言語仕様やAPI資料には、細かな部分までは記載されていません。その細かな部分が「実際にはどう動くのか」を、具体的なコードを書いて動かし、調べる行為のことです。より正確に表現するなら「詳細仕様確認」という感じでしょうか。

もちろん、何から何まで調べるわけではありません。自分が気になった箇所、または疑問を感じた箇所だけ、絞り込んで確認するだけです。

 

言語やAPIの資料で、細かな部分まで記載されていないのには理由があります。おそらく一番大きい理由は、作業量が膨大になることでしょう。

例として、is演算子の場合を考えてみましょう。仕様を簡単に書くとしたら、次のようになるでしょうか。データ型を調べるための演算子で、調べる対象となるものを演算子の左側に置いて、データ型を右側に入れます。左側のデータ型が、右側のデータ型と一致すればtrueを、一致しなければfalseを返します。以上のように、非常に簡単に書けて、それなりに意味は伝わります。

しかし、これでは厳密な仕様とは言えません。まず、左側に入れられるものの範囲、同じく右側に入れられる範囲が不明確です。また、データ型が一致するとは、どの範囲まで含まれるかも不明確です。また、左右の組み合わせによって、一致する範囲の説明が異なることも考えられます。これらを明確に規定しないと、厳密な仕様にはなりません。ここまでの説明を読んだだけで、きっちりと説明するのが如何に大変なのか想像できると思います。is演算子だけでこうですから、全部の機能を同じように説明したら、物凄い量の文章量となり、その作業量も膨大になると推測できるでしょう。

もう1つの理由としては、正確に書ける人が非常に少ない点も挙げられます。細かな部分まで正しく理解できるだけでなく、分かりやすく正確に書ける文章力も必要です。おそらく、普通の人には難しいでしょう。

他にも理由はあるでしょうが、結果として、現状のような「簡単な説明だけ」で落ち着いているわけなのです。おそらく「細かな動きは、動かして調べられるから、自分で調べてよ」という考え方なのでしょう。現状の形が「現実的な落としどころ」だと私も思います。

 

現状の資料が細かな仕様まで含まれていない以上、本格的に使う場合は、実際の動作結果を自分で調べるしかありません。「こう動くだろう」と考えてソースコードを書きますが、本当に「こう動く」かどうかは、試してみないと何とも言えません。

今までの経験上、多くの場合は期待した通りに動くものの、たまに期待どおりに動かないこともあると知っています。だからこそ、できるだけ事前に仕様確認して、正確な動きを知っておくようになったわけです。

実際には、何から何まで仕様確認しているわけではありません。大抵の場合は、次のような流れになります。まず最初に、作りたい機能をiOS実験専用アプリ上で作ります。その際に、作った機能をテストするわけですが、正常な動作のほかに、エラーになる動作も含めます。それら両方が期待どおりの結果なら、仕様確認する必要はありません。

ところがまれに、期待どおりの結果が得られないこともあります。当然ながら最初は、自分のプログラムのバグだと思います。でも、いくら直してもバグが消えず、どう考えてもバグがなさそうとだと感じたあたりで、仕様確認を試そうと考え始めます。どの辺の機能が期待どおりに動いてなさそうなのか、バグとして現れている実行結果から予想して、ターゲットとなる機能を絞り込みます。

絞り込めたら、仕様確認のコードを作ります。同じiOS実験専用アプリ内の別な場所(通常は隣の場所)に、仕様確認の機能を割り当てて、そこに作り込みます。作ったコードを実行すれば、API資料に書かれていない仕様を知ることになります。得られた実行結果によって、元のコードを変更するか、別な機能を使って実現するか、今後の方針を決めるわけです。

作りたい機能のコードも、仕様確認のコードも、同じiOS実験専用アプリ内に保存されます。たいていは隣に入れておきますから、APIのバージョンアップなどで細かな仕様が変更された場合でも、連続して実行できて便利です。

期待どおりの実行結果でなかった場合は、その結果を何かの形で残すことも大事です。実行結果は保存されませんから、対象となる仕様確認ソースコードの中に、コメントとして記録しておきます。XcodeやAPIのバージョン番号とともに。

 

以上の例とは違って、仕様確認を先に行なう場合もあります。

作りたい機能を実現するために、どのAPIが使えるか調べているとき、細かな仕様が不明とか、使い方が不明というケースが意外に多くあります。実際に動かしてみないと、本当に使えるかどうか分からない場合は、仕様確認のためにコードを用意して調べます。この場合も、iOS実験専用アプリを使って、その中に作ります。

仕様確認の結果、そのAPIが使えると判断したときは、作りたい機能を同じiOS実験専用アプリ内で作ります。前の例と同じように、仕様確認コードの隣に作って、近くに入っている状態にします。作る順序は逆ですが、作られた状態は似たような形となります。

 

さらには、作りたい機能とは関係なく、仕様確認を行なう場合もあります。

どのような機能でも、特別な条件のときに正しく動作するか、確認してみたくなるケースがあります。特定の機能を並列で複数動かしたときに正しく動くかとか、一般的ではない使い方での動作です。

このような疑問を生じた場合は、やはり仕様確認を行ないます。iOS実験専用アプリ内に確認用コードを追加し、実行させて確かめます。こちらの場合には、作りたい機能のコードはなく、仕様確認のコードだけが保存されます。

 

作りたい機能も仕様確認も、一緒のiOS実験専用アプリに入れるのには、もう1つの理由があります。

一番大きいのは、作る手順でしょう。新しい機能をアプリ内に追加する場合、非常に簡単な機能でない限り、アプリ内で作り始めることはありません。まずiOS実験専用アプリ内で作って、本当に作れるかどうか試します。その後、ライブラリ化できないか考えたり、より共通で使える形に直したりします。そのような過程を経てから、アプリ内にコードをコピーするのが通常の流れです。

結果として、作った機能が再利用しやすく整えられることはもちろん、テストするコードもiOS実験専用アプリに残ります。アプリへ持って行くのは、テストまで終わったコードだけです。

このような手順でアプリを作ると、アプリ内だけで作るコードは、アプリ独自の機能だけに限られます。また、テスト用のコードも、アプリ独自の機能だけに絞られます。

 

Swiftを使い始めた頃は、iOS実験専用アプリを持っていませんでした。そのため、最初に作ったアプリでは、アプリ独自の機能以外のコードも多く含まれています。

Swiftに少し慣れたころ、iOS実験専用アプリを思い付いて用意しました。それからは、新規の機能はiOS実験専用アプリで作り、作り終わったらアプリへ持って行く形にしています。また、過去に作ったアプリの機能のうち、独自ライブラリの形で作れそうなものは、少しずつライブラリ化を進めています。

最初の頃に作ったアプリも、バージョンアップの際に、新しい独自ライブラリに入れ替える予定です。

 

ここまで、仕様確認について色々と書きました。仕様確認の必要性だけでなく、確認用のコードを残すことや、iOS実験専用アプリとの関係まで含まれます。

最初に述べたように、言語やAPIの資料には細かな仕様が書いてありません。自分で実行して確かめ、細かな仕様を調べる必要があります。

細かな仕様を知ることは、APIで提供される機能を正しく理解することになり、気付かないバグを減らすことにも繋がります。アプリの信頼性を向上させるためにも、自分のスキルをアップさせるためにも、疑問に思った点は仕様確認してみてください。

2015年2月3日火曜日

AnyObjectの欠点を補うクラス(使用例編)

AnyObjectの欠点を補うクラスの続きです。前の投稿では3つのクラスを作りましたので、今回は使い方を紹介します。もともとAnyObjectへ入れて使うのが作成目的のため、紹介するのはAnyObjectへ入れる例だけとなります。

 

最初の例では、AnyObjectの配列に入れてみましょう。一般的なアプリの中での使用例ではなく、動作を見るための使用例となります。

// ========== 使用例:配列
// 変数の準備
var iIntObj : IntObj = IntObj(12)
var iFloatObj : FloatObj = FloatObj(12.34)
var iBoolObj : BoolObj = BoolObj(true)
var iStr : String = "sample"
println("変数:i=\(iIntObj),f=\(iFloatObj),b=\(iBoolObj)")
// 配列の準備
var aryAnyObj : [AnyObject] = [AnyObject]()
aryAnyObj.append(iIntObj)
aryAnyObj.append(iFloatObj)
aryAnyObj.append(iBoolObj)
aryAnyObj.append(iStr)
// 型ごとの処理
for iObj in aryAnyObj {
    switch iObj {
    case is IntObj:
        let iInt : Int = (iObj as IntObj).getValueF()
        println("IntObj=\(iInt)")
    case is FloatObj:
        let iFloat : Float = (iObj as FloatObj).getValueF()
        println("FloatObj=\(iFloat)")
    case is BoolObj:
        let iBool : Bool = (iObj as BoolObj).getValueF()
        println("BoolObj=\(iBool)")
    case is String:
        let iStr9 : String = iObj as String
        println("String=\(iStr9)")
    default: break
    }
}

まず最初、AnyObjectへ入れる前に、それぞれのクラスの変数として用意します。入れたい値を渡して、単純にインスタンス生成するだけです。

生成後には値を表示しています。3つのクラスとも、Printableプロトコルに準拠させました。そのため、printlnで変数名を指定するだけで、値を表示できます。デバッグするときなどに便利ですね。

続いてAnyObject配列を用意して、最初に生成した変数を順番に追加するだけです。

最後はfor文で、配列からAnyObjectを順番に取り出します。今回は、データ型の判定にswitch文を使いました。caseに続くis演算子でクラス名を指定すると、簡単に判定できます。クラス名だけ書けば良いので、Intなどの基本的な型と同じように使えるのが良い点です。

ただし値を取り出すときには、getValueFメソッドが必要です。AnyObjectを該当クラスにキャストした後、そのメソッドで値を取り出します。それぞれのgetValueFメソッドでは、戻り値として型が指定されるので、Swiftの型チェックが有効に活かされます。

 

上記コードの実行結果は、次のとおりです。

変数:i=12,f=12.34,b=true
IntObj=12
FloatObj=12.34
BoolObj=true
String=sample

is演算子できちんと判定され、getValueFメソッドで値が取り出されていることが分かります。また最初の行では、plintlnで変数名だけから値が表示されるのも確認できます。

 

次は、Dictionary内のAnyObjectへ入れる場合の例です。これもまた、一般的なアプリの中での使用例ではなく、動作を見るサンプルとしての使用例となります。

// ========== 使用例:Dictionary
// 変数の準備
var iIntObj : IntObj = IntObj(12)
var iFloatObj : FloatObj = FloatObj(12.34)
var iBoolObj : BoolObj = BoolObj(true)
var iStr : String = "sample"
println("変数:i=\(iIntObj),f=\(iFloatObj),b=\(iBoolObj)")
// Dictionaryの準備
var iDic : Dictionary<String,AnyObject> = [String:AnyObject]()
iDic["aInt"] = iIntObj
iDic["aFloat"] = iFloatObj
iDic["aBool"] = iBoolObj
iDic["aString"] = iStr
println("dic=\(iDic)")
// シリアライズ(書き出し)
let iPathDocs : AnyObject = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let iPathFile : String = iPathDocs[0].stringByAppendingPathComponent("dicAnyObjPlus.plist")
let iResult = NSKeyedArchiver.archiveRootObject(iDic, toFile:iPathFile)
if !iResult { println("ERROR:ファイル書き失敗:dicAnyObjPlus") }
// シリアライズ(読み込み)
let iFileMgr : NSFileManager = NSFileManager.defaultManager()
if !(iFileMgr.fileExistsAtPath(iPathFile)) { println("ERROR:ファイルが存在せず"); return }
let iDic2 = NSKeyedUnarchiver.unarchiveObjectWithFile(iPathFile) as Dictionary<String,AnyObject>
let iInt2 : Int = (iDic2["aInt"] as IntObj).getValueF()
let iFloat2 : Float = (iDic2["aFloat"] as FloatObj).getValueF()
let iBool2 : Bool = (iDic2["aBool"] as BoolObj).getValueF()
let iStr2 : String = iDic2["aString"] as String
println("dic:i=\(iInt2),f=\(iFloat2),b=\(iBool2),s=\(iStr2)")

最初の変数は、前の例と同じです。

続いて、それらをAnyObjectのDictionaryへ、キーを指定しながら入れます。入れた後で、Dictionaryごとprintln文で表示しています。

後半の処理は、Dictionaryのシリアライズです。Dictionaryのまま、Documentフォルダへ書き出します。

以前は、DictionaryをNSDictionaryへキャストして、writeToFileメソッドで書き出しました。しかし今回は、その方法が使えません。writeToFileメソッドが対応しているのは、限られたデータ型のみだからです。今回のクラスは、その限られたデータ型に含まれていないので、NSKeyedArchiverクラスのarchiveRootObjectメソッドを使いました。

最後の部分は、書き出したファイルが存在しているかチェックし、存在している場合にだけ読み込む処理です。読み込んだデータを別なDictionaryに代入して、個々の値を取り出してます。どの値の取り出しでも、Dictionary内のAnyObjectを各クラスにキャストし、getValueFメソッドで値を取得するのが基本です。

 

上記コードの実行結果は、次のようになりました。

変数:i=12,f=12.34,b=true
dic=[aString: sample, aFloat: 12.34, aInt: 12, aBool: true]
dic:i=12,f=12.34,b=true,s=sample

シリアライズは成功し、読み込んだ結果も正常でした。

 

3つのクラスを実際に作ってみると、使い方は意外に簡単だと分かりました。

Int型とIntObjクラスで使い方を比べたとき、is演算子では同じような書き方となります。println文での使い方も似たようなものです。変数への代入や取り出しでメソッドが必要となりますが、それほど面倒ではありません。これでis演算子の判定結果が期待どおりになるのですから、意外に価値はあると思います。

一番最初に考えたのは、1つだけのクラスとして作り、入れたデータ型を情報として持つ方法でした。この方法を採用していたら、今回のように使いやすくはなりませんでした。前回書いたように遠回りしましたが、最終的には良い結果が得られたのではないでしょうか。

これぐらい簡単に使えるなら、以前にライブラリ化した環境設定機能でも使いたくなりました。そのうち、改良しようと思います。

実は、今回のクラスが、Int型よりも良いかもしれない点が1つだけあります。アップルの技術資料には、AnyObjectはオブジェクトのインスタンス用と書いてあり、Int型の値が入れられるのは保証されていません。Swiftのバージョンアップにより、突然と使えなくなる可能性も残っています。そんな場合でも、今回のクラスはインスタンスを生成するので、使い続けられるというわけです。まあ、Int型が入れられなくなる状況には、ならないと思いますけど。

 

AnyObjectの欠点を補うクラスができたことで、AnyObjectの代わりにAnyを使う必要性がほとんどなくなりました。Anyを使うのは、Anyが本当に必要な場合だけでしょう。これからは、今までよりも安心してAnyObjectが使えます。

 

今回の3つのクラスは、独自ライブラリの中で、もっとも基本的な機能を実現するクラスと位置づけられます。そのため、既存の「BC_Base.swift」に入れました。このファイルには、以前に紹介した「数秒後に自動で消えるメッセージ表示」で作ったクラスも入れてあります。

今回作ったクラスも、新しくアプリを作るとき、真っ先にコピーする独自ライブラリとして活躍してくれるでしょう。

 

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

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)