Optional型がいまだに理解できていないようです。今日はテックキャンプの講師に何度も何度も質問しちゃいました。
今日の記事はアプリ内でWEBページにアクセスするためのURLクラスの使い方と、そこから派生してOptional型や強制的アンラップの使い方、Failable Initializerについて調べたことを書いております。
テックキャンプの教材で躓いた部分&講師への質疑応答
例えば以下のコードは自分でも理解ができています。
var str: String print(str!)
これはコンパイルエラーが出てしまう。
Cannot force unwrap value of non-optional type ‘String’
strがOptional型じゃないというエラーがでる。Optional型じゃないのに強制アンラップしようとしてるから。ラップされてないもののラップを取ろうとしたらエラーが出るのは当然ですよね。
それから以下の文。これはテックキャンプで教材を進めていく中でぶち当たった自分の課題。
アプリ内からURLで外部に接続するときに一般的に使われる記述です。
URLクラスのインスタンスを生成し、それを引数にURLRequestクラスを生成しています。
let myURL = URL(string: "https://tech-camp.in/") let myURLRequest = URLRequest(url: myURL)
このコードを入力すると以下のエラーが出てしまう。
Value of optional type ‘URL?’ not unwrapped; did you mean to use ‘!’ or ‘?’?
エラーを見るとURLはOptional型だろ、アンラップしろや。て書いてあるように見える。それでテックキャンプの教材を読み進めていくと以下のような記述を発見。
URLRequestメソッドは引数にnilを指定できないので、myURLを強制的アンラップして(myURL!)nilで無いことを保証する必要がある
とのことでした。ちょっと今の自分には理解が追いついていません。
でもmyURL!で強制アンラップしたらnilでした、となったらそのタイミングでエラーになってしまうのではないだろうか… コンパイルは通るんだろうけど。。
そうするとこれはエラーがでませんでした。
let myURL1 = URL(string: "https://tech-camp.in/") let myURLRequest = URLRequest(url: myURL1!)
で、講師陣になぜ自分のコードはエラーが出るのか質問してみました。
② URL.Requestはnilを指定できないのだとしたら、強制的アンラップをして中身を取り出したときにmyURLがnilだったら、エラーが出てしまうのではないか。
これが私の疑問。
この問に関して以下の回答を得た。
強制的アンラップというよりは暗黙的アンラップ型での宣言、と言った方が正しいように思います。
「!」の使い方は2種類あります。宣言するときと、アンラップするときです。
let myURLRequest = URLRequest(url: myURL!)
上記の使い方は、アンラップするとき、というよりは宣言するときに使われるやり方です。
例えば
var myURLString: String!
というような宣言と似たような使われ方ですね
一時的に「!」でオプショナル型にすることで、「nil」を許容した状態を作ることができます
また以下のような書き方でもOKとのこと。
//これもエラーがでない let myURL2 = URL(string: "https://tech-camp.in/")! let myURLRequest2 = URLRequest(url: myURL2)
うーん。ちょっと混乱しています。Swiftは初心者なので言葉の意味もうまく把握できていないかもしれません。
なので自分でこの先を調べていくことにしました。それで自分なりの回答を得たので記述したいと思います。もし間違いがあれば指摘いただけると助かります。
URLクラスを自分で調べていくことに
URLの宣言部分をAppleのリファレンスで参照してみました。そうするとURLクラスは以下のように
public struct URL : ReferenceConvertible, Equatable {
と書かれています。
この時点で思ったのはURLはクラスではなくて構造体なのではないか、ということです。でも自分が購入したSwift書籍にもURLクラスと書かれているからURLはクラスなのだろう。
そして初期化部分に以下のようにかかれている。
/// Initialize with string. /// /// Returns `nil` if a `URL` cannot be formed with the string ///(for example, if the string contains characters that are illegal in a URL, or is an empty string). public init?(string: String)
コメント部分は正規URLじゃなければnilを返すよ、と訳すことができます。正規URLならURL構造体を返すようになっていると想定しました。
調べていくとSwiftでは構造体にもinitメソッドを保持して初期化をすることができることが判明しました。なのでこのURLクラスだったものは私の中ではクラスではなくて構造体ということで解決しました。
次にinitの後ろに「?」がついているのが第2の不明点でした。WEBで調べてもどこにも見つからない。もしかしたらこれが原因かもしれない。
Googleで探したキーワードは
「swift init?」
「swift メソッドの後ろに?」
「swift initの後ろに?」
などです。けど日本語のサイトで解説しているのが見つかりませんでした。
それで海外のQ&Aサイトで以下の記述を発見しました。
“It is sometimes useful to define a class, structure, or enumeration for which initialization can fail. This failure might be triggered by invalid initialization parameter values, the absence of a required external resource, or some other condition that prevents initialization from succeeding.”
init?() というのはfailable initializer(フェイラブルイニシャライザー)と呼ばれている様子。解釈すると構造体の定義のときにこれを宣言すると初期化のときの引数パラメータが不正や無効であった場合に初期化に失敗させることができます。
なるほど、ということは初期化のときに失敗したらreturn nilで、nilを返すような実装をする構造体ということが理解できた。リターン値がnilである可能性があるのでこれで返ってきた値はOptional型となる。
init?()の挙動をPlaygroundで実験してみる
XcodeのPlaygroundでコードを書いて試してみる。
var oha: String? oha = "おはよう" print (oha)
これはOptionalのString型で定義したohaをprintしています。
結果が画面右側に出ているように”Optional(“おはよう”)\n”となるのでわかります。
以下のような構造体を定義してみました。
struct fruits { let name: String public init?(string: String) { if string.isEmpty { return nil } name = string } }
fruitsという構造体を定義してfailable initializerでinit?で文字列型を受け取るようにします。if文でstring.isEmptyで中身を調べて空であればnilを返し、値があればnameという定数に初期化時の引数を入れるようにします。
let fuji = fruits(string: "りんご")
このように構造体に”りんご”という文字列を渡して初期化します。
print(fuji)
とやると結果は
と出ました。
やっぱり。init?()での初期化の返り値はOptional型で返っているようです。想定はあたっていました。
Optional型なのでそのままは使えませんから
fuji!
とfujiを強制的アンラップをしてラップを外してあげれば使えるようになるはずです。
print(fuji!.name)
とすると結果は
となりました。
↓
もし以下のようにアクセスしようとすると
fuji.name
↓エラーが出てしまいます。
error: value of optional type ‘fruits?’ not unwrapped; did you mean to use ‘!’ or ‘?’?
fuji.name
fruitsはOptional型なのにアンラップされてないよ、という感じですね。
簡単ですが実験ができましたので以下の文の解釈をいれていきます。
URL構造体を渡すとエラーになる理由と解決策
最初に躓いたコード
let myURL = URL(string: "https://tech-camp.in/") let myURLRequest = URLRequest(url: myURL)
これがエラーになる理由は
1行目でURLは構造体であり内部でfailable initializer init?()が使われていて初期化されています。なので初期化に失敗すると(渡した文字列がURLになってない等)nilを返す仕様です。しかしnilを返すためにOptional型にラップして返してるわけですね。正規のURL文字列を引数にして初期化が成功しても返す値はOptional型となります。
なので let myURL はOptional型です。
2行目でURLRequestへの引数としてURL構造体(myURL)を渡すけど、ラップされてる状態なので
Value of optional type ‘URL?’ not unwrapped; did you mean to use ‘!’ or ‘?’?
というエラーが出てしまっている。
なのでラップを外してから引数に与えて上げる必要があります。それで以下のように
let myURL = URL(string: "https://tech-camp.in/")! let myURLRequest = URLRequest(url: myURL)
や
let myURL = URL(string: "https://tech-camp.in/") let myURLRequest = URLRequest(url: myURL!)
のように「!」をつけて強制的アンラップをして引数を渡して上げる必要がある。と理解しました。
そうするとテックキャンプの教材の以下の部分
>URLRequestメソッドは引数にnilを指定できないので、myURLを強制的アンラップして(myURL!)nilで無いことを保証する必要がある
これはどういう意味なんだろう。「nilでないことを保証」しているようには見えない。
例えば以下のような適切ではない文字列のURLを指定したとすると。
let myURL = URL(string: "https://■■“) let myURLRequest = URLRequest(url: myURL!)
コンパイル時点ではエラーは出ないけど、シミュレーターが起動した時点でこのプログラムが落ちてしまった。
なのでもしnilでないことを保証するなら以下のような文になるのではないかと思う。
let myURL = URL(string: "■") if myURL != nil { let myURLRequest = URLRequest(url: myURL!) myWebView.loadRequest(myURLRequest) print("URLへアクセス可") } else { print("URLはアクセス不可") }
これはmyURLがnilでなければというif文を使っています。nilでなければ次の処理へ進むのでmyURL!で強制的アンラップをしたら中身がnilだったから落ちるということがありません。
シミュレーターを起動してみたらエラーもでずアプリも落ちませんでした。
ということで自分の中で今回の件は解決したことにします。
そうするとテックキャンプの教材のこの文は…
>URLRequestメソッドは引数にnilを指定できないので、myURLを強制的アンラップして(myURL!)nilで無いことを保証する必要がある
自分の中ではあまり的を射ていない気がするのですが‥
これに関しては再質問してみようかなと思います。