Swiftをきちんと学習したのは今年に入ってからなので私だと説明不足のところもあると思いますが、復習もかねて説明します。
ちなみに、参考文献[1]は「荻原剛志著・詳解Swift第5版」、参考文献[2]は「石川洋資、西山勇世著・改訂新版Swift実践入門」からの引用です。
①ここに気がつくまで1日かかってしまったのですが、正直このエクスクラメーションマークの意味となぜこれがないとerrorが生じるのかまでわかりませんでした。変数でクラスや型を指定する際に
player:inout AVAudioPlayer!の様にエクスクラメーションマークが必要なものや
path: URLの様にエクスクラメーションマークが必要でないものがあるのですが、
このエクスクラメーションマークの意味とどの様に要否を使い分けるのかをご教示願えますでしょうか?
変数宣言時に指定するエクスクラメーションマーク(!
)は有値オプショナル型(implicity optional value、暗黙的開示オプショナル型とも、参考文献[1]p.105)と呼ばれる値です。
そもそも、オプショナル型とは何かというと、ある特定の型で表せる値以外に nil
という特別な値を持たせた型のことです。言い換えれば、値があるか空かのいずれかの値を表す型とも言えます(参考文献[2]p.60)。
ある型(Wrapped型)をオプショナル型として宣言するときには、Optional<Wrapped>
と宣言するほかに、Wrapped?
と宣言する方法(糖衣構文、シンタックスシュガー、参考文献[2]p.60)もあり、多くは糖衣構文の方が用いられるようです。
swift
1var a : Int // Int型
2var b : Int? // Wrapped<Int>型
上記の例以外も、続く例で引用する例文は Playground で実行できますので、そちらで試されることをお勧めします。
ここで、変数a
は Int
型の変数で10
という値を持ちますが、変数b
はオプショナル型のInt
型(Optinal<Int>
)型となります。同じ構文で違う型も宣言することが可能であり、String
型のオプショナル型であればString?
、UIButton
のオプショナル型ならUIButton?
と宣言できます。
ところで、Optional
型から値を取り出すとき、たとえばInt?
型からInt
型の値を取り出すためには、開示(アンラップ、参考文献[1]p.97)を行う必要があります。開示を行うことを開示指定といいますが、そのためには!
を開示する変数の右側において指定します。
Swift
1var b: Int? // Optional<Int> として宣言
2var c: Int = 20 // Int として宣言
3b = 10 // Optional<Int>に10を代入
4c += b! // d は Int 型で Optional<Int> 型を代入できないのでアンラップする
5
6print(b)
7print(c)
上記の例だと、print(b)
の行で Warning が出ます。本来はprint(b!)
とアンラップして使うべきなのですが、Optinal
型だと分かりやすくするため、あえて開示せずに使っています(参考文献p.98)。ご自身で試されるときにはアンラップした場合も実行して、結果の違いを確かめるといいかと思います。
上記の例でわかるかと思いますが、Int
型で定義された変数c
に値を代入するためには、Optional<Int>
型からInt
型を取り出す必要があります。その目的で!
を使っています。**重要な点は、オプショナル型を開示するためには、プログラマは!
を用いて逐一開示指定を行わなければいけないという点です。**面倒にも思えますが、このようにして nil
を持つ可能性がある変数の扱いをプログラマに意識させることが、結果としてバグの少ないプログラムへと繋がる利点もあると言えます。
このとき気を付けなければいけないのは、開示指定の!
と、冒頭にのべた有値オプショナル型の!
は違う目的である、という点です。混乱してしまいそうですが、重要な点ですので押さえておく必要があります。
さて、話は有値オプショナル型に戻るわけですが、「オプショナル型」である以上、当然これまでの議論も関係してきます。
変数宣言時に!
を指定する場合も、?
を用いた指定と同じく「オプショナル型」の変数であることには違いがありません。!
を用いた宣言と?
を用いた宣言の違いは何かというと、!
を用いた宣言は、文や式の中で逐一開示指定をする必要がない(しても構わない)という点です。開示指定が不要という点以外は通常の(?
で宣言した)オプショナル型と同じです(参考文献[1]p.105)。
では、!
で宣言したオプショナル型(有値オプショナル型)と、?
で宣言したオプショナル型はどのように使い分けるべきかというと、!
で宣言されたオプショナル型は、その後確実に値(インスタンス)が代入され、その後変数の値が nil
になることがないような場合に用います。
一番代表的な例は、UILabel
など、インタフェイスのインスタンスを宣言するような場合です。
Swiftのクラスは、すべてのプロパティは明示的に初期されている必要があります(参考文献[1]p.214など)。イニシャライザを使って初期化しない場合には、あらかじめ何らかの値を代入しておく必要があります。
しかし、場合によってはそのような宣言が適当でない場合(という表現が良いかはわかりませんが)もあるので、変数宣言時に明示的に値を代入しない場合、nil
が代入されるオプショナル型を使って宣言しておき、後に値(インスタンス)を代入するような場合が多くあります。
もちろん、この宣言で?
を使って変数を宣言することも可能ですが、実際は毎回アンラップするのが大変なため、プログラマの責任で!
を使って宣言し、アンラップの手間を省いているということもできると言えるでしょう。
適切な例題が思いつかないのですが、たとえば以下のような例があるかと思います。
Swift
1class testA {
2 var a:Int!
3}
4
5var A: testA!
6print(A) // この時点では nil
7
8A = testA()
9print(A) // たとえば Optional(__lldb_expr_27.testA) __lldb_expr_27 は内部名なので環境によって変わる
10
11print(A.a) // この時点では nil
12A.a = 10
13print(A.a) // この時点で Optional(10)
なぜ変数宣言をオプショナル型で行わなければならないのかという問題は、実はクラスとイニシャライザによるプロパティの振る舞いも関連してくるはずのですので、適切な参考書して理解を進めるのがいいかと思います。
②また、関連してですがテキストによって、変数の設定の仕方について
var cymbalPlayer: AVAudioPlayer!//here3
の様にする場合と
var cymbalPlayer = AVAudioPlayer()
と書いている様なものもあるのですが、どちらでも良いのでしょうか?エクスクラメーションマークとも関連があるかもしれないのでお尋ねさせていただきました。
結論から言いますと、どちらでも構わないと思います。
たしかに、var cymbalPlayer = AVAudioPlayer()
の例だと Xcode 11.3 では実行時エラーになりますが、Xcode 11.4 では問題なくコンパイルが通りますし、実行時エラーを起こすこともありませんでした。
コメントで触れましたように、teratail の過去問や Stack Overflow でもこの辺りの問題について議論されていたようですが、あまり深く考えなくてもいいかと思います。
var cymbalPlayer: AVAudioPlayer!
は先に変数だけ宣言しておいて、後からインスタンスを代入していますし、var cymbalPlayer = AVAudioPlayer()
では変数宣言と同時にインスタンスを代入しているだけの違いです。
Swift
1var a: Int
2a = Int(10) // a = 10 とおなじ。
3print(a)
上記の例(例-a)だと、まず変数をInt
型で宣言しておき、その後a
にInt
型のイニシャライザを用いて10
を代入している例です。
これは、
Swift
1var a: Int = Int(10)
2print(a)
と同じ理屈だと理解して頂けるのではないでしょうか。
③また、少し話が逸れてしまい申し訳ないのですが、
soundPlayer(player: &cymbalPlayer ,path: cymbalPath, count: 0)//here2の様に
参照渡しを利用する場合なのですが、これを&を使用せずに値渡しすべきではない理由、またはその弊害が> ピンと来ないのですが、テキストには
"「soundPlayer」処理後にシンバルの情報を呼び出し元に戻して利用したいので、参照渡しにしている"
と記載されています。実際には&なしでコーディングしても結果、動作させても影響がない様に思えるのですが、具体的にどの様な影響があり、なぜ参照渡しにすべきなのかアドバイスをを頂戴できると大変嬉しいで す。
私が少し勘違いと混乱していたようです。
&
を取ってコンパイルするとエラーになるはずですが、質問者さんの環境ではいかがでしょうか。
したがって、具体的な影響としては、まず「コンパイルに通らない」という影響があります。
理由ですが、英語による Swift のマニュアルのうち、In-Out Parametersの項をみるとこのように記述してあります。
You place an ampersand (&) directly before a variable’s name when you pass it as an argument to an in-out parameter, to indicate that it can be modified by the function.
また、参考文献[1]では「ここで使っている&
という記号は通常の演算子ではなく、inout
引数に対応する実引数で表す記法です」とあり(p.44)、参考文献[2]では「インアウト引数を持つ関数を呼び出すには、インアウト引数の先頭に&
を加えます」とあります(p.110)。
C言語をやっていたらアドレス演算子としての印象が強く、&
という記号そのものに何らかの操作があるように感じます(し、私の場合はついその印象で考えてしまいました)。
しかし英文マニュアルの説明などを見る限り、Swiftでは&
を付与することによって特別な操作があるわけではなく、&
をつけることによって、引数として与えられた変数が書き換えられるということを明示的に示していると考えられます。
では、そもそも何故値渡しではなく&
をつけて参照渡しするのかというと、処理先の関数の中で実引数として渡したプロパティが参照しているインスタンスそのものを書き換えているからです。
あんまり良い例題ではないですが、こんなサンプルを作ってみました。
swift
1import UIKit
2
3class ClassA {
4 var msg: String
5 init(msg: String) {
6 self.msg = msg
7 }
8 deinit {
9 print("deinit - msg: (msg)")
10 }
11}
12
13func newClass(cl: ClassA){
14 // Swift は仮引数の値は書き換えられないので、同名の別の変数に置き換える
15 // 参考文献[1]p.46
16 var cl = cl
17 cl = ClassA(msg: "newClass内で作ったインスタンス")
18}
19
20func newClassWithInout(cl: inout ClassA){
21 cl = ClassA(msg: "newClassWithInout内で作ったインスタンス")
22}
23
24do{
25 print("inout 無しで呼び出し")
26 var clA = ClassA(msg: "最初のインスタンス")
27 print("newClass呼び出し前 - msg: (clA.msg)")
28 newClass(cl: clA)
29 print("newClass呼び出し後 - msg: (clA.msg)")
30}
31
32print("")
33
34do{
35 print("inout 付きで呼び出し")
36 var clA = ClassA(msg: "最初のインスタンス")
37 print("newClass呼び出し前 - msg: (clA.msg)")
38 newClassWithInout(cl: &clA)
39 print("newClass呼び出し後 - msg: (clA.msg)")
40}
実行すると、こんな感じになると思います。
inout 無しで呼び出し
newClass呼び出し前 - msg: 最初のインスタンス
deinit - msg: newClass内で作ったインスタンス
newClass呼び出し後 - msg: 最初のインスタンス
deinit - msg: 最初のインスタンス
inout 付きで呼び出し
newClass呼び出し前 - msg: 最初のインスタンス
deinit - msg: 最初のインスタンス
newClass呼び出し後 - msg: newClassWithInout内で作ったインスタンス
deinit - msg: newClassWithInout内で作ったインスタンス
注意深く読まないと分かりにくいとおもいますし、もっと細かくprint()
をいれて動作を確認する必要はあると思いますが、inout
あり無しの処理において、最初に作ったインスタンスと、関数内で新たに作ったインスタンスのライフサイクルの違いはわかるかと思います。