以前の投稿で、APIの仕様確認の話を書きました。SwiftやiOS APIのことを、まだ深くは理解していないので、少しずつですが仕様確認を進めています。ちなみに仕様確認とは、APIなどの記述が詳細まで含んでいないため(SwiftやiOSに限らず、一般的なことです)、詳細の動きを調べようと、実際のコードを動作させて確かめる作業のことを意味します。
AnyObjectの仕様確認を行なったとき、面白い結果が得られたので、もう少し実験してみました。AnyObjectと似たようなAnyも一緒に。そうしたら面白い結果が得られたので、ここに紹介します。
最初にAnyとAnyObjectの違いをネットで調べたのですが、曖昧な記述が多くて、それぞれの明確な仕様は得られませんでした。かろうじて得られた情報から、大まかには次にような感じだと思います。
AnyもAnyObjectも、いろいろな種類の型を入れられる汎用的なデータ型です。このうちAnyObjectは、クラスのインスタンスを入れるのが役割のようです。もう片方のAnyは、より広いデータ型を入れられ、関数も含むほとんどのデータ型が入れられるようです。
とは言うものの、AnyObjectにもIntなどのデータ型が入れられますし、そういった例が探せば見付かります。また、実際に試してもコンパイルエラーにならず、実行させても正常な様子で動きます。こうなると、違いがますます不明になりますね。
以前に紹介した開発例では、AnyObjectに入れたデータでの、is演算子での動きが期待どおりではありませんでした。そこで、同類であるAnyの動作結果も知りたくなり、実際に実行させて試したというわけです。
それぞれに入れるデータ型としては、Int、Float、Bool、Stringの4つを用意しました。それぞれのデータをAnyとAnyObjectの変数に入れて、is演算子での判定結果がどうなるかという試験です。具体的なSwiftコードは次のようにしました。
// AnyとAnyObjectで、is演算子の判定結果を調べる試験(配列)
// 変数の準備
let aryTypeName : [String] = ["Int", "Float", "Bool", "String", "Error!!!"]
let iInt : Int = 5
let iFloat : Float = 3.3
let iBool : Bool = false
let iStr : String = "test"
var aryAny : [Any] = [Any]()
var aryAnyObj : [AnyObject] = [AnyObject]()
aryAny.append(iInt)
aryAny.append(iFloat)
aryAny.append(iBool)
aryAny.append(iStr)
aryAnyObj.append(iInt)
aryAnyObj.append(iFloat)
aryAnyObj.append(iBool)
aryAnyObj.append(iStr)
// is演算子のテスト
println("Test:Any - (Int Float Bool String)\n")
for i in 0..<aryAny.count {
let iBoolInt : Bool = aryAny[i] is Int
let iBoolFloat : Bool = aryAny[i] is Float
let iBoolBool : Bool = aryAny[i] is Bool
let iBoolStr : Bool = aryAny[i] is String
println("\(i) \(aryTypeName[i]) -- > \(iBoolInt) \(iBoolFloat) \(iBoolBool) \(iBoolStr) \n")
}
println("\nTest:AnyObject - (Int Float Bool String)\n")
for i in 0..<aryAnyObj.count {
let iBoolInt : Bool = aryAnyObj[i] is Int
let iBoolFloat : Bool = aryAnyObj[i] is Float
let iBoolBool : Bool = aryAnyObj[i] is Bool
let iBoolStr : Bool = aryAnyObj[i] is String
println("\(i) \(aryTypeName[i]) -- > \(iBoolInt) \(iBoolFloat) \(iBoolBool) \(iBoolStr) \n")
}
難しい処理ではないので、見て簡単に理解できると思いますが、注意した点だけ書きます。
変数aryTypeNameでは、5番目の要素として、エラー表示の文字列を含めました。これを表示したときは何か間違っていることを意味し、異常終了させずに気付かせるのが目的です。異常終了が嫌いなので、このような作り方をよくやります。
AnyとAnyObjectへ入れる値は、データ型を確定させるために、型を指定した変数に入れました。こうすることで、間違いなくデータ型が確定できます。また、実際のコードでも変数に入れた値を代入することがほとんどでしょうから、実際のコードに近い条件で試験することにもなります。
AnyとAnyObjectの変数は、配列にしました。こちらも、配列の形で使うことが多いと思ったからです。もしかしたら、私だけかもしれませんが。
is演算子の判定テストでは、すべての組み合わせを実行しています。AnyとAnyObjectのそれぞれで、配列に入れた1つのデータに対して、is演算子で4種類のデータ型を順番に判定しました。配列の1要素ごとに、1行の実行結果が得られます。
println文には、実行結果が少しでも見やすくなるように改行を入れてあります。最初は入れてなかったのですが、試しに入れたらだいぶ見やすくなったので、この形で確定しました。
上記のコードを実行させて、得られた結果(コンソールへの出力)は次のとおりです。
Test:Any - (Int Float Bool String)
0 Int -- > true false false false
1 Float -- > false true false false
2 Bool -- > false false true false
3 String -- > false false false true
Test:AnyObject - (Int Float Bool String)
0 Int -- > true true true false
1 Float -- > true true true false
2 Bool -- > true true true false
3 String -- > false false false true
見てのとおり、AnyとAnyObjectでは違う結果となりました。
Anyに入れたデータは、is演算子の判定結果が期待どおりになっています。IntとFloatとBoolは区別され、一致しているときだけtrueを返します。つまり、データ型の判定が正しく行なわれているということです。
AnyObjectに入れたデータは、is演算子の判定結果が期待どおりではありません。IntとFloatとBoolは区別されず、それらのどの組み合わせでもtrueを返します。データ型が一致しているかではなく、データが代入可能かどうかで判定しているように見えます。
AnyとAnyObjectで、このような違いがあるとは想像もしませんでした。この結果を見たとき、正直、大きな驚きがありました。
結果は同じになると予想しますが、配列ではなくDictionaryに入れた場合も試しました。予想外のことが起こらないとは限りませんので、実際に試すことは大事ですから。Swiftコードは、次のとおりです。
// AnyとAnyObjectで、is演算子の判定結果を調べる試験(Dictionary)
// 変数の準備
let aryTypeName : [String] = ["Int", "Float", "Bool", "String", "Error!!!"]
let iInt : Int = 5
let iFloat : Float = 3.3
let iBool : Bool = false
let iStr : String = "test"
var dicAny : Dictionary<String,Any> = [String:Any]()
var dicAnyObj : Dictionary<String,AnyObject> = [String:AnyObject]()
dicAny[aryTypeName[0]] = iInt
dicAny[aryTypeName[1]] = iFloat
dicAny[aryTypeName[2]] = iBool
dicAny[aryTypeName[3]] = iStr
dicAnyObj[aryTypeName[0]] = iInt
dicAnyObj[aryTypeName[1]] = iFloat
dicAnyObj[aryTypeName[2]] = iBool
dicAnyObj[aryTypeName[3]] = iStr
// is演算子のテスト
println("Test:Any - (Int Float Bool String)\n")
for i in 0..<dicAny.count {
let iBoolInt : Bool = dicAny[aryTypeName[i]] is Int
let iBoolFloat : Bool = dicAny[aryTypeName[i]] is Float
let iBoolBool : Bool = dicAny[aryTypeName[i]] is Bool
let iBoolStr : Bool = dicAny[aryTypeName[i]] is String
println("\(i) \(aryTypeName[i]) -- > \(iBoolInt) \(iBoolFloat) \(iBoolBool) \(iBoolStr) \n")
}
println("\nTest:AnyObject - (Int Float Bool String)\n")
for i in 0..<dicAnyObj.count {
let iBoolInt : Bool = dicAnyObj[aryTypeName[i]] is Int
let iBoolFloat : Bool = dicAnyObj[aryTypeName[i]] is Float
let iBoolBool : Bool = dicAnyObj[aryTypeName[i]] is Bool
let iBoolStr : Bool = dicAnyObj[aryTypeName[i]] is String
println("\(i) \(aryTypeName[i]) -- > \(iBoolInt) \(iBoolFloat) \(iBoolBool) \(iBoolStr) \n")
}
基本的に、配列の場合のコードとほとんど同じです。入れ物をDictionaryに置き換えただけですから、コードの説明は不要でしょう。その実行結果は、次のとおりです。
Test:Any - (Int Float Bool String)
0 Int -- > true false false false
1 Float -- > false true false false
2 Bool -- > false false true false
3 String -- > false false false true
Test:AnyObject - (Int Float Bool String)
0 Int -- > true true true false
1 Float -- > true true true false
2 Bool -- > true true true false
3 String -- > false false false true
期待したとおりの結果で、予想外のことは何も起こりませんでした。AnyとAnyObjectでの判定結果は、配列の場合とまったく同じです。
実際に試したかったのは、これから先の処理です。Dictionaryに入れたデータをファイルとして保存し、AnyとAnyObjectで違いがあるのか見たかったのです。
以前に紹介した、環境設定機能のライブラリ化でも、Dictionaryの内容をファイルに保存しました。そのコードを、そのまま用います。DictionaryをNSDictionaryにキャストして、writeToFileメソッドで書き出すだけです。
書き出すためには、ファイルの保存場所を指定するパスが必要です。それも加えて、次のようなSwiftコードを、前のコードの後ろに追加しました。
// Dictionaryをファイルとして保存(前のSwiftコードの続き)
let iPathDocs : AnyObject = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let iPathFile1 : String = iPathDocs[0].stringByAppendingPathComponent("dicAny.plist")
let iPathFile2 : String = iPathDocs[0].stringByAppendingPathComponent("dicAnyObj.plist")
//let iResult1 : Bool = (dicAny as NSDictionary).writeToFile(iPathFile1, atomically:true)
//if !iResult1 { println("ERROR:ファイル書き失敗:Any") }
let iResult2 : Bool = (dicAnyObj as NSDictionary).writeToFile(iPathFile2, atomically:true)
if !iResult2 { println("ERROR:ファイル書き失敗:AnyObj") }
Any側でファイルを保存する2行だけを、コメント行にしてあります。実は、コンパイルエラーが出たので、コメント化したわけです。もう片方のAnyObjectでは、コンパイルエラーも発生せず、実行も正常に終了しました。
Anyでだけ、コンパイルエラーが出たのですが、そのエラーメッセージが意味不明でした。おそらく、何か別な原因でエラーが出ているものの、1行の中に複数の命令が含まれているため、エラーメッセージが正常に出ないと推測しました。ということで、試しに次の行を挿入してみました。
// エラーメッセージを変えるために、前のコードでコメント化した2行の前に挿入
let iNSDic : NSDictionary = dicAny as NSDictionary // コンパイルエラー
エラーメッセージが変わり、NSDictionaryへはキャストできないことが判明しました。どうやら、Anyを含むDictionaryは、NSDictionaryへキャストできないようです。AnyとAnyObjectでは、is演算子の判定結果だけでなく、いろいろな点で細かな違いがありそうですね。
コンパイルエラーになったので、目的の比較が出来ませんでした。別な方法でシリアライズすれば比較できるのですが、最近は忙しいこともあり、また別な機会にしましょう。
ここまで見てきた結果から推測すると、AnyとAnyObjectには、別な違いもありそうです。それを究明する確約は出来ませんが、やる可能性はあるということで、タイトルを(is演算子編)と括弧付きにしました。でも、あまり期待しないでください。正直、誰かが代わりにやってくれると助かります。
簡単な実験でしたが、AnyとAnyObjectで予想外の違いがあり、かなり驚きました。
そもそもAnyObjectには、IntやFloatが入れられないということ(本当?)なのに、コンパイルエラーになりません。何か特別に、自動変換して代入しているのでしょうか。変換して代入しているので、is演算子の判定結果が代入可能という形になっているのでしょうか。疑問は深まるばかりです。
AnyObjectのDictionaryは、XML形式のファイルとして保存できました。それを見る限りは、Intは「integer」型の値として、Floatは「real」型の値として、Boolは「true」または「false」のどちらかの値として、Stringは「string」型の値として保存されています。データ型の違いが、きちんと区別されて保存されているのが確認できました。それなのに、is演算子の結果は期待どおりになっていません。やはり、疑問は深まるばかりです。
原因は不明ですが、実行結果がこうなっている限り、それを受け入れて使うしかありません。入れたデータ型を明確に区別したい場合は、AnyObjectではなくAnyを使うしかないでしょう。
また、今回の実験結果から、仕様確認は非常に重要だと改めて感じました。これからも少しずつですが、SwiftとiOS APIの仕様確認を続けたいと思います。また何か発見したら、ここで紹介するつもりです。
ちなみに今回の実験も、いつものようにiOS実験専用アプリで行ないました。実験アプリで作るときは、通常ならラベルへ出力します。しかし今回は、コンソールへの出力を使いました。1回しか実行しないので、実行しながら結果を何度も確認する必要はありません。また、行数の多い処理結果に、ラベルは適しません。このような場合は、コンソールへの出力を使うことが多いです。複数行を表示できるTextAreaなどを用意するより簡単ですから。
今回の実験内容はアプリごと保存できるため、いつでも実行が可能です。XcodeやSDKのバージョンが変わる度に、試してみようと思います。
(使用開発ツール:Xcode 6.1.1, SDK iOS 8.1)
0 件のコメント:
コメントを投稿