https://youtu.be/Dy-meVoWtVc
今回も引き続きThe Basics
の 数値型変換
について眺めていきます。このところ、脇道にそれて気になる細かなところを見ていく機会が多かったので、今回は立ち戻ってスライドに沿って 型変換
の基礎的なところを優先的に見ていく感じに心がけてみますね。どうぞよろしくお願いします。
———————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #122
00:00 開始 00:10 今回の展望 01:09 整数変換の例 01:44 表現範囲を合わせて演算する 02:45 演算結果の型推論 03:16 型が揃えばエラーにならないとは限らない 05:01 何故か EXC_BREAKPOINT エラーになる 06:06 例外ブレークポイント 07:40 オーバーフロー演算子 08:03 オーバーフロー演算子と最適化 09:37 オーバーフロー演算子の使い道 11:32 算術演算子のオーバーフロー検査省略 13:03 安全性を重視した言語 14:40 インスタンス生成時のオーバーフローチェック 17:28 キャリーフラグ 18:20 型変換の慣習 18:40 型変換にはイニシャライザーを使う 21:35 型変換と型キャスト 23:56 変換イニシャライザーの基本流儀 25:40 型変換の詳細 26:24 想定外の変換はサポートされない 26:54 型変換の透明性 28:31 既存の型に独自型からの変換機能を追加する 29:48 Int(value) と value.toInt() 31:56 より安全な型変換を実現 36:49 勘違いと次回の展望 ————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #122
では始めていきましょう。今日は引き続き型変換のお話です。ずいぶん時間をかけて話してきましたが、特に数値型の変換については深く掘り下げて話してきたため、最近は少し脱線しがちでした。
もちろん、脱線は悪いことではないのですが、基本的な知識を抑えておくことが理解を助けることもありますので、今日はスライドに沿って話を進めていくことにします。ただし、途中で話題が脱線しても構いません。その場合も柔軟に対応しながら進めたいと思います。
さて、前回のスライドの内容に少し触れて、次のスライドに進みましょう。ここには定数の変換の例が載っています。2つの定数(例として 200
と 1
)がありますが、名前の付け方は実際にはあまりしないようなものですね。ただし、例示として使います。
以前、異なる型の値を計算する際には意図的に型を合わせる必要があるという話をしました。具体的には片方が UInt8
で、もう片方が UInt16
の場合、計算する前にどちらか一方に合わせるのが一般的です。この場合、UInt16
に合わせるコードが示されています。これにより、両方の値が UInt16
として計算され、正確な演算が可能になります。
型が同じだからといって、必ずしもエラーが発生しないわけではありません。特にオーバーフローには気を付ける必要があります。例えば、Int16
型の変数 A
に 32700
、Int8
型の変数 B
に 200
を代入しているとします。この2つを足し合わせると、型を合わせて Int16
に変換したとしてもオーバーフローが発生します。
最近のXcodeでは、オーバーフローが発生するとブレークポイントで止まる傾向があります。以前のバージョンではもう少し具体的なエラーメッセージが表示されていた気がしますが、自分の環境だけではなく、他のMacBookやMacStudioでも同じ現象が見られます。Xcode全体での挙動かもしれません。
このようなシンプルなコードではブレークポイントが設定されるのは普通ではないので、何か設定が影響しているのかもしれません。設定を確認してみても良いかもしれませんね。 ブレークポイントについて話している内容ですね。全ての例外をキャッチできるブレークポイントがありますが、デバッグのときにとても便利です。ただし、このブレークポイントがうまく働かない場合もあるので、その点は不便に感じるかもしれません。これが自身の環境だけの問題であれば修正は早いですが、他の環境でも同じ問題が発生するなら、次回のアップデートで修正されるかもしれません。
オーバーフローに関しては、Swiftではランタイムでチェックが行われるため、アンドプラス(&+
)の演算子を使ってオーバーフローを無視することができます。しかし、これにはオーバーフローチェックをしない分、パフォーマンスが向上するという利点があります。
たとえば、ループでたくさん足し算をする場面などでは、アンドプラスを使うことで明らかにオーバーフローはしないとわかっている場合、パフォーマンスが向上します。現在ではオーバーフローチェックをしない方が速いですが、将来的にはオーバーフローチェックがスタンダードになる可能性もゼロではありません。
実際のところ、アンドプラスを頻繁に使うことは少ないかもしれませんが、意識して使うことで無駄なif文を減らし、パフォーマンスを向上させることができます。
前回の話にもあったように、Swiftでは基本的にint型を使っていく方針です。サイズを意識するよりもint型だけで表現するほうが互換性も高まり、通常問題なく動作します。そのため、オーバーフローを意識せずに確実に高いパフォーマンスを得たいときにはアンドプラスを使用するのも一つの手です。
オーバーフローに関する考え方は、C言語的なアプローチとも似ていて、それを知っていると「&+」を使う場面が明確に理解できると思います。オーバーフローを意識するよりも、条件分岐を減らしてパフォーマンスを向上させることが重要です。 Swiftで数値を操作する際には注意が必要です。特に整数のオーバーフローに関しては、適切に対策を講じる必要があります。Swiftコンパイラにはオプティマイズプラグがあり、これをアンチェックにすると、プラス(+
)演算がオーバーフローチェックをしなくなり、&+
と同等の速度で動作するようになります。しかし、「アンチェックの最適化」は注意が必要です。プログラマーが独自にオーバーフローの管理をすることは、広範囲なバイナリ全体に影響を及ぼす可能性があり、非常にリスクが高いです。インデックスがオーバーフローするかどうかのチェックも重要です。ランタイム上でのオーバーフローテストと、コンパイル時の最適化の違いを理解しておくことが重要です。
たとえば、Swiftでは配列のインデックスがオーバーフローするかどうかをチェックし、フェイタルエラーを投げる仕組みがあります。これも一種のオーバーフローチェックですが、「アンチェックの最適化」を導入することで、これが無視される可能性があります。もしも大量の計算、特に配列の計算が必要な場面では、適切なチェックを行うことが重要です。
Swiftは安全性を重視しているため、基本的にはオーバーフローチェックが優先されています。これにより、高速な演算が求められる場面でも、安全性を犠牲にすることなく対応できるようになります。
ただし、プログラマーが意図的に使う場面もあります。&+
や&*
などの演算子を意図的に選んで使うことで、特定の条件や要件下での最適化が可能になります。
複数の同じ形式を持つ機能については、特にイニシャライザーを使う際には注意が必要です。たとえば、int16
が欲しいときには、Int32
にキャストしてから戻すという方法もありますが、これにはオーバーフローチェックが含まれています。以下のように使います:
if let x = Int16(exactly: someInt32Value) {
// `x`が正常な範囲内に収まっているときの処理
} else {
// オーバーフローが発生したときの処理
}
ただ、この方法も万能ではありません。ログを確認して、エラーの発生箇所を特定し、必要に応じてデバッグを行うことが重要です。たとえば、int32
への変換でエラーが発生した場合、その原因を明確にするために、エラーメッセージやログを確認することが有効です。 なので、これでいいんだと思います。そうすれば適切に処理できるのではないでしょうか。ここでは、一つ上の精度に変換した上で Int
型で exactly
を利用して元に戻すという方法を取っています。これで一つのチェックはできますが、ちょっとオーバーな表現かもしれませんし、Int64
型ではこの方法で対応できないので、賢いやり方とは言えません。もう少し適切なチェック方法があるかもしれません。
例えば、マシンのようにオーバーフローフラグやキャリーフラグがないと辛いですよね。キャリーフラグは、オーバーフローするとフラグがオンになるというとても便利な機能です。しかし、Swift にはそのような機能はないようです。こういうときはどうするのでしょうか。やりがいはあると思いますが、とにかく頑張るしかないということにしておきましょう。
さて、スライドに戻りましょう。型変換の基本的な方法について話します。Swift では、基本的に型変換を行う際に「型名(丸括弧)値」という書き方を用います。例えば、UInt16(1)
のような表現です。これは Swift の一般的な型変換の書き方で、特に特別なものではありません。これはイニシャライザーを呼び出すための構文です。Swift ではこの表現を使って型変換を行い、イニシャライザーを用いて型変換を行うというルールになっています。この点は、Swift らしいコードを書く上で重要なポイントです。
次に、C言語などの他の言語では異なる書き方が存在します。例えば、int
型の変数 A
と int
型の変数 B
を型変換する際の方法です。これはプレイグラウンド上で実行するので、コンパイルエラーは無視してください。異なる言語では両方の書き方が許されることがあります。
例えば、Double(A)
という書き方や A as Double
のようなキャストを書くことができます。Swift では、二行目のような書き方はできないようです。Double のイニシャライザーに A
を渡していると解釈されるのがポイントです。推奨される書き方としては、A
が int
型の値であり、それを Double
型に変換する場合、Double(A)
という書き方をします。
このように、型変換についてはイニシャライザーを用いることが Swift では一般的です。API デザインガイドラインに沿ったコードを書くためにも、この点をしっかりと押さえておくことが重要です。 そうなので、型変換といったら変換イニシャライザーを通して変換するということになります。もしキャストするという話になったとすると、例えば、ある基底クラスとサブクラスがキャストできる関係にある場合の例を挙げますね。
たとえば、クラス Base
型があって、そのサブクラス Sub
があるとします。そして、Sub
のインスタンスを Base
型にキャストするという場面です。このとき、コード例を示すと以下のようになります。
class Base {
// Baseクラスの定義
}
class Sub: Base {
// Subクラスの定義
}
let x: Base = Sub() // Sub型のインスタンスをBase型にキャスト
この Sub
クラスのインスタンスを Base
型の変数に代入することでキャストが行われています。このとき、変換先の型が変換イニシャライザーを持っている必要があります。例えば、次のようなコードがあるとします。
class Base {
init() {
// 初期化処理
}
}
class Sub: Base {
override init() {
super.init()
// Subクラス独自の初期化処理
}
}
let x: Base = Sub() // 正しくキャストされる
このように、基底クラスの Base
に対して Sub
クラスのインスタンスをキャストするためには、Sub
クラスが基底クラスの初期化処理を通過する形で初期化されなければなりません。
型を定義している人が、その型に対して安定した変換方法を把握することが求められるわけです。これはとても合理的な型変換の仕組みです。型変換において、変換先の型が予期されるパラメータを明記することはとても大切です。
イニシャライザーを使った型変換は、特にSwiftにおいて非常に重要です。例えば、以下のように UInt16
型を UInt8
型の変数から初期化する場合、このイニシャライザーを利用します。
let smallValue: UInt8 = 42
let largeValue = UInt16(smallValue) // 正しい型変換
これは、UInt16
型が UInt8
型を引数に取る変換イニシャライザーを実装しているためです。このイニシャライザーを使って型変換が成立しています。
Swiftでは、任意の値を渡すことができず、予め想定されている値しか渡すことができないため、安全性が担保されているのがメリットです。他の言語、特にC++については独自の型変換演算子を実装できる場合もありますが、C言語の場合はコンパイラーによる暗黙の変換方式しか利用できないです。
冒頭で述べたことや例を踏まえると、イニシャライザーを使って安全で確実な型変換を実装するのがSwiftにおける推奨される方法であり、他の言語に比べても非常に合理的なアプローチだと言えます。 ただし、Swiftの場合、イニシャライザーを定義した人間が変換方式を設定できるので、これは暗黙的な変換ではなく、少し言い過ぎかもしれませんが、ロジックがコンパイラーに依存しなくなります。特に独自の型を定義したときには、それが明確に表に出てきます。型を定義した人や、その型を使用する人が型変換を設計するため、どういう変換をするかはソースコードを見れば確認できるのが良い点です。
特にオープンソースな環境では、コンパイラー自体もオープンソースなので、追跡していけばどのように変換が行われているかを解析できます。ただし、ライブラリのソースコードとコンパイラーのソースコードは敷居が異なるため、この違いが大きなポイントです。ライブラリやソフトウェアのより高い応用レベルで変換が明記されるのは良い点だと思います。
ここがまた面白い点で、ユーザー定義の型を既存の型に変換させたい場合、イニシャライザーを既存の型に拡張することで、変換ロジックを壊さずに搭載できるのです。要するに、例えば「Value」という構造体が存在していて、これが内部的にInt
型のプロパティを持っているとします。こういった設計だった場合、Int
型に変換する必要が生じます。このとき、変換先に適切なイニシャライザーが存在しないと、以前はValue
型に対してtoInt
のような変換メソッドを実装し、一旦Int
型に変換する方法を利用していました。
例えば、以下のような方法が一般的でした:
struct MyValue {
var rawValue: Int
func toInt() -> Int {
return self.rawValue
}
}
let myValue = MyValue(rawValue: 10)
let intValue = myValue.toInt()
しかし、こういった方法だと統一感が欠け、煩雑になる問題がありました。そこで、Swiftではエクステンションを利用して、既存の型にイニシャライザーを追加することができます。これにより、以下のように変換方法を明記できます。
extension Int {
init(_ value: MyValue) {
self = value.rawValue
}
}
let myValue = MyValue(rawValue: 10)
let intValue = Int(myValue)
このようにエクステンションを使うことで、Int
型にMyValue
型を直接渡して変換ができるようになり、コードがより統一されて美しくなります。このアプローチにより、型変換が誰でもできるようになり、さらに型を定義した人が自身の構造体に最適な変換方法を書けるため、コードがより安全で理解しやすくなります。
要するに、型に精通している人がその型の変換方法を明示することで、コードの整合性と安全性を高めることができるのがSwiftの良いところです。 また、マイバリューに他の値から変換したいときには、イニシャライザーを利用するのが良いでしょう。エクステンションとしてマイバリューに対してinit
を追加することで、例えばInt
型から変換する場合にも、マイバリューの詳細を知っている人がInt
型の値を受け取るという形のコードが書けます。そのための判断として、ローバリューに入れておけば良いといったものがあります。マイバリューをよく知っている人がイニシャライザーを書くことで、安全に動作することが期待できるでしょう。これにより、マイバリューとしてInt
型を受け入れることができるようになります。
ただし、これはコンパイラが安全性を担保しているわけではありません。型を設計する人が適切な方法で型変換を記述することによって、知識のある人が書くコードが安全になるという感覚です。こういった安全性の向上のアプローチも非常に興味深いものだと思います。このような変換イニシャライザーという発想は個人的に非常に好みです。
例えば、「toInt
」というようなメソッドを用いることで、変換イニシャライザーで適切に表現できる場合があります。この考え方に慣れると、より良いコードが書けるようになるでしょう。また、マイバリュー型を利用する場面においても、適切な理由でこの型を使ってもらえると安心です。
ただし、「toInt
」を使わなければならない場面もあります。例えば、プロトコルがあり、そこで変換イニシャライザーを規定している場合です。ここで、例えば「IntConvertible
」というプロトコルがあって、Int
型に変換できるというものを定義したとします。このときに「toInt
」を追加する必要があるのですが、この例ではNSNumber
に変換イニシャライザーを設置することができないことがあります。
イニシャライザーを利用した型変換は、時として混乱を招くことがあります。そのため、利用する際には事前に頭を整理しておくと良いでしょう。また、このような場面では「toInt」を必ずしも匿名できるわけではないこともあるので、その点を理解しておくとプロトコル設計時に役立つかもしれません。
時間になりましたので、今日はここまでにいたします。