https://youtu.be/VXKslbiaGZ8
今回も引き続き A Swift Tour
の 列挙型
から 列挙子
に注目して眺めていきます。列挙型にメソッドを定義するところから、そうやって列挙型を扱う中でどのように列挙子を扱っていく感じになるのかといったあたりを整理していきます。
それが終わって時間があったらそのあとは列挙型の特徴的な機能ともいえる関連値について見ていくことになりそうです。どうぞよろしくお願いしますね。
——————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #58
00:00 開始 00:49 前回のおさらい 03:39 練習問題 04:43 列挙型にメソッドを追加 08:53 プロトコル指向を意識した言語仕様 13:13 メソッドで定義するか計算型プロパティーで定義するか 15:03 前後の文脈から肩が推定できる場面 15:56 複数のパターンを一度に用いる方法 16:39 戻り値の型から推論する 17:25 変数宣言から型を推定する 18:02 引数の型から推論する 19:04 静的メソッドと型推論 22:39 型推論のされ方 25:03 Double 型の静的メンバー 26:44 呼び出しが曖昧になったときの確認方法 28:20 名前空間 32:35 列挙子の参照方法 33:54 列挙子が値を持つ方法 37:56 関連値 45:52 関連値を持つ列挙型でのパターンマッチング 46:32 関連値を持つ列挙子をイニシャライザーとして扱う 50:07 列挙子別に異なる関連値を持たせる 51:47 付属値を持つ列挙型のサイズ 53:56 クロージング ———————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #58
はい、では始めていきますね。今日は、列挙型についてのお話です。この列挙型について、以前からゆっくりと2回に渡って見てきていますが、まだ続きますね。実際、Swiftの中で列挙型は非常に重要な機能であり、機能的にもとても充実していますので、じっくりと見ていくのは価値があるでしょう。
まず、今画面に映しているのは前回お話した資料です。列挙型自体が値になっていて、ローバリューは必要なときだけ使いましょうというお話を前回しました。その中で、今一般的に使われているプログラミング言語の中で、列挙型自体が値を持たないという言語が他にあるかどうかという話がありました。Javaを挙げてもらいましたが、自分はそれを知らなかったので、「ああ、そうなのかな?」と思っていました。調べてみたら、やはりJavaもSwiftのように列挙型自体には値が標準ではセットされていないという特徴を持っていることが分かりました。
Javaには面白い特徴があり、「オーディナル」というプロパティが標準で搭載されています。これを使うと、定義した順番ごとに値が取れます。つまり、0、1、2というように連番で取ることができます。また、「valueOf」という静的メソッドが用意されていて、これを使うと文字列から列挙型をインスタンス化できるという特徴もあります。こういった特徴のある列挙型をJavaは持っているので、Swiftっぽい列挙型でもありますが、もう少し言語的に手厚いサポートが入っている感じがします。初めて見る方には興味深い特徴を持つ列挙型であると思いましたので、興味がある方はJavaの列挙型を調べてみてください。
さて、今日はこの続きです。練習問題があったので、そこから見ていきましょう。列挙型があって、これにcolor
メソッドを追加してみましょうという話です。列挙型は単に列挙子を列挙するだけでなく、メソッドやプロパティも実装できるという、Swiftの大事な特徴になっています。ちなみに、保存型プロパティはダメですが、計算型プロパティは実装できます。この練習問題で、それらを見ていこうという意図があるのでしょう。
ということで、まずcolor
メソッドを追加してみましょう。列挙型があって、普通にこのまま使うとすると、例えば以下のように書けます。
import Foundation
import Cocoa
enum Suit {
case spades, clubs, hearts, diamonds
func color() -> NSColor {
switch self {
case .spades, .clubs:
return NSColor.black
case .hearts, .diamonds:
return NSColor.red
}
}
}
color
メソッドを追加しようとしたときには、普通の関数のように定義します。このcolor
メソッドは色を返さないといけないので、CocoaフレームワークのNSColor
を使用します。コードを見ると、spades
やclubs
の場合はNSColor.black
を返し、hearts
やdiamonds
の場合はNSColor.red
を返すという内容になっています。
では、もう少し冗長な、無駄の多い書き方からやってみましょう。 とりあえず、switch
ステートメントを使って自分自身に対して条件分岐を行います。それで、ケースの内容について後で説明しますが、まず switch
ドット(ドットを用いて)で書きます。具体的には、switch
文で各ケースに対して以下のように処理を記述します。
spade
だったらblack
を返す。heart
だったらred
を返す。diamond
だったらred
を返す。club
だったらblack
を返す。
こんな感じで書いていけば大丈夫です。これで、列挙型に4つのケースを指定することができます。これでSwiftのコンパイラーに通すことができます。
次に、メソッドを呼び出すことができるようになりました。このようにメソッドを搭載することができました。Playgroundが動かないので、一度Xcodeを再起動しますね。
このように構造体やクラスを使っている方であれば問題なく理解できると思います。Swiftでは列挙型に対してもメソッドを搭載できることが特徴的です。構造体や列挙型にメソッドを搭載できるというのは、とても重要なポイントです。
これについて個人的な感想ですが、Swiftのプロトコル指向がその背景にあります。プロトコル指向プログラミングにおいて、すべての型にプロトコルを適用できるようにする必要があります。そのため、プロトコルが要求する機能を各型が備える必要があるのです。例えば、エクステンションを使って、今作った列挙型にCustomStringConvertible
に準拠させるためにはdescription
プロパティを持たせる必要があります。これもメソッドを定義することで実現可能となります。
以上のように、Swiftではプロトコル指向を支えるために列挙型や構造体に対してメソッドを定義する機能が備わっています。それでは、コンソールで実際に動かしてみましょう。成果が表示されるか見てみます。print
を使ってコンソールに出力してみます。
最後に動作を確認できたので、これで練習問題としてはクリアです。 まず、「カラー」はメソッドで用意しましたけど、今時のSwiftだと計算型プロパティで定義するのが一般的です。計算型プロパティにすることで、self.color
のようにプロパティとしてアクセスできるので、自然な書き方になります。
APIデザインガイドラインによれば、計算量が一定である場合には、プロパティにするのが適切です。カラーのような属性は単純な計算で値を返すので、プロパティにするのが望ましいと思います。
型推論についてですが、前後の文脈から型が推察できる場合には型名を省略できます。例えば、スイッチ文でself
を使っているとします。このself
が特定の型(ここではスーツ型)であるため、その型とパターンマッチする対象も同じ型だと推察できます。したがって、型名を省略することが一般的です。
また、パターンマッチでは複数のパターンを一行で書くことができます。例えば、次のように書けます。
case .clubs, .spades:
return .black
case .hearts, .diamonds:
return .red
ここで、何気なく省略していますが、NSColor.black
とNSColor.red
も文脈から戻り値の型がNSColor
であると推察できるため、省略可能です。
さらに、変数の型も前後の文脈から推測可能です。例えば、次のように書くことができます。
let value = someFunctionReturningSuit()
この場合、value
の型はSuit
型であると推測されます。
まとめると、計算型プロパティの使用、型推論、パターンマッチの省略、これらに気をつけることで、Swiftコードをより簡潔で自然な形にすることができます。 なので、普通は渡す値として具体的な値(例: .diamonds
)を省略することができます。同じ発想で、メソッドがあった場合にも、suit
型の値を取るときに、省略可能です。例えば、doSomething
のパラメーターはSuit
型を取りますよね。ということは、Suit
型の何かを渡すよね、という感じで省略可能です。これはSwiftの基本的な動きですが、こういうふうに書き方を工夫することでコードの簡潔性も向上し、コーディングが楽になります。
積極的に使われているこのような書き方です。ここからは余談ですが、この話は列挙型に限ったことではなく、クラスでも他の構造体でも似たような性質を持ちます。例えば、NSColor
がそうですが、独自に定義した場合でも同様です。
例えば、Type Bar
というデータがあるとします。このデータがValue
型のインスタンスを持つときに、その値が0
を示す場合、Value
型インスタンスの一部として0
を定義することができます。以下のようにします。
struct Value {
var value: Int
static let zero = Value(value: 0)
}
このようにすると、列挙型とまったく同じように書くことができます。インスタンス化する場合、通常は型名と値を指定して書きますが、Value
型に対しても型名を省略して .zero
で代入できます。メソッドのパラメーターとしても、.zero
と省略して渡すことが可能です。
さらに余談ですが、スタティックなものが候補に上がってくる場合、イニシャライザーも同様に書くことができます。例えば、以下のようなコードです。
struct Value {
var value: Int
static let zero = Value(value: 0)
static func getZero() -> Value {
return .zero
}
}
このように Value
型の静的プロパティ zero
を使用し、getZero
メソッドで .zero
を返すようにします。
型推論を行ってくれるため、列挙型、構造体、クラスが同様に動作します。例えば、doSomething
が Value
型のインスタンスを取る場合、パラメーターを省略して .zero
を渡すと、Value
型のインスタンスが得られるメソッドを呼ぶことになります。
この仕組みのおかげで、通常はスタティックなプロパティやメソッド、イニシャライザーが候補としてリストアップされます。こういった書き方便利ですし、読みやすくもなるので、ぜひ使えるようになってください。自分で型を作ったときに便利ですし、コードを共有する際にもわかりやすくなります。
まだ慣れていない方は、意識的に使ってみて、馴染ませていくと良いでしょう。こうすることで、きっと良いことがあると思います。 ダブル型にも同じようにスタティックプロパティが用意されているので、実際にコードを書く際にも覚えておくと便利です。例えば円周率の計算をする場合、Double
型にはπ
(パイ)という定義があります。これを利用すると次のようになります。
let s = 2.0 * Double.pi
こういった書き方をすることで、コードが簡潔になります。もう少し具体的な例として、関数に渡す場合も同様にDouble.pi
を利用することができます。
例えば、let s = 2 * Double.pi
のように書くこともできます。ただし、型推論が複雑なケースでは曖昧さが生じることがあります。そもそも整数リテラルはさまざまな型に変換可能なので、混乱を招くこともあります。
曖昧さが生じた場合は、Xcodeの「Issue Navigator」を利用すると、どこが曖昧なのかがリストアップされます。これにより、問題箇所を迅速に特定することができます。型推論は非常に強力ですが、それでも全てのケースを処理するわけではありません。
次に、Swiftの名前空間について話しましょう。Swiftでは、名前空間を独自に定義することはできませんが、型が名前空間を作ります。たとえば、Double
型は名前空間の役割を果たし、その中にpi
のような特定の定数やメソッドを持つことができます。他の言語では名前空間を独自に定義することができるものもありますが、Swiftの場合は型がその役割を担います。
この仕組みの良いところは、型には明確な役割があるため、名前空間の概念よりも具体的かつフォーカスされたイメージを持つことができます。たとえば、スタンダードなものではなく、型に関連する名前空間があることで、コードの可読性が上がります。
以上がSwiftの名前空間とスタティックプロパティの概略です。個人的には、この名前空間の仕組みが非常に気に入っています。余談になりますが、これがSwiftの魅力の一つだと思います。 練習問題はこんなところでいいですね。練習問題はOKです。カラーメソッドを追加してみました。ブラックやレッドを返すようにしました。
いいですね、じゃあ次にいきます。列挙型の参照方法についてです。さっきお話しした内容をスライドで軽く紹介しておけば大丈夫ですね。
ここ、一番最後のところがこのスライドの最初の行で説明されています。この定数宣言では前後の文脈から型名が想定できないため、boots.heart
のように全部丁寧に書く必要があるということが書かれています。また、スイッチ文では self
によって自分自身の型に対してパターンマッチングをしているため、.heart
から始める省略表記で書いているということがわかります。さっきプレイグラウンドでお話ししたとおりのことが書いてありますね。
ここまでそんなに難しい話ではないので大丈夫でしょう。はい、じゃあ次が列挙型についてです。
Swiftの列挙型で最も特徴的かつ強力な機能として、列挙子が値を持つことができます。これは列挙子自身に静的に値を埋め込むというものではなく、列挙子ごとに動的にランタイム時に値を持たせることができる仕組みです。日本語で言うと「関連値」と呼ばれることが一般的ですね。この仕組みによって、列挙子にランタイム時に値を添えることが可能となります。ここが非常に大きなポイントです。
ローバリュー(Raw Value)はコンパイル時に値を持たせる方法で、アソシエイティブバリュー(Associative Value)はランタイム時に値を持たせる方法です。この違いを押さえることが重要なキーポイントです。Swiftはコンパイル時とランタイム時のそれぞれの良さを活かして使えるようにしています。これは非常に面白い特徴ですね。
確か、以前Swiftを作ったクリス・ラトナーさんも、この辺りのコンセプトについて言及していました。彼はSwiftを「動的な側面も見せる静的型付け言語」と表現していました。こうした着目点で見ていくと、コンパイル時に行うこととランタイム時に行うことの区分けが見えてきて面白いですね。
さて、ローバリューとアソシエイティブバリューの違いについてもう一度確認してみましょう。スライドには、アソシエイティブバリューのことが2番目に書かれており、ローバリューのことが1番目に書かれています。これらの特徴の違いとして、ローバリューは列挙子ごとに必ず同じ値を持ち、アソシエイティブバリューはインスタンスごとに異なる値を持ちます。ここでの違いは、コンパイルタイムかランタイムかによるということになります。
関連値は、保存型プロパティのように扱えるという点も大きなポイントです。列挙型には保存型プロパティを直接持たせることはできませんが、それと似た形で列挙子に対して保存型プロパティのようなアソシエイティブバリューを持たせることができます。
実際に関連値はどういうものかというと、Swiftを触っている方にはお馴染みかと思いますが、列挙型の列挙子に加えて、どんな型の値を何個持たせるかを丸括弧で指定することができます。例えば、レスポンスの型に対して、2つの文字列を持つ型にすることができます。このような使い方によって、その型の列挙子に2つの値を渡すことができるのです。この例にあるようなレスポンス型はよく使われますね。 他にもオプショナル型も使っていますし、いろんな場面でこれは活用できます。例えば、自分で何かエラー型を作るとすると、とても便利です。例えば、コンバージョンエラーを作る場合、ケースとしてタイプミスマッチエラーがありますね。コンバージョンに関連するエラーというと、タイプミスマッチのほかにシンタックスエラーなど、状況に応じていろいろあります。また、「よくわからないけどエラーが起きちゃった」というようなアンエクスペクテッドエラーもあります。このように、エラーを作る際には、一般的な列挙型を使うことが当たり前の表現方法になります。
例えば、エラーとしてコンバージョンエラーのタイプミスマッチエラーが出た場合、そのときにもっと詳細な情報が欲しいと感じることがあるかもしれません。エラー型をちゃんと定義して、例えば convert
関数でエラーを返す際に、コンバージョンエラーとしてタイプミスマッチエラーを返す状況を考えます。エラーが発生したときに、何を期待していて実際にはどんな型だったのかといった情報が含められていないと困ることがあります。そうした詳細な情報があると、プログラムでエラーを処理したり、エラーから復帰したり、ユーザーに通知する際に非常に役立ちます。
例えば、型のミスマッチエラーの場合、期待していたのは String
型だったのに、実際には Int
型でしたというような情報をエラーに含めます。実際にこれを扱うときには、switch
文でエラーに対してパターンマッチングを行い、ケースごとに値を取り出して処理します。例えば、
switch error {
case .typeMismatch(let expected, let actual):
print("Expected \\(expected) but found \\(actual)")
}
このようにして、期待した型と実際の型の情報を変数にバインドして使うことができます。これによって、ランタイムにインスタンスごとに任意の値を割り当てることができます。今回は String
と Int
でしたが、状況によって別の型、例えば Double
型を期待していたけど Int
だったなどの情報も扱うことができます。これは、とても有用な機能です。 こうやって Int
型を期待しているのに Double
型が来た場合や、あるインスタンスが String
型を期待して実際は Double
型の場合など、いろいろな状況で表現が行えます。例えば、タイプミスマッチエラーが起こったとき、期待する値や実際の値がどうでもいい場合は、case
文で付属値に対して何も指定せずに、ざっくりとパターンマッチングすることもできます。これも結構便利な表現力だと思います。
他の例としては、例えばパラメーターとして Any
タイプ型を2つ取ってコンバージョンエラー型を返す関数に対して、ConversionError.TypeMismatch
を渡す場合です。そして、print
関数を使って、渡された値がちゃんと取れているか確認します。列挙型を後でインスタンス化することもでき、この関連値が付いた列挙子も同等の関数として扱えます。静的関数としても扱えるので、便利な機能です。
また、列挙子を使ってマップと組み合わせることもできます。例えば、let values
配列の中に Int
型や Double
型、String
型があるとします。この values
に対して map
をかけ、ConversionError.TypeMismatch
を適用してインスタンス化し、print
して確認することもできます。
列挙型の特徴として、特定のエラー型に対して異なる値を持たせることができます。こうした機能により、Swift言語ではユニオン型的な構造を持たせることができます。C言語やC++にはユニオン型がありますが、Swiftの列挙型も似たような特徴を持っています。
例えば、コンバージョンエラーのサイズを確認する際に Int8
や Int64
のように異なるサイズのテストを行うと、メモリーサイズが変わります。インダイレクトの場合はポインターに変わり、参照型的に扱われるためサイズも異なってきます。インダイレクトケースについては別の機会にお話ししようと思います。
では、1時間経ちましたので、今日の勉強会はここで終わりにします。お疲れ様でした。ありがとうございました。