https://youtu.be/66iuuZD7gm0
第84回で話したタプルのところで自分が勘違いしていたところがあったので、今回はまずその辺りをお話ししてから、前回に引き続き The Basics を眺めていきます。前回は型安全についてみんなとあれこれお話ししたので、その次の 定数と変数
から見ていく回になりそうです。どうぞよろしくお願いしますね。
—————————————————————————— 熊谷さんのやさしい Swift 勉強会 #87
00:00 開始 00:46 第84回の訂正 01:23 タプルスプラット 03:56 複数の戻り値と型エイリアスの組み合わせ 07:13 名前付きの型に差し替えることも容易 09:06 規模に応じて使い分けてみても良いかも 10:07 タプルのイニシャライザー 11:53 タプル型は init は持たない 14:14 質疑応答 15:29 コード補完は効かない 16:11 タプルの初期化はラベルを省略可能 16:28 タプルの使いどころは? 18:13 タプルの要素を型エイリアスで説明する案 21:45 定数と変数、プロパティー 24:36 ローカル変数でもゲッターが使える 28:31 初期化フェーズ 29:25 確定初期化と didSet 32:19 代入前の値が存在しない 33:11 型に所属するプロパティーの初期化フェーズ 36:26 確定初期化できなくなる理由を考えてみる 38:42 次回の展望 ——————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #87
はい、じゃあ始めていきますね。今日は「The Swift Programming Language」の「The Basics」について新しいセクションに入ります。ただ、その前に、以前の勉強会で話した内容について少し補足したいと思います。
以前の勉強会でタプルについて話した際に、型エイリアスについて少し誤解して話していた部分がありましたので、その点を補足します。
まず、タプル(Tuple)についてですが、例えばint
型とstring
型のタプルがあって、それを関数の引数として渡す場合を考えます。この時に型エイリアスの話をする前提として、タプル型とタプルスプラット(Tuple Splat)を混同してしまっていたんです。
具体的には、例えば(Int, String)
というタプル型があったとして、それを関数で受け取る場合に、タプルスプラットを使うことで関数の引数もタプルとして扱えるという話をしました。しかし、Swiftでは関数の引数としてタプルを使う範囲が狭まっているという話がありました。
実際に、その時に話していた内容としては、マルチプルリターンタイプ(複数戻り値)の例としてタプルを使うことができるという話でした。例えば、以下のようなコードを考えます。
func getResponse() -> (Int, String) {
return (200, "OK")
}
このように書くことで、関数が複数の戻り値を返すことができます。この時、型エイリアスを使うともっと表現がスッキリすると教えてもらいました。例えば、以下のようにします。
typealias Response = (code: Int, message: String)
func getResponse() -> Response {
return (200, "OK")
}
これにより、複数の戻り値を返す関数がとてもシンプルに書けるようになります。
ただし、タプル型は基本的には複数の型を簡単に組み合わせただけのものなので、複雑な操作をしたい場合はストラクト(構造体)やクラスを定義した方が良いです。例えば、以下のようにストラクトを使って定義し直すことができます。
struct Response {
let code: Int
let message: String
}
func getResponse() -> Response {
return Response(code: 200, message: "OK")
}
このようにすることで、コードの可読性が向上し、複雑な操作も柔軟に対応できます。
今日は以上のような些細な勘違いが多くて申し訳なかったです。ありがとうございます。それを踏まえて、次に進んでいきたいと思います。 とりあえず、こうやってちょこちょこっと調整してあげれば、これでタプル型だったのを構造体にサクッと差し替えられます。今、ちょっと9行目の戻り値の調整はしましたが、ほんの少しの調整で、12行目から15行目までは何も手をつけていないのに、サクッと差し替えが完了するというわけです。
これを教えてもらったのですが、全然違うコンプリーションハンドラーの話を自分が始めてしまって、これは良くなかったなと思ったので、補足として今日お話ししました。これ便利ですね。だからサクッとプロトタイプなんかを書くときにも使えるんじゃないですかね。まずタプルを使っておいて、後で規模が大きくなってくると、タプルって意外と使いにくいです。型自身がいろんな機能を持つといった複雑さもありますが、さまざまな場面でこの型を使うと、大規模なコードでは意外とタプルは複雑になってきます。
なので、プロトタイプみたいな狭い範囲のときにはタプルでやっておき、実際にプロダクトが動き始めて、規模がどんどん発展していくときには構造体に差し替えるといった使い方が良さそうですね。それで、今コードを書いていて思ったのですが、9行目を手直ししましたが、多分手直ししなくてもこのコードなら動くと思うので、ちょっとやってみましょう。
typealias Response = (code: Int, message: String)
これでコンパイル通るかな。自分の記憶では通るはずです。エラーが出てないから通ってますね。だから、ちゃんと将来差し替えることを想定してコードを書けば、タプルでも良い感じに行けます。タイプエイリアスでも Response
って書いても良いんですよ。そのあたりは、タイプエイリアスを使ったときの混乱ぐらいで、例えば Int
型を別の型にタイプエイリアスしたときも同じです。
タイプエイリアスとして typealias ID = Int
としたときに、 ID
をイニシャライズできますよね。こうやってタイプエイリアスを普通にイニシャライザー呼ぶために丸カッコがオッケーっていうのは意外とシンプルなことです。タプル型をイニシャライズ的に呼べるほうが面白くて、例えば
let response: Response = (200, "OK")
みたいなコードが書けたはずです。これ動きましたよね。律儀にラベルを書いても良いわけです。
let response: Response = (code: 200, message: "OK")
こんな感じだから、タプル型もイニシャライザーを暗黙的に持っているんですよね。 .init
は確かなかったと思うんですけど、 .init
はないですよね。多分エラーになります。そうなるんですよ。なので、ちょっと不思議なアンバランス感がありますね。タプルだと関数型の変数にイニシャライザーを入れられないんですけど、文法上はタプル型をイニシャライザー的に呼べるので、そういったところを考慮すると、複雑な場面になってくるとタプルでは耐えられないけど、簡単な場面であればタプル型をそのまま使えます。
そして、タイプエイリアスを交えると確かにいい感じに、 とりあえずタプルでいいや
っていう状況が簡単に作れそうです。これはいいですね。確かにこれなら とりあえずタプルでいいや
っていう場面が生まれてきそうです。意識してみると、マルチプルリターンタイプの特徴を生かせそうな気がします。
こんなお話でした。面白いですね。次の話に行っちゃいますかね。タプル周り、今の話で何か追加のご質問や実際に使っているよとか、そういったお話があればどうぞ。どうだろう、コメントを拾っておくと、そうそう、タプルをイニシャライズっぽく書く発想ってなかなかないですよね。自分も何で思いついたのか忘れちゃったんですけど、この9行目のことですね。1行で見たほうが見やすいですよね。でも、見にくいときもあります。
タイプエイリアスを使うととてもいい感じで、これがないと とりあえず使っておこう
っていう敷居が高くなっちゃったと思うんですけど、この9行目の書き方があるおかげでいい感じです。5行目のコードが書けるのは大きいですね。でも、補完がどうだったんだっけ。補完出たかな。さっき出てましたかね。あれは Int
型だからか。補完は出ないんだ。これだとサクッと書くには支障がありますね。何を書くんだ、というのをタイプエイリアスを見ていかないといけないので、活用範囲が狭まりますね。
一応、定義はたどれるので、定義をたどれば何を書くのか分かってきますが、補完が効かないのはやっぱり規模が大きくなるとダメそうですね。ここで中に入って、 Response(code: 200, message: "OK")
でも大丈夫ですよね。 大丈夫ですね。そちらは名前を付けるときに、なるべく短いほうがいいです。将来的に構造体と差し替えることを考えると、ラベルをつけてあげないといけなくなりますね。そうですね、その場合は最初から構造体を使用したほうがいいでしょう。確かに、わざわざタイプエリアス (typealias
) と書いている間があるなら、構造体 (struct
) を使うべきですね。短いコードを書くために、わざわざタイプエリアスを使うこともありますが、バランスを見極めるのが難しいですね。
例えば、簡易的な場合に書くことがあるかもしれません。イント (Int
) やストリング (String
) ではなく、URLResponse
や型名が既に何を示しているか明確な場合はそれを使います。Appleのコードやメッセージを見ると、Int
や String
が何を表しているのか明示しなければならない場合は、構造体が一番いいかもしれませんね。
設計の観点から言えば、最初から将来を見据えたデザインを使うのが良いと思いますね。まだAPIの必要性が明確でない場合、簡易的な方法で対処するのも良いかもしれません。スコープが小さい場合や、外部からのリクエストが不要な場合などは、タイプエリアスを使うことがあります。
また、コードとメッセージに対するタイプエリアスを使う方法も一つあります。具体的には、コード: コロン
やラベルを省略する場合です。ラベルを省略すると、形を整えるときに不便かもしれませんが、ラベル省略したイニシャライザーを構造体に搭載することもできますね。
このように、さまざまな表現方法があることを抑えておけば良いでしょう。適材適所で使い分ける感覚を持っていると良いですね。
さて、エラーが出ているのはラベルを省略したからですね。ラベルを省略すると、01
になってしまいますが、そこも踏まえて考える必要があります。
それでは、タプルの話はこの辺にして、本題に戻りましょう。今日のテーマは「ザ・ベーシックス」の最初のセクション、定数と変数についてです。私はこれをひっくるめて「変数」と呼んでいましたが、Swiftが「constants」と「variables」を普通に使うので、最近は「定数と変数」という表現に落ち着く感じがしてきましたね。
定数と変数をひっくるめて呼ぶときに「変数」と呼ぶこともありますし、また「プロパティ」として呼ぶことも悪くないと思います。他の言語では、変数と定数とプロパティは別の意味合いを持ちます。具体的には、プロパティならオーバーライドができたり、プロパティなら読み取り専用にできたりします。これらの違いは他のプログラミング言語でもよく見られる特徴ですね。
要するに、定数と変数にゲッターやセッターを介入させるのが今どきの言語の特徴です。C++やJavaなどのモダンな言語では、これが一般的です。 C++やJavaのような言語では、変数に対してゲッターを備えるという発想が一般的です。しかし、Swiftでは最初からゲッターが備わっており、必要に応じて最適化でそれを省くという発想になっています。
例えば let a
と var b
といった変数定義はプロパティと同じ重みで存在します。具体的にどうなるかというと、変数を定義するときにゲッターを備えて値を返す計算型プロパティとして使えるということです。例えば、次のように変数b
を定義します。
var b: Int {
get {
return 10
}
set {
print("新しい値: \\(newValue)")
}
}
このように、b
が参照された時にゲッターの中のコードが実行され、その値が返されます。また、値が書き換えられた時にはセッターコードが実行され、新しい値が表示されます。
このように、グローバルスコープだけでなく、関数の中のローカルスコープでも同じことができます。たとえば、次のように関数内で変数を定義します。
func example() {
var value: Int {
get {
return 20
}
set {
print("新しい値: \\(newValue)")
}
}
print(value * 2) // ゲッターが呼ばれる
value = 30 // セッターが呼ばれる
}
この結果として、value
を参照したときにはゲッターが呼ばれ、その後value * 2
の結果が表示されます。また、新しい値を代入した場合にはセッターが呼ばれて新しい値が表示されます。
さらに、Swiftには確定初期化(Definite Initialization)という仕組みがあります。これは変数宣言時に必ずしも初期化をしなくても、後で初期化することができるというものです。例えば、次のようなコードがあるとします。
var a: Int
a = 10
ここで、1行目は変数宣言を行っていますが、初期化は行っていません。その後、2行目で初期化が行われています。この2行目のことを初期化フェイズと呼びます。
var b: Int {
didSet {
print("新しい値がセットされました")
}
}
b = 20 // 初期化フェイズ
b = 30 // 代入フェイズ
この例では、b
の値が初期化されるときにdidSet
が呼ばれます。このときに一度目は初期化フェイズで、二度目以降は代入フェイズとなります。
ですが、注意しなければならないのは、didSet
やwillSet
などのプロパティオブザーバを持つ変数の場合、グローバルスコープやローカルスコープで確定初期化を行わないとコンパイルエラーになることがあります。
こうした言語の特性を理解することで、Swift特有のプロパティに対する柔軟な操作が可能になります。例えば、コンスタントかバリアブルかに関わらず、それをプロパティとして扱うこともできます。この話題の理解が深まると、Swiftの特性を活かした効率的なコードを書くことができるようになります。 では次の内容から整えます。
今は、「ビッグセット確定初期化ができなくなる」という特徴があります。これはなぜかというと、厳密な理由はともかく、ビッグセットは再代入のときに走ります。したがって、このときに「オールドバリュー」が存在しますが、このオールドバリューが13行目のときにどうなるか、値が決まっていません。確定初期化を遅らせるとその値が不明な状態になってしまいます。なので、初期化フェーズが必要であると思います。
しかし、型のプロパティの場合には初期化フェーズをイニシャライザーまで遅らせることができます。ちょっとややこしい話になりますが、わからない方は将来分かる日が来るだろうと思って聞いていただければ大丈夫です。例えば、以下のようなコードを書いた場合:
var b: Int = 0 {
didSet {
print("y")
}
}
これがコンパイルが通ります。これでバリューとしてイニシャライザーとして初期化してあげれば、ちゃんと代入されます。それでこれを var x = b
みたいに受け取って、x
の b
に対して別の値を入れます。すると、同じ値でも「y」がプリントされるわけです。
なので、ディフィニットイニシャライゼーションができなくなるというか、初期化フェーズが遅れようとも問題はなかったような気がするので、分かりにくさかなと感じました。もう一つ述べておかないといけないことがあって、例えば以下のようにイニシャライザーを規定して、この中で何かしらの初期化を行う場合です:
struct MyStruct {
var b: Int
init() {
b = 0
}
}
型の場合は、初期化フェーズがイニシャライザーが処理を終える時点まで続きます。この間は didSet
が走らないという特徴があります。これは処理が複数の行をまとめて初期化フェーズとして捉えられるからです。そのため、例えば以下のようにしても didSet
は走りません:
struct MyStruct {
var b: Int = 0 {
didSet {
print("y")
}
}
init() {
b = 0
}
}
この場合、33行目の didSet
が1回しか走らず、最終的に「y」が1回しか出ません。
これは初期化フェーズ中には didSet
が走らないという特徴があるためで、前の値がない状態での didSet
実行は安全性を損なうことになります。この考え方をグローバル変数にも適用できます。例えば、以下のように初期化フェーズを後にずらすことができます:
var b: Int = 0 {
didSet {
print("y")
}
}
b = 30
この場合、初期化フェーズ中は didSet
が走らず、初期化が終わった後に didSet
が実行されます。しかし、この考え方は少々ややこしくなることがあります。例えば、グローバル変数を書き換える関数を作成し、その中で初期化フェーズが走るかどうか不明確になる場合です。
こういった理由から、確定初期化をしておかないといけない状況があります。とりあえず、今日の勉強会では、ディズセットがグローバルスコープの場合、初期化フェーズを明確にしておくことが必要という仕様を紹介しました。理由を突き詰めて話していくと、どうしても分かりにくさが残る点もあるかと思います。
ここまでが今日の勉強内容でした。次回は、変数とその特徴的な部分についてもう少し詳しく見ていきたいと思います。それでは、今日の勉強会はこれで終わりにします。お疲れ様でした。ありがとうございました。