https://youtu.be/JbDi0gcDTXI
今日は、前回の終わりに少しお話をした 変数宣言におけるルール
のところの 変数や定数の書換可能性
についてを振り返りつつ、時間が余ればその続きの 定数と変数の出力
について見ていくことになりそうです。この節は見どころがたくさんある感じなので、まずは print
関数みたいなピンポイントなところを見ていってから、次回以降でより広めの視野で眺めて行けたらいいなと思っています。よろしくお願いしますね。
————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #100
00:00 開始 00:48 変数の型の互換性 04:36 クラス継承と互換性 06:36 さまざまな互換性のある型 07:35 サブタイピングによる互換性 08:37 存在型による互換性 09:39 不透明な型に対する互換性 11:55 不透明な型に再代入できる値の型は? 14:53 不透明な型とリテラル変換 22:15 不透明な型と Self 29:36 存在型と不透明な型 31:46 AnyObject と互換性 33:55 互換性のある型についてのおさらい 35:11 クロージャーにおける互換性 37:43 変数と定数の可変性まとめ 38:24 unsafeBitCast による互換性の担保 40:19 次回の展望 —————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #100
はい、では始めますね。
次のセクションに入る予定だったのですが、前回最後に値の変更について話していました。その際、変数や定数の名前のところに埋め込まれているという話がありました。その中で「互換性のある」という部分に注目して、面白い言葉だなと思ったので、ここから考察を始めたいと思います。
言われてみると、既存の変数の値を互換性のある型の別の値に変更可能ということは、普段あまり明確に言葉にされることは少ないですね。ただやっていること自体は普通のことです。しかし、そういった表現を改めて聞くと、「おや、これは何を言ってるんだろう?」という印象を持つかもしれません。
具体的な例として、このスライドを紹介しましょうか。スライドの例は型推論が混ざっているため、少しややこしい部分もあるので、実際にプレイグラウンドで試してみます。
まず、var friendlyWelcome
に"Friendly Welcome"
を代入します。そして、この変数に対して別のテキストを入れることが可能だということを説明します。この例で「互換性のある」という言葉をどう読み解いていくかですが、最初の行では型推論が行われているので少し分かりにくいです。
明示的に型アノテーションをしてあげる方法もあります。例えば、var friendlyWelcome: String = "Friendly Welcome"
とします。この場合、大事なのは、文字列型(String型)としてfriendlyWelcomeという変数を定義しており、この変数に型互換性のある別の文字列を入れることができるということです。
しかし、文字列リテラルがString型と互換性があるから入れられると説明するのは少し不自然です。むしろ、String型はString型の変数に入れられるという解釈の方が自然です。3行目の例では、同じ型(String型)を入れているため、「互換性がある」と言うのは少し曖昧です。
もう少し分かりやすい例を挙げると、クラスとサブクラスの関係などが挙げられます。例えば、ベースクラスとサブクラスを用意します。サブクラスはベースクラスと互換性があると言えます。オブジェクト指向の言い方としてはあまり一般的ではないですが、上位互換として考えることができます。このように定義したオブジェクトがベースクラスだった場合、サブクラスも互換性のある型として扱うことができます。
この章で言いたいことは、あらかじめ宣言した変数の型は変更できないという事実ですね。これは前回のどこかで触れていたと思いますが、どこだったかはちょっと忘れてしまいました。その部分も確認しながら、次のセクションに進んでいきたいと思います。 とりあえず、一旦宣言した場合にはそれを変更できない。そうやって変更できないけれど、互換性のあるものは入れていける、という話ですね。
ちなみに、他に互換性のある例を思い浮かぶ方はいますか?今、紹介したのはベースクラスとサブクラス、要はオブジェクト指向ですね。他にいくつぐらい思い浮かびますかね。2つぐらいしか思い浮かばないな。何かこの他にも互換性があって代入可能な例があります。
まずどれからいこうかな。分かりやすそうなやつ、どれも分かりやすいのか分かりにくいのか分かんないですけど、要は互換性のあるものっていうのは、まず真っ先に出てくるもの、それに限るのかな。要はサブタイピングっていうポリモフィズムがあるんですけど、それに当てはまるものがまず互換性のあるものとして考えられます。
例えば、バリューのInt
型がオプショナルですよ、みたいなとき、このようなときに、バリューに対してあとから10
みたいな値を入れられる。これInt
のオプショナル型と捉えることもできますけど、純粋なInt
型として扱うこともできますよね。このときにバリューはInt
のオプショナル型なんだけれども、そこに互換性のあるInt
型を代入することができますよ、という例が一つ挙げられます。
あと他にも、例えばある変数がCustomStringConvertible
に準拠しているものみたいなふうにして、ここに例えばString
の値を入れていく。これも互換性のあるものですね。
今回、変更可能という話に着目されています。ここにInt
型とかも入れられますよね。ここまでくると一瞬混乱する人も出てくるかもしれない。当たり前なことなんですけどね。CustomStringConvertible
に準拠したものには入れられるし、入れ替え可能だという話ですね。
例えば、バーとしてあるSomeHashable
としてInt
型を入れられますよね。これは互換性のある型に変更可能かどうか、そもそも何と互換性があるのか。この辺り面白いですよね。例えば同じノリでやってみましょうか。さっきのように、ここに数字を入れるんじゃなくて、テキストを入れておいて、あえてストリング型を明記しておきます。こうすると、まずこのあとBにInt
型の10
を入れようとすると、Int
型自体はハッシュ可能ですよね。いいんですよね。
とりあえずコンパイルタイムエラーが出ますね。当たり前ですね。このように、SomeHashable
には代入できないというふうになるわけです。ただし、あらかじめInt
型の20
とかが入っているものに対してなら…コンパイル通らないですね。通らないんだ。そうなのか。SomeHashable
にInt
を入れられませんよ、18行目のところですね。これは通りますよね。
また、実行できなくなりましたね。ここは実行がちょっと大事ですね。なんとなく違和感を覚えるんですが、Some
っていうのは不透明な型っていう仕組みになっていて、何らかのSomeHashable
が入るというふうに書いてありますが、代入した値によってちゃんとコンパイラがInt
が入っていることを認識してくれます。20入っていますね。ここで18行目を実行させると、やっぱり駄目なんですね。駄目でしたね。自分のところでもやってみましたが。
どうですか?違和感ありませんか?じゃあ何が再代入できるんだろう?自分だけが違和感を感じているのかな。関数を通してみましょうか。SomeHashable
で、これで例えば10
とか返すでしょ。そして、これでバーティートでゲットバリューでしょ。ここまでオッケーですよね。 ```swift
cに20を再代入したいんですけれど、これはどうするのか。例えば、cに対して c += 20
という操作はできるのか。これも駄目ですね。SomeHashable
型は再代入できないようにしている可能性がありますね。なるほど、その辺を Integer
型にしてみましょう。また動かなくなったかな、とりあえず落としましょう。
そうすると var
の意味がなくなってしまいますよね。でも、あれは SwiftUI の場合、some
の body はどこかのプロパティに入っているわけです。SwiftUIの some
は基本的には再代入できない、というかミューティングがないのでそもそも再代入はできないということですね。
あ、なるほど。できた。足し算はできましたね。String to Integer の変換もできた。なるほどということは、なるほどですね。SomeBinaryInteger
でその型が何であるかは分からないから T
に Int
型を代入しようとしても、ここの T
が SomeBinaryInteger
で中身が Int
型になっているということはコンパイラーは一応把握しているけれど、こっちの Int
型と同じ型かどうかまでは判断できないっていうふうになっているのかな、多分。
でも B
も SomeBinaryInteger
にすれば、コメントアウトしたところは通りますね。発射可能を SomeBinaryInteger
に変えると通りますね。
確かにね、どこだ、自分のところ。ここ引っかかった。Int
とか、そういうことか、なるほどね。これは、確かに。これでもしかすると SomeBinaryInteger
が ExpressibleByIntegerLiteral
に準拠しているから、自己にキャストするよっていうことですかね。そうかもしれない。
ちょっと SomeHashable
にするとリテラルにすればいける。いけなかった。SomeHashable
がリテラル変換できないはずなので駄目ですよね。ここをエクステンションして何らかの形で ExpressibleByIntegerLiteral
とかに変換できればまた違うんじゃないですかね。どうでしょう、そんな器用なことできるのかな。
これで自分自身に触覚するためには SomeHashable
が、これはなんだ。こんなエラーが出るんだったかな。確かにリテラルじゃなかったら通らないんですね。
今、コメントで貼ったコードですけど、a = b
のところでエラーが起きますね。やっぱり先に Int
に解決しちゃうと駄目なんですね。なるほどね、いいですね。
SomeHashable
の IntegerLiteralConvertible
がちょっとうまくできなかったが、まあいいや、これはとりあえず追い込みでいいのかな。だから独自型でやってみればいいですね。例えば Tract
とかあって、これで整数リテラル対応です。まずこれでいいや、これで。
``` 「プロトコルにして、あとで拡張しておくべきですね。エクステンションを使います。何型にすればいいかな。以前作ったサブクラスかベースクラスがありますね。これにしましょう。こうしておいて、それで SomeValue
としてベースクラスを入れてみます。
そして、この中に再びベースを入れ直そうとするとエラーになります。リテラルから変換できないといけないので、ExpressibleByIntegerLiteral
に準拠させてみます。まず、ベースクラスに ExpressibleByIntegerLiteral
を実装します。これで Int
としてリテラル変換ができるように用意しておきます。
extension Base: ExpressibleByIntegerLiteral {
convenience init(integerLiteral value: Int) {
self.init()
// 何かしらの初期化処理
}
}
これで SomeValue
としてリテラルが入ります。そうすることで、このコードは正常に動作するようになります。しかし、エラーが発生する箇所があります。
コンビニエンス・イニシャライザーを使うので、クラスにも required
イニットを定義しましょう。
class Base {
required init() {
// 初期化処理
}
}
この方法で、リテラル変換を含む初期化がサポートされるようになります。ここで27行目を通る場合と通らない場合を試したかったのですが、as Base
でキャストしてみます。
let someValue: Base = 2 as Base
これで動作すると思うのですが、まだエラーが出る場合があります。しかし、この構造でようやく既存の B
の型と互換性のある型として判断できるようになります。
プロトコルベースで Self
を返すメソッドを作るという発想も確かに有効です。テストしてみたいと思います。セルフキャストを試すので、プロトコルに Self
を返すメソッドを追加してみます。
protocol ValueProtocol {
var value: Self { get }
init()
}
extension Base: ValueProtocol {
var value: Self {
return self
}
}
試してみると互換性が確認できましたが、Playgroundがうまく動作しませんね。required
イニシャライザーをエクステンションで定義するとエラーになる場合がありますので、ベースクラスの定義の中に入れておかなければなりません。
class Base {
required init() {
// 初期化処理
}
convenience init(integerLiteral value: Int) {
self.init()
// 何かしらの初期化処理
}
}
この修正を加えることで、互換性がある型として認識され、期待通りに動作するはずです。長い説明になりましたが、ご理解いただけましたでしょうか。エラーが多くて少し混乱しましたが、これで進めていこうと思います。」 これで動きましたね。ちょっと遊んでみましょう。
まず、let c = b
と予め設定しておいて、b
と c
のアイデンティティを比較すると、false
になるはずです。どうでもいいチェックですが。someValue
は、戻るべきだな。AnyClass
や AnyObject
にしておかないとアイデンティティを比較できませんね。リテラル変換から別インスタンスを作って互換性を持たせていたので、アイデンティティで比較する必要はありません。
エラーの原因は、バイナリで c
が二重に宣言されているためです。エラーが発生している部分はどこでしょうか。c = b
の部分ですね。ここを修正します。試そうと思った結果、あまり関係ありませんでした。
セルフを返す場合、return self
と書くことがリターンになります。同じインスタンスが返ってくるかどうかを気にしてしまいましたが、どうでもいい話でした。プレイグラウンドが再び動かなくなってしまいましたが、省略します。
コメントでもいただいた通り、someValue
自身のセルフが返ってくるので互換性があるように見えますが、サムバリューの中で互換性があるという解釈ができなくなってしまうのです。as
で someValue
としてみると互換性があるかのように見えますが、実際にはできません。バリュー型でセルフを使わない場合、サムバリュー型にキャスト可能です。
不透明な型は、コンパイラが裏で実際の型を把握しているので、コンパイルが通らない場合があります。バリュー型はボクシングという技法を使います; 一種のコンテナに入りさえすれば、どの型でも扱えます。例えば、Any型
ならどんな型でも入れることができるようになります。
他にも AnyObject
というのがあり、Objective-Cの世界観ではすべての型を代入できます。しかし、AnyObject
に変える必要があります。例えば、以下のようなコードです:
var x: AnyObject
x = 123 as AnyObject // キャストが必要
これは安全性のための保護です。キャストさえすれば渡せるが、キャストの手間が混乱の元となります。いずれにせよ、AnyObject
型を使う場合、それが互換性のある型でコンパイラが認識してくれるのが理想です。
これで、互換性のあるものに限って再代入できるということが理解できました。 その中でリテラル変換の面白い動きについて説明しました。気づいていなかった点も多くて興味深かったです。それで、他に互換性のあるものについて話していましたね。さっきオプショナル型を紹介しましたが、ボクシングやプロトコル型、存在型といった概念も紹介しました。最初にサブタイピングについても触れましたが、オブジェクト指向の継承クラスと派生クラスの関係を説明しました。今のところ三つの例が出ていますが、他に何か互換性が取れているものはあるでしょうか。クロージャーもそうですね。
例えば、Int
のオプショナルを引数に取り、Int
を返す関数があったとします。そのようなクロージャーが用意されていた場合、これに対してInt
を引数に取り、Int
を返すクロージャーを再利用することができます。逆の場合もそうです。そのような代入も可能です。この点については両辺反転の話でも説明しましたが、互換性があるということは代入可能であるということです。
例えば、次のように書くとします。
let f: ((Int?) -> Int) = { $0 ?? 0 }
これは両辺に互換性があるため正しく動作します。
また、サブクラスとベースクラスの関係のように、オブジェクト指向のサブタイミングと同じ感覚で理解できます。これも特に難しい話ではなく、慣れてくれば自然に理解できるでしょう。
次に他に何かあったかというと、サブタイミングに尽きる気がします。サブタイプの関係にあるものであれば代入可能であるということですね。値の変更は定数ではできない、というのは既にご存じの方も多いでしょう。
さらに興味深い点として、互換性があれば問題ないということです。こういった調整キャストを使って計算することも可能です。たとえばunsafeBitCast
を使って型を変更する例です。
以下のコードを見てみましょう。
let x = 10
let y = unsafeBitCast(20, to: type(of: x))
ここで、unsafeBitCast
を通すことで、型が一致していれば動作します。このように、裏ではどんな型になっているかわからない場合でも、同じ型を表に持ってくることで互換性が確保されます。
では、時間になりましたので、本日の勉強会はこれで終わりにします。次回は変数や定数の標準についてもっと詳しく見ていきます。お疲れ様でした。ありがとうございました。