2015年3月17日火曜日

Optional型の使い方(改めて考えてみた)

Swiftの特徴の1つに、Optional型があります。どのような役割なのか、どのように動作するのかは、多くの詳しい人がブログなどで書いていますから、ここで改めて書く必要はないでしょう。

でも、どのように使うのが良いのかは、あまり深く考察されてないように感じています。それなのに、いろいろと悩む点が多いのではないでしょうか。私自身も、まだ悩み続けています。Swiftを最初に使い始めた頃に比べると、使い方が少しずつ変化してきてます。Swiftにも少しずつ慣れてきたところなので、Optional型の使い方を改めて考えてみました。じっくりと改めて考えることで、使い方の整理ができて、今後のプログラミングに役立つのではないかと思うからです。

なお、Optional型のような機能の使い方は、各人のプログラミング方針に大きく影響を受けやすいものでもあります。このような考え方もあるのかという意識で読んでください。ちなみに私は、Swiftの変数宣言で、データ型の指定を積極的に付ける方針でプログラミングするタイプ(コンパイラのチェック機能を積極的に利用して、実行前に間違いを見付ける派)であり、またInterface BuilderやStoryboardを使わない派でもあり、世間的には少数派だと認識しています。

 

正直なところ、最初のうちは深く考えずにOptional型を使っていました。APIでOptional型が返る場合は、そのままOptional型で受けたり、アンパックしてOptionalを外したり、その場その場で決めていました。明確な判断基準は持たず、何となく決めていたという感じです。

自分が作る関数でも、戻り値にnilが入る場合には、深く考えずにOptional型を使っていました。あまり疑問は持たず、そうするのが当たり前だと思っていました。

新しい言語やAPIを使い始めた当初は、細かい部分にまで気を回す余裕がないので、当然と言えば当然のことでしょう。

 

では、Swiftに慣れてきた現時点で改めて考えると、どのような使用基準が良いのでしょうか。一番大事な基準は、やっぱり「Optional型を、できるだけ使わない」でしょう。使い方の話なのに、いきなり「使わない」という基準が登場します。でも、それが極めて大事なことですから、やはり一番気にしなければなりません。

Optional型の変数では、指定された型の値ではなく、nilが入っている可能性があります。つまり、nilが入っている場合も常に考慮しながらプログラミングする必要が生じるのです。nilが入っていないか検査するとか、必要に応じて余計な処理を作らなければなりません。

とくに大変なのが、時間が経過してからメンテする場合でしょう。全体の構造から細かな機能まで、何もかも忘れてしまっています。もし一部の機能だけ修正が必要になったとき、そこにOptional型の変数が含まれていたら、nilの検査を入れるべきか判断するために、関係するソースコードを広く見なければなりません。もし面倒に感じたなら、nilの検査を一応入れておき、nilならエラーメッセージを出すといった、小手先の対処方法を考えるかもしれません。

ですから、Optional型の変数を使うのは、nilが入っている可能性がある狭い範囲に限定し、途中でnilが入ってないと判断できたら、Optional型の変数を使うのを止めて、Optional型でない変数に切り替えるのがベストな方法だと思います。

 

Optional型を使わないようにしようと思っても、使わなければならない場面はいくつもあります。その場面ごとに、できるだけ使わない方法を考えてみました。

 

Optional型が必須となる状況として真っ先に挙げられるのは、APIから受け取る値です。種類が多いのも特徴で、インスタンス生成が失敗したとき、nilを返すような仕様になっています。こうした値にOptional型が指定されていれば、Optional型の変数を使わなければなりません。

ただしSwiftには、このような状況を上手に記述できるようにと、if let文が用意されています。Optional型の変数で受け取るのではなく、Optional型でない変数で受け取るように記述できます。また、nilを受け取ったときの処理(非正常時の処理)も、一緒に記述できます。

// APIから得た値を、 Optional型でない変数で受け取る
...
// Optional型の値を受け取る
if let iNSDic: NSDictionary = NSDictionary(contentsOfFile:iPathFile) {
    // 正常時の処理
    ...
} else {
    // 非正常時の処理
    println("ERROR:ReadPlist")
    return
}
...

このような作り方すれば、Optional型の変数を使わなくて済みます。

逆に、if let文を使えない状況のときは、いったんOptional型の変数で受け取って、必要なところだけOptional型の変数を使うような作り方になります。その場合も、Optional型の変数をできるだけ使わないように作ります。

 

次は、UI部品などを入れる変数です。複数の箇所から参照されるUI部品は、変数名を付けて宣言します。最初の状態では空で、初期か処理の中でインスタンス生成するという流れになります。

理想的には、変数宣言した箇所でインスタンス生成するのが一番です。しかし、そのように作れないことも多くあります。関連するUI部品の変数宣言は並べておきたいし、初期化処理はまとめて別な場所に入れたいなど、それなりの理由があるからです。結果として、変数宣言とインスタンス生成が離れてしまいがちです。

このような場合には、「?」を付けた通常のOptional型ではなく、「!」を付けた特殊なOptional型(Implicitly Unwrapped optionals)の変数として宣言します。このタイプの変数は、変数の値を参照するとき、明示的にアンラップしなくて使える点です。Optional型でない変数のように扱えます。

// 宣言した後から初期化する変数は、「!」を付けた特殊なOptional型で作る

// 変数の宣言
var txfUserName: UITextField!

// まとめて初期化(インスタンス生成)の中に含める
...
txfUserName = createTxtFldF("12", 16, ALIGN_LEFT, 700, 460, 100, 30)
...

使う際の注意点としては、値を参照する前に、必ず初期化しなければならないことです。初期化する前に参照すると、実行時エラーとしてアプリが異常終了します。でも実際には、理解して使っている限り、初期化しないで参照するなんて起こりません。まれに初期か処理を付ける前に実行してしまったとかはありますが、異常終了したことで気付きます。この種のエラーは、きちんと作っている人にとって、心配してくて構わない部類のエラーと言えます。

 

Optional型の変数をアンラップする方法として、「?」と「!」の2種類が用意されています。違いは、変数の値がnilの場合の動作です。「?」ならnilが返り、「!」なら実行時エラーとなります。

通常の場合は、「!」で構わないと思います。アンラップして使う際には、nilではなく、インスタンスが入っている前提があります。それなのにnilが入っているというのは、プログラムまたはデータに問題がある証拠です。その場で実行時エラーを発生させたほうが、どの変数が悪かったのか明確になって調べやすいでしょう。

逆に「?」でアンラップするのは、nilが入っても構わない特殊な使い方です。該当する場合にだけ、「?」でのアンラップを使うことになります。

 

自分が作る関数やメソッドも、取り上げましょう。戻り値のある関数の場合、指定された型の戻り値を返せない状況が、どうしても発生します。その場合にnilを返して、呼び出し側にエラーだと伝えます。この形が、昔から(C言語などの時代から?)使われている一般的な方法でしょう。こうした考え方があり、その考えに沿ってAPIが作られているからこそ、Optional型が用意されていると言えます。

では、本当に仕方がないのでしょうか。自分が作る関数やメソッドでも、すべて同じ考え方で作って構わないのでしょうか。この点に、私は疑問を持ちました。そして、いろいろな状況を区別して考えました。その結果、本当に仕方がないときにだけnilを返し、それ以外はnil以外を返す形で作っても構わないのではないかと思い始めました。状況の違いを、具体的に示して説明しないと、何を言っているのか分からないと思いますので、いくつかの例を挙げてみます。

 

まず最初は、独自ライブラリの使い方を間違ったケースです。対象となる独自ライブラリのクラスは、UIViewを拡張したクラスで、UIViewの大きさ(幅と高さ)に最低値があります。その最低値よりも小さな値を指定して、インスタンス生成しようとした状況です。普通に考えると、指定値が間違ってますから、インスタンスは生成せず、nilを返す形の作り方になります。当然、戻り値はOptional型となります。

しかし、考え方を少し変えたらどうでしょう。最低値よりも小さな値を指定した場合は、エラーにせず、最低値に置き換えてインスタンス生成する仕様に変えたら。インスタンスが生成できるわけですから、nilを返す必要がなくなります。

ただし、そのままだとエラーに気付きません。目立つエラーメッセージをコンソールに表示して、気付いてもらいます。きっちりと開発したい人(私も該当します)なら、エラーメッセージを見逃さないで気付くはずです。

エラーを知らせるための、別な方法も考えられます。UIViewのように画面に表示するクラスなら、そのUIViewに付けてあるUILabelなどに、エラーメッセージを表示させるのも1つの手です。文字を赤くして目立たせるなども効果的でしょう。これなら、コンソールのメッセージを見なくても気付けます。

この例での考え方は、エラーにせずに救える場合は救ってあげて、nilを返さないというものです。救ってあげられる場合は限られていますから、適用は限定的でしょう。

 

似たようなケースとして、何かの値を文字列に変換する関数を考えます。変換対象のデータにエラーがあるとき、Stringの代わりにnilを返すことで、エラーを知らせるという仕様もアリでしょう。当然、戻り値はOptional型になります。

しかし、nilを返す代わりとして、エラーを意味する短い文字列(たとえば「ERROR!!!」とか)を返すとともに、コンソールにエラーメッセージを出す仕様も考えられます。こうすると、戻り値にOptional型を使わずに済みます。

データ型が文字列でなく数値の場合は、あり得ない値を返すことになります。整数なら-99999、浮動小数点なら-99999.9とかです。使用状況に応じて、あり得ない値を決める必要があるでしょう。

こちらの例での考え方は、該当するデータ型の変な値をとりあえず返し、エラーメッセージで知らせるというものです。当然ながら、最悪の場合はアプリが正常に動いてしまうので、エラーメッセージに気付く必要があります。

ただし、関数を呼び出す側が、正しく変換できたのか知りたい場合は、nilを返す必要があります。呼び出し側でnil検査をして、変換の成否を確認できるようにするためです。当然、戻り値にはOptional型を使うことになります。

 

ここまで2つのケースを挙げましたが、忘れてはならない大事な注意点があります。エラーが発生したとき発見しづらくなるようなら、こうした方法を使わずに、素直にOptional型を使うべきです。

少し具体的に説明します。文字列に変換する関数が、ファイル名に関係する関数だったとしましょう。そうすると、エラーが発生するかどうかは、使っている環境に依存します。念入りにテストしたとしても、予想外のファイル名が来た場合、エラーとして判断される可能性もあります。

このように特殊な実行時に発生すると、コンソールのエラーメッセージを見るという行為はできません。エラーが発生せずに何かの文字列が返り、そのまま実行が続いて、最終的には関係ない別な箇所でエラーが発生したりします。すると、最初のエラーまでたどり着くのが極めて大変になります。このような状況が予想される場合には、素直にOptional型を使ってnilを返すのが一番です。

逆にOptional型を使わないと判断して構わないのは、実行時にエラーに気付く可能性はゼロで、開発中にエラーに気付く状況しかあり得ないときだけです。ライブラリの使い方の間違いとか。それに該当するときだけ、Optional型を使わない作り方をすべきでしょう。

 

もう1つ、別な視点として、実行時エラーだけは避けたいという考え方もあります。表示する値が変になっても、実行時エラーで異常終了しないほうがよいという考えなら、エラーメッセージで知らせて、Optional型を使わない選択肢もアリでしょう。最終的には、作る人の考え方で決まります。

もちろん、この場合はプログラムの作り方も少し変わり、単にOptional型を使わないだけでは済みません。実行時エラーを発生させないのに加えて、エラーを発見しやすくしたり、関係する処理でも異常終了させない配慮が必要です。今回の話題とは別な話なので、この辺にしておきます。

 

それにしても、エラーのときにnilを返すという方法が、根本的に何とかならないのでしょうか。もっと別な形でエラーを返せるような、言語上の仕組みとか、API上の工夫とか、格段にエレガントな方法があっても良さそうに思います。

オブジェクト指向とか入ってきましたけど、根本的な部分では大きく改善されてないような感じですね。プログラミングの技術って、本当に進歩しているのか、疑問に思う部分でもあります。

 

ここまでOptional型の使い方というか、使わずに作る方法を考えてみました。読んで分かるように、これが常にベストという方法などありません。いろいろな視点で悩みながら得られた方法で、特定の視点では優れているものばかりです。最終的には、自分の考えに合った作り方を選ぶしかありません。

これから先も、Optional型に関しては悩みながら作ることになるでしょう。また何か面白い考え方がアイデアが思い付いたら、続編として紹介したいと思います。皆さんも、Optional型の上手な使い方がないか考えてみてください。

 

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

0 件のコメント:

コメントを投稿