https://youtu.be/2MJKfN7oaoQ
今回も引き続き The Basics の 定数と変数
について見ていきます。宣言方法を詳しく眺めていっているところですけれど、今日は型注釈まわりがテーマになりそうです。よろしくお願いしますね。
——————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #93
00:00 開始 00:25 前回に関する告知 01:08 今回の展望 02:00 型注釈 03:41 型注釈の添え方 04:16 型注釈という言葉 08:38 Type Script と型注釈 09:26 型推論の文脈から 09:58 Type Script における型注釈についての考察記事 16:23 iOS 界隈ではあまり使われない言葉? 17:38 型注釈で使う記号の意味 19:06 型注釈についての所感 20:36 型注釈とバリアント型 22:33 NSNumber や NSValue の演算 25:17 引数で型を指定するのも型注釈? 32:24 NSString が NSValue を継承しない理由は? 37:12 複数の変数に対する型注釈 42:02 未初期化でも type(of:) が使える 46:17 type(of:) の挙動が気になりつつ、クロージング ———————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #93
はい、それでは今日も引き続き、定数や変数、そのあたりの基本構文を見ていくことになります。
その前に、前回話した内容ですが、前回の最後の時間には、再帰呼び出しの reduce
を自分で作るというのをやっていました。それが完成したので、興味のある方はアーカイブの方を見てみてください。基本的にはあの時のアプローチで問題なく、一部混乱していただけという感じです。
さて、それはそれとして、今日は前回に続いて型注釈(型アノテーション)について学びます。これは、自分自身あまり馴染みのない言葉かもしれないのですが、変数にその変数が何型かを明記するやり方ですね。Swiftでは、定数や変数を宣言するときに型注釈を添えられます。
例えば、変数名にコロンをつけ、その後に何型かを書くというのが基本的な構文です。これにより、定数や変数がどんな種類の値を保存できるかが明瞭になり、コンパイラーにも分かりやすくなります。これにより、文字列型のところに数値を誤って入れるといった間違いを防ぐことができたり、意図しない演算を避けたり、引数やパラメーターの間違いを防ぐことができます。
型注釈という言葉についてですが、多分 "タイプアノテーション"(Type Annotations)と言った方が一般的かと思います。型を明記する、型を定義する、そんなふうに言うのが自然かもしれないですね。
オブジェクティブCでは "アノテーション" という言葉が使われていたので、同じように考えてもらえば良いと思います。例えば、変数 ウェルカムメッセージ
が String
だよという宣言ですね。
この型注釈自体は、型推論ができる場合でも、わざわざ明記して注釈をつける形になります。これによってプログラムが安全に動作するように制約を付けることができます。
では、実際に例を見てみましょう。次のように書くことができます。
var welcomeMessage: String
これは、変数 welcomeMessage
が String
型であることを明示しています。
このように型注釈を付けることで、いろいろなミスを未然に防ぐことができるわけですね。例えば、間違った引数を渡すようなことがなくなるということです。
このように、型注釈はプログラムの安全性に大いに役立つので、積極的に使っていくと良いでしょう。 さて、次の話題に移ります。普通に変数宣言の時に型を明記できるという話ですが、この点について書かなくてもいいかもしれません。現在スライドに出している内容が原文そのもので、「Type Annotations」という専門用語がイタリック体で示されています。スライドに書いてある内容と一致しているので問題ないと思います。
例として示したのは、welcomeMessage
という変数に String
型を保存できることを示すためのタイプアノテーションの話です。これがまさしくタイプアノテーションというわけですね。コメントでいただいた意見についても参考にし、TypeScriptの記事が多数出てくることにも言及しています。
「型注釈」で検索すると、言語によっては一般的に使われる言葉であることがわかります。TypeScriptの文脈での「型注釈」は、変数や関数の戻り値、オブジェクトなどの型を明示することを指しますね。
型推論がない場合は型宣言が必要で、型推論がある場合には注釈として捉えられるのかもしれません。これは興味深い考え方です。型注釈(Type Annotation)について、次のように整理できます。
- 変数や関数の戻り値、オブジェクトなどの型の明示
- 型を明示する構文や機能、または手段
例えば、var welcomeMessage: String
という例では、変数 welcomeMessage
に String
型の注釈を付与しています。これがまさに型注釈を利用する例です。
以下の表現も、型注釈の意味として理解できます。
- 型注釈を利用する
- 型注釈を付与する
- 型注釈をつける
- 型注釈を行う
- 型注釈を含める
- 型注釈を書く
- 型注釈を記述する
一般的に、これらの表現はどれも同じような意味を持つと言えます。最終的に、「型の明記を記述する」と表現されることが多いようです。
記事を最後まで読んで、型注釈に関する理解が深まったと思います。まとめとして、型の明記に関して書かれた記事を見て、自分自身も同じような解釈に至ったという感想です。
このような感じで、TypeScriptやその他のプログラミング言語においても、型注釈は非常に重要な概念だということがわかりました。 型注釈と型推論について、だいたい自分の中のイメージでOKな気がしています。型推論の文脈という発想も面白かったです。型推論がなければ、もしかすると宣言で済ませてしまうのではないか、という話でしたが、確かにその通りかもしれないですね。とりあえずスライドに戻ってみましょう。
型注釈について、iOS界隈ではあまり聞かない印象です。おだしょーさんはどうですか?
おだしょー:ないですね。
りなたむ:私もこのスライドを用意するまで、型アノテーションという言葉をちらっとは聞いたことがありますが、これが型注釈だと認識してはいませんでした。この解釈が間違っていなければ、これは型注釈と認識しておきましょう。
日本人の感覚で日本語にタイプアノテーションという言葉がないだけかもしれません。英語に慣れ親しんでいる人にとっては、何の変哲もない言葉かもしれないですね。型注釈で使うコロンは「of type」みたいな意味ですね。たとえば、var welcomeMessage: String
という感じで、ストリング型のウェルカムメッセージ変数を宣言します。
Swift言語は流暢な英語表現を目指している割には、どうして var welcomeMessage: String
という記述形式なんでしょうね。AppleScriptは英語表現に非常に近い構文の言語ですが、逆に読みづらいのかな。
まあ、とにかく var welcomeMessage: String
というふうに書くと、ウェルカムメッセージ変数はストリング型の値だけを保存することができるという話です。これが型注釈の説明として挙げられていました。型アノテーションの面白い点が他にあるかどうかは分かりませんが、次に進みましょう。
型アノテーションした型しか保存できなくなるということで、これによって意図しないデータが入らなくなり、不要なエラーを防ぐことが出来ます。意図したデータしか入らないようにすることで、くだらないエラーが起こらないようにする、というのが型注釈の効果です。
話している感じでは、特に難しいことはなく、話すこともあまりないですね。とりあえず書いてみますかね。var welcomeMessage: String
で、この場合、数字などは入らなくなります。これが挙げられていた話ですかね。
また、この勉強会で以前に話したことですが、JavaScriptコアを使って演算がいろいろと奇妙になることもあります。型アノテーションがないと、文字列として解釈されたり、数値として解釈されたりすることがあるということです。JavaScriptの標準の変数はバリアント型であり、一つのインスタンスにいろんな値を代入することができます。
型を明記する言語であっても、SwiftのNSNum型などもバリアント型として認識しており、NSValueも同様です。NSNum型もバリアント型として使えるので、中に入っている値を任意に解釈して、たとえばboolValue
や integerValue
などがあります。同じインスタンスでも、状況に応じて型を調整できます。NSNum型は演算もできるのでこういった点が面白いですね。 あんまり意識していなかったけど、「A足すB」とかやるとどうなるんだろう。これは as Decimal
なんだね。演算は同じ型同士での足し算演算が提供されていないんですね。
NSDecimalNumber って、実際にはよく使わないかもしれないけど、比較的使う可能性のある型だと思うんです。でも、NSDecimalNumber に足し算がないんだね。NSDecimalNumber って stringValue
はあるのかな。あ、当たり前か。
でも、NSNumber を使うと結局数字に丸めていくって感覚だから問題ないですね。どっちかっていうと、NSValue か。恒例の足し算がどうなるのっていう疑問が正しかったですね。
いずれにしても、こうやって足し算演算で、あ、この辺りにもないんだ。完全にインターフェースが計量なのかな。NSValue で getValue(ofType:)
ってあったよね、あれもありますね。value(ofType:)
ってある。これを value(of: String.self)
ってやるのかな。まあ、いろいろ脱線してますけどね。
例えば、Int.self
と Bool.self
でやってみるとどうなるんだろう。Bool.self
の演算はどうなるんだろう。こういうのは本当にカットしたほうがいいんですけどね。とりあえず Int.self
だけで走らせてみるとランタイムエラーが出て、Bool
もランタイムエラーになりますね。String
は空文字になるのかな。えっと、動かなくなってしまったかな。まあいいです。
とりあえず、ランタイムエラーじゃなかったから、きっと何か空文字か何か得られているんでしょう。これもある意味型注釈とは言わないかもしれませんが、これはストリングですよと書いています。形式的には型注釈をしているようなものですが、言語構文としての型注釈ではないので、ちょっと違うのかなと思います。今回は違うということで進めます。
注釈というよりも、単純に型を明示的に引き継ぎ渡しているだけですね。テンションをつけているというよりは、単純に指定しているだけという感じでしょう。
次に、NSValue がそもそもどういった目的で存在しているのか、それにもよるんでしょうね。NSNumber が継承しているでしょ。NSValue の値を抽象化して持っているのでしょうか。NSNumber の初期化の時に、String.self
みたいなことをラップしてくれているのかもしれません。NSNumber の場合は数字を想定して変換しているので、それ以外の時に問題が起きるのかもしれません。
他の独自の例えばシリアルナンバー型などを NSValue から継承させて、value(ofType:)
を使って値を取り出すことも汎用的にインターフェースを提供している場合があるのでしょう。NSValue の配列として扱えるようなインターフェースを提供して、それによって汎用的な型変換として value(of:)
を提供しています。例えば、for ループの中で for value in values
のように使えます。
ここで汎用的な型ターゲットタイプなどをあらかじめメタタイプで用意しておいたりすると、何型を取っていたっけ、タイプオブストワードタイプだったかな。このストワードタイプがどこにあるかですが、おそらく NSValue が持っているのでしょう。Import しているファンデーションの中にあるはず。出てこないのが謎ですが、メソッドのジェネリクスだったというのを考えると、戻り値を合わせているんでしょうね。ありがとうございます。 そうすると、ここでメタタイプを使用してみましょう。例えば、let targetType = Int.self
のように指定します。この状態で、let result
を用いて型をターゲットタイプに変換すると、result
はInt
型になります。
次に、NSValue
についてです。NSValue
を使用することで、すべての値を共通のインターフェースで扱うことができます。これは、例えばNSNumber
とは異なり、String
のような型も扱うことが可能です。ただし、String
に変換できない可能性があります。特に、NSValue
を検証するクラスが必要になる場合には都合が悪くなることがあります。NSValue
を使用する理由は、共通のインターフェースを提供するためです。
おそらく、NSNumber
ではこのメソッドがオーバーライドされており、与えられたタイプがString
型の場合にはStringValue
をリターンするような実装になっていると考えられます。しかし、NSString
はNSValue
を検証しないのが興味深いですね。おそらく、NSString
はもっとシンプルな数値を対象としているのではないかと思います。他にも理由があるかもしれませんが。
また、NSString
がNSValue
を検証しない理由については、パフォーマンスの観点から厳密に制御されている可能性があります。NSString
は内部的にさまざまな型がありますので、その点でも別の実装がされていると考えられます。NSValue
を使用することによるパフォーマンスへの影響を避けるために、NSString
ではNSValue
が使用されていないのかもしれません。
次に、トールフリーブリッジなどについても考慮すると、NSValue
がNSNumber
やNSDecimalNumber
などのサブクラスで提唱されているのかもしれません。ただし、NSString
との互換性については、ややこしい問題がある可能性があります。
確かに、NSNumber
とNSString
の違いはパフォーマンスの観点からも重要です。たとえば、NSNumber
でliteral
を指定した場合と、NSString
で指定した場合では型が異なることがあります。NSNumber
はリテラルとして扱われる一方、NSString
は別の取り扱いがされるのかもしれません。
ここで、型注釈について話を戻しましょう。複数の宣言の中で1つだけ型注釈を行うことができます。たとえば、以下のようにします。
let r, g, b: Double
この場合、r
、g
、b
はすべてDouble
型になります。また、g
だけをInt
型にしようとすると以下のようになります。
let r: Double = 0.0, g = 0, b: Double = 0.0
これはコンパイルが通りません。全ての変数を同じ型にする必要があります。試しにコンパイルしてみると、g
だけInt
型にすることはできないことがわかります。以下のように修正するとコンパイルが通ります。
let r: Double = 0.0, g: Double = 0.0, b: Double = 0.0
このようにすることで、g
もDouble
型として扱われることになります。 予想通りですね。皆さんの予想通り、 Double
でした。じゃあここでもう一回 0
にしたいな。要はさっきの b
に初期値を代入していたのとちょっと違う。これだとちゃんと Int
になるのかな。あ、なんですね。これ多分小数リテラルのデフォルトの型とかで個別に見ていくんじゃないですかね。だからパールイコールもタイプアノテーションというよりかは 0.0
の小数リテラルによって決められた型みたいな感じがしますよね。
そうですね。だからきっと代入式で何かしらのことを書けば、その型に優先的に適用されていくっていうことになるんですよね。こう書いてみれば当たり前ですね。ここで Double
になるはずもなく。そうですね。
で、あとは g
が Double
で b
が何にも書かないわね、当然のようにコンパイルエラーですよね。こう考えて見ていくと、まあ自然ですね。これでこうやって Int
とかできるし、ここでね例えば c
、d
とかやると、type(of: c)
とか type(of: d)
とかは Int
ですよね。そうですね。
ここで代入式を抜くと、r
と g
は Double
で、それ以外は Int
ですね。こういうふうに複数の変数を1行で宣言するときに、適切にタイプアノテーションを入れていって、今見ていったみたいに値が与えられていたらその値の型と、でなければ型アノテーションの型で、型アノテーションと値が与えられていたらアノテーションと値が一致していなければエラーになるみたいな感じですね。
面白いですね。コメントをいただいていますけど、「definite initialization」、要は値を入れていないのに type(of:)
が動く、これ面白いですよね。何か最適化が図られている感じがしますよね。代入される前に使っちゃうとコンパイルエラーになるのに。そうなんですよね、何か特別な措置がされてる関数ですよね。
こういうの自分で定義できるのかな、できないですよねきっとね。どうやって実装してるんだろうな。メタタイプ取ればできるのかな。お時間になってしまったので、サクッと終わらせますね。えーと、ここで any(String)
として、 any(_: type:)
とか取るとこれに渡せるのかな?ちょっと興味深いですね。アクションに対して、あ、これインスタンス渡してますよね。ここで r
結局 type(of:)
になっちゃった。なんかめちゃくちゃになってしまった。
そうね、面白いですね。自力でこういった関数作れるというのを見つけられたら、ぜひ教えてほしいな。組み込み関数なのかな、type(of:)
って定義はたどれないんだ。なるほど、たどれなかったんですよね。ドキュメント的にはありますね、これですね。ドキュメント的にはこれになってて、p
とメタタイプジェネリックスで p
を取って。でもこれはユーザー定義はダメですね。この書き方だとインスタンス取りたがるから、やっぱりそうすると仮にその定義がちゃんとあるものだったら、ジェネリックファンクションとしてあるんだ。そうね、やっぱりちょっと自分でこの構えに渡せるっていうコードを書くっていうのは、このドキュメントを見る限りではちょっと難しそうですね。
エラーになるもんね。そうですね、ここだ。書き構えに使っちゃダメって言われちゃいますね。昔はこの type(of:)
関数じゃなくて dynamicType
だったのが何かの事情で、この関数に名前が変わったんですよね。大文字の Self
ドット、小文字の self
じゃダメでしたっけ。
大文字の Self
。ここだと R
クラスじゃない、わかんない、すいません。R
って何言ってるんでしたっけ。これ R
はここの変数ですね。あー、そうか。経験した変数。んー、じゃあダメか。そうですね。
これをスタートすると、何かしらメンバーとして実装しないとダメですね。でももしかするとこの書き方、何か面白い使い方が見つかるかもしれないですけど、うん、なるほどなるほど。
昔は dynamicType
っていう特別な名前だったんですよね。それが特別な関数になったということなんでしょうね。これうっかりすると変な動きを見せる箇所が作れそうな気がするな。オプションと併用したときに nil
が入っちゃうのかどうなのか、このあたりちょっと時間になったので保留にしますけれど、ここちょっと気にしてみたほうがいい気がしますね。極端に危ないことはないと思うんですけど。
とりあえず、ふわふわした感じになりましたが、今日はこれぐらいで終わりにしておきましょうかね。今日の勉強会はこれで終わりにします。お疲れ様でした。ありがとうございました。