はじめに
(参考①) [A Swift Tour]
(https://docs.swift.org/swift-book/GuidedTour/GuidedTour.html#//apple_ref/doc/uid/TP40014097-CH2-ID1)
↑本記事の主となるSwift公式サイトの記事。
(参考①') [A Swift Tour]
(https://rusutikaa.github.io/docs/index.html)
↑Swift.orgやApple Developer Documentationの日本語訳。
(参考②) [Swift実践入門 〜 今からはじめるiOSアプリ開発! 基本文法を押さえて、簡単な電卓を作ってみよう]
(https://eh-career.com/engineerhub/entry/2017/05/25/110000)
↑Swift 5.3に拘らない人向け。
(参考③) [【Swift入門】オプショナル型を理解しよう]
(https://fukatsu.tech/optional-swift)
↑Optional型について理解したい方向け。
(参考④) [Swiftのオプショナル型の使いこなし]
(https://thinkit.co.jp/article/13489)
↑Optional型を扱う際の注意点を詳しく知りたい方向け。
(参考⑤) [When to use guard let rather than if let]
(https://www.hackingwithswift.com/quick-start/understanding-swift/when-to-use-guard-let-rather-than-if-let)
↑guard-let構文の使い所について知りたい方向け。(英語記事)
(参考⑥) [Optional Chaining]
(https://docs.swift.org/swift-book/LanguageGuide/OptionalChaining.html)
↑オプショナルチェイニングを解説するSwift.orgの公式ページ。(英語記事)
(参考⑦) [Swift Standard Library(GitHub)]
(https://github.com/apple/swift/tree/main/stdlib/public/core)
↑GitHubで公開されているSwiftの標準ライブラリ。(英語)
(参考⑧) [[JavaScript] 猿でもわかるクロージャ超入門 まとめ]
(http://dqn.sakusakutto.jp/2009/01/javascript_5.html)
↑クロージャについて理解を深めたい方向け。(JavaScriptで記述)
(参考⑨) [イメージで理解するSwiftの高階関数(filter, map, reduce, compactMap, flatMap)]
(https://qiita.com/shtnkgm/items/eaad3c6ab368463de8e3)
↑高階関数について理解を深めたい方向け。
(参考⑩) [[Swift]flatMap・compactMapの挙動はソースコードを読んで理解しよう〜型情報と実装から難関メソッドを突破する〜]
(https://qiita.com/crea/items/b065425bde990cbd1c82)
↑flatMap・compactMapの内部処理を理解したい方向け。
(参考⑪) [mapとflatMapという便利メソッドを理解する]
(https://qiita.com/shimesaba/items/1a89cb5600454f91cc67)
↑flatMapについて理解したい初心者の方向け。
(参考⑫) [Swift Language - ジェネリックス]
(https://sodocumentation.net/ja/swift/topic/774/%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AA%E3%83%83%E3%82%AF%E3%82%B9)
↑Swiftにおけるジェネリクスを理解したい方向け。
(参考⑬) [[Swift] rethrowsを少し整理してみた]
(https://dev.classmethod.jp/articles/swift-about-rethrows/)
↑rethrowについて理解したい方向け。
(参考⑭) [Swiftで値型と参照型の違いを理解する]
(https://qiita.com/koher/items/bcdbf6578b6edd1f9e0c)
↑値型と参照型の違いについて理解したい方向け。(@koher さんの記事は特におすすめ)
(参考⑮) [Swiftにおけるclassとstructの使い分け]
(https://cockscomb.hatenablog.com/entry/choosing-between-classes-and-structures)
↑データ・メソッドをまとめる「クラス」と「構造体」の使い分け方を知りたい方向け。
(参考⑯) [【Swift】クラスの継承とプロトコルの準拠の使い分け]
(https://qiita.com/Howasuto/items/9c84739a210b5a684d80)
↑クラスとプロトコルの違いを詳しく知りたい方向け。
A Swift Tour
"Hello, world!"
どの言語を学ぶにしても、最初に必ず作成・実行するプログラムがあります。
"Hello, world!"と画面に表示させるだけのプログラムです。
Swiftでは、以下のように記述します。
print("Hello, world!")
このコードから分かることは、Swiftの簡潔性です。
Swiftと同じオブジェクト指向言語(object-oriented language)であるJavaでは、以下のように記述します。
class Hello {
public static void main(String[] args) {
System.out.print("Hello, world!");
}
}
Javaは"Hello, world!"と表示させるだけのプログラムでさえ5行も要するのに対し、Swiftは1行で済んでしまいます。main()関数も、行の末尾に;(セミコロン)も必要ありません。
単純変数(Simple Values)
変数・定数の宣言
定数(constant)を宣言(declaration)するときはlet、変数(variable)を宣言するときはvarを使います。
<編集メモ: mutable/immutableな変数(動的/静的),定数との違いにも触れる>
var myVariable = 42 // 変数:myVariable = 42
myVariable = 50 // 変数:myVariable = 50(値の上書き)
let myConstant = 42 // 定数:myConstant = 42
print(myVariable, myConstant) // 実行結果: 50 42
変数・定数には、型(type)という概念があります。
例えば、「614」は数値(numeric type)の中でも整数(integer)ですが、「614.0」のように小数(float)としても表現できます。一方で「"614"」と記述すれば、文字列(string)として認識されます。
型の分類
型の一覧は以下の通りです。<編集メモ: 図を分かりやすくする>
型については、いきなり気にする必要はありません。
最初のうちは、整数はintで宣言し、実数はdoubleで宣言する、などと決めて覚えてしまえば良いと思います。
Swiftの特徴: 型推論(type inference)と型変換
Swiftのコンパイラは初期化子(initializer)から型を推論(infer)することができます。上のコードも、型を定義(definition)していないのに変数を宣言することができています。
もちろん、次のコードのように、型を明示的(explicit)に定義(=型アノテーション)することもできます。
型推論は便利ですが、バグや処理速度の低下に繋がるため、明示的に型を定義するようにしましょう。
let implicitInteger = 70 // 暗示的(implicit)な型定義
let implicitDouble = 70.0 // 暗示的な型定義
let explicitDouble: Double = 70 //明示的な型定義
print(implicitInteger, implicitDouble, explicitDouble)
// 実行結果: 70 70.0 70.0
Swiftでは、型が暗黙的(implicit)に他の型に変換(convert)されることはありません。
そのため、以下のようなコードを記述するとエラーが出力されます。
let label = "The width is "
let width = 94
let widthLabel = label + width
print(widthLabel)
// 実行結果:
// error: binary operator '+' cannot be applied to operands of type 'String' and 'Int'
String型(=文字列)とInt型(=整数)を+演算子で連結することはできません。Int型をString型に変換してから連結する必要があります。
型変換は以下のようにして行います。
let label = "The width is "
let width = 94
let widthLabel = label + String(width) // Int型widthをString型に変換
print(widthLabel)
// 実行結果: The width is 94
文字列に値を埋め込むのであれば、型変換を行わずとも表示させる方法があります。
値を()で囲み、()の直前に\(バックスラッシュ)を記述するだけです。
なお、Macにおける\のショートカットキーは⌥(option) と ¥になります。
let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."
print(appleSummary)
print(fruitSummary)
// 実行結果:
// I have 3 apples.
// I have 8 pieces of fruit.
複数行にわたる文字列の記述
複数行にわたる文字列を記述したい場合は、"""(ダブルクォテーション)で囲いましょう。
なお、可読性を向上させるためにSpaceやTabを入力した場合は、出力結果にもSpaceやTabが含まれている状態になります。""で囲っているため、SpaceやTabも一つの文字列として扱われています。
可読性は落ちますが、出力結果に含みたくない場合は左づめで記述するようにしましょう。
let quotation = """
I said "I have \(apples) apples."
And then I said "I have \(apples + oranges) pieces of fruit."
"""
print(quotation)
// 実行結果:
// I said "I have 3 apples."
// And then I said "I have 8 pieces of fruit."
配列と辞書
配列(array)や辞書(dictionary; 連想配列)を生成するには、[]でインデックスやキーを囲みましょう。
それぞれの構成要素(component)は,(コンマ)で列挙(enumerate)します。
//配列の生成
var shoppingList = ["catfish", "water", "tulips",] // 型推論
shoppingList += [bread] // 構成要素の追加(append)
shoppingList[1] = "bottle of water" // 構成要素の更新
var wishList: [String] = ["salmon", "soda", "violet",] // 型アノテーション
print(shoppingList[1])
print(shoppingList[3])
print(wishList[0])
// 実行結果:
// bottle of water
// bread
// salmon
//辞書の生成
var occupations = [ // 型推論
"Malcolm": "Captain",
"Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations" // キー"Jayne"の追加
occupations["Kaylee"] = "Engineer" // キー"Kaylee"の更新
var currency: [String: String] = [ // 型アノテーション
"JPN": "yen",
"USA": "dollar",
]
print(occupations["Jayne"]!)
print(occupations["Kaylee"]!)
print(currency["USA"]!)
// 実行結果:
// Public Relations
// Engineer
// dollar
配列のインデックスに基づいた構成要素の出力はprint(配列変数名[インデックス])と記述したのに対して、辞書のキーに基づいた値の出力はprint(辞書名["キー"]!)のように、末尾に!を付加して記述しています。その理由は後述します。
Swiftの特徴: 「Optional型」の存在
配列と辞書の大きな違いは、**存在しないインデックスの参照(refer)**のされ方です。
以下に、存在しない構成要素を出力するように仕組まれた配列と辞書のプログラムを記します。その実行結果に注目してください。
var errArray = [0, 1, 2,]
print(errArray[3]) // 存在しない構成要素の出力
// 実行結果:
// Fatal error: Index out of range
var errDictionary = [
"zero": 0,
"one": 1,
"two": 2,
]
print(errDictionary["two"]) // 存在する構成要素の出力
print(errDictionary["three"]) // 存在しない構成要素の出力
// 実行結果:
// Optional(2) # 戻り値をunwrapするよう求められる
// nil # 戻り値をunwrapするよう求められる
配列は、存在しないインデックスを出力しようとすると実行時エラー(runtime error)を出力します。この場合の戻り値はnilではありません。nilが格納される箱すら用意されていないのです。
一方で、辞書はOptional型(=nilへの参照を認める)の値を出力します。箱は用意されており、nilが格納されています。
辞書は、存在しないキーによる値の参照が行われる可能性があるため、Optional型で値を返すように定義されています。しかし、print()関数は引数(argument)として非Optional型(=nilへの参照を認めない)の変数しか認めていないため、print()関数の引数の末尾に!を記述し、Optional型 → 非Optional型の型変換(unwrap)を行うようにします。
この型変換は、Optinal型変数の値がnilであっても非Optional型に変換を行うため、強制アンラップ(forced-unwrap)と呼ばれています。
ただし、print(errDictionary["three"]?)と記述してアンラップしてしまうと、値がnilであるにもかかわらずnilを許容しないため、以下のような実行時エラーが出力されてしまいます。
Fatal error: Unexpectedly found nil while unwrapping an Optional value
Optional型導入のメリットと使い方
Swiftでは基本的に、値はOptional型で返されます。理由は簡単で、Optional型を導入し、文法的エラーを対象行数と共に返す仕様にすることで、バグの原因となりうる箇所(=Optional型変数を含む行)を容易に特定できるようになります。
一方で、print()関数のように**Optinal型を認めていない関数**は数多く存在します。Optional型のメリットを採用しながら処理(procedure)を実現する方法としてオプショナルバインディング(Optinal Binding)やオプショナルチェイニング(Optional Chaining)、Nil結合演算子(nil-coalescing operator)などがあります。いずれの方法も、Optinal型変数の中身を参照しながら処理を進めていきます。
A Swift Tourでは、以下のように紹介されています。
オプショナルバインディング
var optionalName: String? = "John Appleseed"
var greeting = "Hello" // 初期化として、挨拶は"Hello"と返すことにしておく
if let name = optionalName { // ただし、名前のある人(optionalName != nil)が挨拶してきた場合は
greeting = "Hello, \(name)" // "Hello, (名前)"で返すようにする
}
print(greeting)
// 出力結果:
// Hello, John Appleseed
if-let構文によって、Optional型の値がnilでない場合の処理を実現しています。値がnilかnilでないかで条件分岐を行うため、変数の宣言時に?を型の後ろに付け、Optional型であることを明示的に定義しましょう。if-let構文では、Optional<String>型変数であるoptionalNameを暗黙的にアンラップしています。
if-let構文の代わりにif文を使う場合は、以下のように記述します。
var optName: String? = "John Appleseed"
var ifGreeting = "Hi" // 初期化として、挨拶は"Hi"と返すことにしておく
if optName != nil { // 変数optNameがnilでない場合は
let ifName = optName! // 変数optNameを強制アンラップし、
ifGreeting = "Hi, \(ifName)" // "Hi, (名前)"で返すようにする
}
print(ifGreeting)
// 実行結果:
// Hi, John Appleseed
条件処理節でlet name = optionalName!のように強制アンラップする必要があるため、使用する際はif-let構文で記述するようにしましょう。
if-let構文とguard-let構文
またif-let構文と似た処理としてguard-let構文があります。guard-let構文では、Optional型変数がnilの場合の処理節の中に、return・break・throwなど、関数から抜け出す(=スコープ(scope)を抜ける)処理が含まれていないと実行時エラーを吐くため、コーディングする際の安全性を向上させます。
func checkNil(name: String?) {
guard let yourName = name else { // 名前のない(name = nil)場合は
print("No name.") // "No name."と返すようにして
return // 関数checkNil()から抜ける
}
print("You are \(yourName).") // 名前のある(name != nil)場合の処理
}
checkNil(name: nil)
// 実行結果: No name.
checkNil(name: "Jack")
// 実行結果: You are Jack.
if-let構文は**Optional型変数がnilでない場合にアンラップを行うことを目的としているのに対し、guard-let構文はOptional型変数がnilであるかどうかを確認することを目的としています。そのため、guard-let構文は処理の冒頭に置かれ、nil判定(nil-check)として機能**させるのが一般的です。
なお、guard-let構文を使用する際は、guard let <変数の参照> elseのように、末尾にelseを記述するのを忘れないようにしましょう。
オプショナルチェイニング
オプショナルチェイニングの説明に入る前に、以下のサンプルプログラムを見てみましょう。
// 共通
class Residence {
var numOfRooms = 1
}
class Person {
var residence: Residence?
}
let john = Person()
// 強制アンラップ
let roomCount = john.residence!.numOfRooms
print(roomCount)
// 実行結果:
// Fatal error: Unexpectedly found nil while unwrapping an Optional value:
class (クラス名)で宣言しているクラス(class)とは、データ(data)とデータを処理するメソッド(method)を枠組みとしたオブジェクト(object)です。
上記プログラムでは、冒頭に2つのクラスResidence・Personを宣言しています。
クラスResidenceのクラス宣言(class declaration)では、クラスResidenceの実体(インスタンス(instance))を構成するフィールド(field)のnumOfRoomsを生成し、Int型の整数「1」を参照(refer)するようにしています。
クラスPersonのクラス宣言では、クラスPersonのフィールドresidenceを生成し、これはクラスResidenceのクラス型変数(class type variable)であると宣言しています。
また、let文でクラスPersonのクラス型変数(定数)johnを生成し、= Person()でクラスPersonのインスタンスを参照するようにしています。
そして、ややこしい表現ですが、定数roomCountを生成(let roomCount)し、クラス型変数johnのフィールドjohn.residenceが参照するべきクラスResidenceのフィールドjohn.residence.numOfRoomsを参照させています。
ここで、クラス型変数johnを構成するフィールドresidenceを強制アンラップした結果、john.residence = nilというエラーが表示されました。
これは、let john = Person()でクラスPersonのクラス型変数johnがクラスPersonのインスタンス(を構成するフィールドresidence)を参照できるようにしているのに対し、クラスPersonのクラス宣言(class declaration)では、デフォルトコンストラクタ(default constructor)によってクラスResidenceのクラス型変数residenceが生成されただけで、residenceが参照するものは宣言されていません。
クラス型変数residenceの箱は生成されていますが、箱residenceが参照すべきデータnumOfRoomsが宣言されていないため、residenceの値がnilとなっているのです。
上のプログラムでは、値がnilのjohn.residenceを強制アンラップしているため、実行時エラーが出力されました。
このエラーを解決するには、以下のように記述を加えて**residenceがクラスResidenceのインスタンス(を構成するフィールドnumOfRooms)を参照**できるように明示します。
class Person {
var residence: Residence? = Residence() // Residenceのインスタンスへの参照
}
class Residence {
var numOfRooms = 1
}
let john = Person()
let roomCount = john.residence!.numOfRooms
print(roomCount)
// 実行結果: 1
なお、()は、厳密にはインスタンスを表すものではなく、コンストラクタ(constructor)の呼び出し(call)であることにも触れておきます。()によってクラスのコンストラクタを呼び出すことで、コンストラクタによってインスタンスを参照できるようになるのです。
オプショナルチェイニングは強制アンラップ(!)と似ていますが、実行結果が異なります。
// 共通
class Residence {
var numOfRooms = 1
}
class Person {
var residence: Residence?
}
let john = Person()
// オプショナルチェイニング
if let roomCount = john.residence?.numOfRooms {
print("John's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// 実行結果:
// Unable to retrieve the number of rooms.
強制アンラップを行ったプログラムでは、nilのプロパティ(property)であるjohn.residenceを非Optional型に変換しようとしたため、実行時エラーが出力されていました。
一方で、オプショナルチェイニングを行ったプログラムでは、実行結果から分かるように、正常に処理が実行されているのです。
オプショナルチェイニングは、Optional型のプロパティやメソッド、添字(subscript, index)の後ろに?を付加することで、?を付加したプロパティ等の値がnilの場合は、その後ろのプロパティ(=.numOfRooms)を無視してnilを返します。
この性質を利用して、上のプログラムでは、if-let-else文でroomCountがnilである場合の処理として、文字列"Unable to retrieve the number of rooms"を表示するようにしています。
オプショナルチェイニングは、値がnilになりうるメソッドやプロパティを扱うときに有効な処理方法です。上記プログラムのように、オプショナルバインディングと組み合わせて使うこともあります。
Nil結合演算子
let nickname: String? = nil
let fullName: String = "John Appleseed"
let informalGreeting = "Hi, \(nickname ?? fullName)"
print(informalGreeting)
// 出力結果:
// Hi, John Appleseed
??(nil-coalescing operator; Nil結合演算子)はx ?? yのように記述し、x = nilの場合はxを返し、x != nilの場合はyを返します。
条件演算子(conditional operator)でも同様の処理は可能です。以下のように記述します。
let nickname: String? = "Jack"
let fullName: String = "John Appleseed"
let conditionalGreeting = "Hi, \(nickname != nil ? nickname! : fullName)"
print(conditionalGreeting)
// 出力結果:
// Hi, Jack
条件演算子の場合、x ? y : zのように記述し、条件式xを評価した結果、trueであればyを返し、falseであればzを返します。今回のようにnicknameがnilでない、つまり条件式xがtrueである場合は、yにあたるnicknameを返します。
一方で、最終的にprint()関数の引数となるconditionalGreetingは非Optional型でなければならないため、その中身となる""で囲まれた文字列内の変数も非Optional型でなければなりません。そこで、nicknameの末尾に!を付けることで強制アンラップしています。
オプショナル型(?)と暗黙的アンラップ型(!)
Optional型変数を扱う際に?や!を型・変数の末尾に付加しますが、この2つには違いがあります。
以下のプログラムで、両者の動作を比較してみましょう。
var num1: Int = 1 // 非Optional型
var optNum1: Int? = 4 // Optional型
print(num1 + optNum1) // 非Optional型 + Optional型
// 実行結果:
// Error: Value of optional type 'Int?' must be unwrapped to a value of type 'Int'
print(num1 + optNum1!) // 非Optional型 + 強制アンラップされたOptional型
// 実行結果: 5
print(optNum1)
// 実行結果: Optional(4)
var num2: Int = 3 // 非Optional型
var optNum2: Int! = 5 // 暗黙的アンラップ型
print(num2 + optNum2)
// 実行結果: 8
print(num2 + optNum2!)
// 実行結果: 8
print(optNum2)
// 実行結果: Optional(5)
?を付加して定義したOptional型変数は、関数の実行時に**!を付けて強制アンラップする必要があります。
一方で、!を付加して定義した暗黙的アンラップ型(implicitly unwrapped optional)変数は、強制アンラップしなくても正常に実行されます。
上記プログラムの実行結果からも分かるように、暗黙的アンラップ型変数は基本的にOptional型として扱われますが、非Optional型変数と連結する際には暗黙的にアンラップ**が行われます。
制御フロー(Control Flow)
制御フローとは、プログラムの処理の流れ(flow)を指します。プログラミングでは、命令文を上から記述していきますが、実際に実行される処理にも以下の3つの種類が存在します。
- 順次処理(sequence)
- 分岐処理(selection):
if,switch - 繰返し処理(iteration):
for,while,repeat
プログラミングでは、基本的に上の命令文から下の命令文に向かって1行ずつ読み込む順次処理が行われますが、if文(if statement)やswitch文(switch statement)を用いた分岐処理や、for文(for statement)やwhile文(while statement)、repeat文(repeat statement)を用いた繰返し処理を定義することもできます。
A Swift Tourでは、以下のようなサンプルプログラムによって分岐処理・繰返し処理の宣言方法が紹介されています。
拡張for文(for-in文)
let individualScores: [Int] = [75, 43, 103, 87, 12]
var teamScore: Int = 0
for score in individualScores {
if score > 50 {
teamScore += 3
} else {
teamScore += 1
}
print(teamScore)
// 実行結果: 11
拡張for文(enhanced for statement)は、for x in yのように記述することで、配列や辞書のように、複数のデータやオブジェクトを格納するコレクションであるyの各構成要素に対して繰返し処理を実行します。一般的に、コレクションは複数のデータを格納するため、変数名は英単語の複数形で宣言します。そのため、各構成要素を表す変数xは、コレクション名の単数形で宣言します。
拡張for文を応用したサンプルプログラムは以下の通りです。
let interestingNumbers: [String: [Int]] = [
"Prime": [2, 3, 5, 7, 11, 13,],
"Fibonacci": [1, 1, 2, 3, 5, 8,],
"Square": [1, 4, 9, 16, 25,],
]
var largest: Int = 0
for (kind, numbers) in interestingNumbers {
for number in numbers {
if number > largest {
largest = number
}
}
}
print(largest)
// 実行結果: 25
for (x, y) in zと記述し、キーxと値yで構成される辞書に対して繰返し処理を実現することもできます。
上記プログラムのfor (kind, numbers) in interestingNumbersの部分に注目しましょう。この拡張for文は、様々なキーkindと値numbersを持つ辞書を1つずつ走査(scan)すると宣言しています。
それに続くfor number in numbersの部分では、まずキーPrimeに格納されている配列の全要素(=numbers)を1つずつ走査します。キーPrime内の全要素の走査を終えると、次はキーFibonacciに格納されている配列の全要素の走査に移ります。同様の処理を繰り返すことで、すべての辞書の値を走査することができます。
switch文
let vegetable = "red pepper"
switch vegetable {
case "celery":
print("Add some raisins and make ants on a log.")
case "cucumber", "watercress":
print("That would make a good tea sandwich.")
case let x where x.hasSuffix("pepper"):
print("Is it a spicy \(x)?")
default:
print("Everything tastes good in soup.")
}
if文は条件式(conditional expression)の判定結果に応じて処理を2つに分岐(else-if文によってswitch文のような処理も可能)させるのに対し、switch文はcaseの後に続くラベル(label)と照合(=評価(evaluate))しながら処理を複数に分岐させます。なお、Swiftではswitch文のスコープを抜け出すためのbreak文(break statement)は不要です。
switch vegetableで定数vegetableの値を見るよう宣言し、case x:の部分で定数vegetableの値がラベルxである場合の処理を実現しています。
次に、case let x where x.hasSuffix("pepper"):の部分にも注目しましょう。
switch s {case let x where y: z}の構文では、switchに続く変数sが、条件式yを満たすようなローカル変数(local variable; 局所変数)であるxと照合した結果、trueであれば、s = xとし、処理zを実行します。
また、String型変数のメソッドx.hasSuffixは、引数で指定した接尾辞(suffix)と一致していればtrueを、一致していなければfalseを返すメソッドです。
基本型は、パッケージ(package)にhasSuffixのように型独自のメソッドを有しています。上記プログラムで用いられたhasSuffixはSwift言語と密接に関連する重要クラスのクラスメソッド(class method)であるため静的インポート宣言(static import declaration)は不要でした。ただし、中にはimport文で静的インポート宣言が必要なメソッドもあることに留意しましょう。
while文とrepeat文
while文・repeat文は、与えた条件式を満たす間の繰返し処理を実現します。
while文は条件式の評価 → 処理の順に実行されるのに対し、repeat文は処理 → 条件式の評価の順で実行されます。
両者の違いを比較するサンプルプログラムは以下の通りです。
// while文
var m: Int = 8
while m < 8 {
m *= 2
}
// repeat文
var n: Int = 8
repeat {
n *= 2
} while n < 8
print(m) // 実行結果: 8
print(n) // 実行結果: 16
while文は、m < 8を評価した結果がfalseだったため、処理m *= 2を実行していません。
一方で、repeat文は、まずは処理n *= 2を実行した後でn < 8を評価(この時点でn = 16)した結果がfalseだったため、繰返し処理を中断しています。
関数(function)とクロージャ(closure)
複数の処理を一まとまりの集合として定義したものを、関数やサブルーチン(sub routine)、プロシージャ(procedure)、メソッド(method)と呼びます。厳密には返り値(return value)を持つかどうかでこれらの用語(technical term)を使い分ける人もいますが、A Swift Tourにおいては"function" と説明されているため、本記事においても一貫して**関数**と表現します。
関数
関数を宣言したサンプルプログラムは以下の通りです。
// 引数ラベルが必要な関数の宣言
func greet(person: String, day: String) -> String {
return "Hello \(person), today is \(day)."
}
var str1 = greet(person: "Bob", day: "Tuesday")
print(str1)
// 実行結果:
// Hello Bob, today is Tuesday.
// 引数ラベルが省略可能な関数の宣言
func greet(_ person: String, _ day: String) -> String {
return "Hello \(person), today is \(day)."
}
var str2 = greet("John", "Wednesday")
print(str2)
// 実行結果:
// Hello John, today is Wednesday.
// 任意の引数ラベルを付与した関数の宣言
func greet(name person: String, on day: String) -> String {
return "Hello \(person), today is \(day)."
}
var str3 = greet(name: "Tom", on: Friday)
print(str3)
// 実行結果:
// Hello Tom, today is Friday.
なお、関数greet()のように、-> xと「返り値がある」と定義した関数では、処理の中に必ずreturn文(return statement)を含む必要があります。
クロージャ(closure; 無名関数, anonymous function)
上記プログラムでは、関数greet()と、グローバル変数(global variable)として関数の呼び出し元(caller)であるstr1、str2、str3の計4つを定義しています。これらの定義によって、他の場面でgreetやstrを関数・変数名として宣言すると、コード全体に紛らわしさが生まれ、可読性の低下に繋がるため使用しづらくなってしまいました。しかし、これでも関数greet()では多重定義(overload)を用いることで、関数で使用する名前の領域(space)を節約しています。
可読性の高いコード(readable code)にするためには、グローバル変数の数を極力少なくする必要があります。グローバル変数含むグローバル名前空間(global namespace)を節約するために、上記プログラムの変数strのように多用途な名前である場合や、用途が限られ再利用(reuse)が考えにくいような場合は、クロージャ(無名関数)を使用するようにしましょう。
上記プログラムを、クロージャを用いて書き換えると以下のようになります。
var greeting: (String, String) -> String = {(person: String, day: String) -> String in
return "Hello \(person), today is \(day)."
}
print(greeting("Bob","Tuesday"))
// 実行結果:
// Hello Bob, today is Tuesday.
関数greet()をクロージャとして{(m: x) -> y in z}と記述しました。この構文では、x型のローカル変数(local variable)であるmが処理zによってy型の値として返されます。
クロージャを使うことで、グローバル名前空間の節約が可能になります。ただし、関数に名前を付けることは「関数が行う処理をイメージできるようにする」という大きな意味があります。クロージャの乱用はかえって可読性の低下に繋がることにも留意しましょう。
returnとprint
クロージャを用いた上記のサンプルプログラムは、以下のように書き換えることもできます。
var greeting: (String, String) -> Void = {(person: String, day: String) -> Void in
print("Hello \(person), today is \(day).")
}
greeting("Bob", "Tuesday")
// 実行結果:
// Hello Bob, today is Tuesday.
var greeting: (String, String) -> Voidの部分では、(String, String) -> Void型変数としてgreetingを宣言しています。これは、パラメータとして(String, String)型が入力され、返り値としてVoid型の値を返す即時変数(IIFE; Immediately Invoked Function Expression)として定義されています。
また、変数greetingはクロージャ{}を参照するよう定義しています。このように、変数と同様に扱われるような関数を第一級関数(first-class function)と呼びます。Swiftは第一級関数の性質を有しています。
Voidはパラメータや返り値を持たないような型です。print()関数は、値を出力(output)するだけであり、その値は他の関数に参照されません。このサンプルプログラムではprint()で出力するだけなので、返り値の型としてVoid型を定義しています。
print()関数によって求める値を出力すればよいのに、なぜ今までのサンプルプログラムではreturn文を介していたのでしょうか。
その理由は、値を再利用する可能性があるからです。先述したように、print()関数は値を出力するだけであり、その値を使って演算することはできないのです。
ただ出力するだけでよいのであればprint()関数を使い、他の関数内で使用する可能性がある場合はreturn文で値を返し、その受け取り先の変数をprint()関数によって値を出力するようにしましょう。
高階関数(higher order function)
Swiftは、関数が変数と同様に扱われる第一級関数を有しています。
高階関数とは、第一級関数の性質のうち「関数を引数や返り値にとることができるような関数」を指しています。
本記事では高階関数のうち、多用途である以下の5つを取り上げて説明します。
map
mapは、「各構成要素への処理(=マッピング(mapping))」を実現します。サンプルプログラムは以下の通りです。
var numbers: [Int] = [20, 19, 7, 12, 4]
var triple: [Int] = numbers.map{(number: Int) -> Int in
var result: Int = 3 * number
// warning: Variable 'result' was never mutated
return result
}
// 引数の省略
var tripleAbbreviate: [Int] = numbers.map{
return 3 * $0
}
print(triple)
// 実行結果: [60, 51, 21, 36, 12]
print(tripleAbbreviate)
// 実行結果: [60, 51, 21, 36, 12]
var triple: [Int] = numbers.map(..)の部分では、高階関数numbers.map()の呼び出し元として[Int]型変数tripleを定義しています。
x.map{y}の部分では、Sequence型レシーバ(Sequence-Type receiver)である[Int]型変数numbersの各構成要素に対して、クロージャyを実行するよう宣言しています。
また、型推論によってクロージャ内の引数の省略(abbreviate)ができます。今回は引数が1つしかないため、その第一引数(first argument)を$0と記述していますが、引数が複数ある場合でも$0、$1、$2、のように記述することで、クロージャ内の引数の記述を省略することができます。
上記プログラムでは、var result: Int = 3 * numberの部分でwarningエラーが表示されます。"Variable 'xxx' was never mutated."とは、「変数(の値)が不変(immutable)である」という意味です。
変数numbersを[20, 19, 7, 12, 4]という定数の配列で定義している以上、各要素を3倍した値は不変であるため、その積resultは変数として宣言(var ..)するのではなく、**定数として宣言(let ..)**しなさい、というXcodeからの忠告です。
変数の方が値の変更が可能という点で自由性が高いですが、設定した値が後から変更されてしまう危険性もあるため、不変の値をもつ数についてはなるべく定数として宣言するようにしましょう。
filter
filterは、「条件を満たす構成要素の抽出」を実現します。サンプルプログラムは以下の通りです。
var words: [String] = ["blanket", "roof", "rail", "building", "sandstorm"]
let extract: [String] = words.filter({(word: String) -> Bool in
return word.hasPrefix("b")
})
// 引数の省略
let extractAbbreviate: [String] = words.filter({
return $0.hasPrefix("b")
})
print(extract)
// 実行結果: ["blanket", "building]
print(extractAbbreviate)
// 実行結果: ["blanket", "building]
x.filter{y}の部分では、Sequence型レシーバであるxの各構成要素を、抽出条件yと照合しながら抽出(extract)する処理を宣言しています。また、その抽出条件yを$0.hasPrefix("b")と定義しています。
String型メソッドである$0.hasPrefix(z)は、[String]型配列の各構成要素$0が、接頭辞(prefix)に引数zを持つような要素であればtrueを返すメソッドです。
上記のことから、words.filter{return $0.hasPrefix("b")}の部分は、配列wordsの各構成要素$0のうち、接頭辞bを持つ(=$0.hasPrefix("b")がtrueである)要素を抽出した[String]型配列を指しているのが分かります。
reduce
reduceは「各構成要素の集計」を実現します。サンプルプログラムは以下の通りです。
var numbers: [Int] = [20, 19, 7, 12, 4]
let countUp: Int = numbers.reduce(0){(sum: Int, num: Int) -> Int in
return sum + num
}
// 引数の省略
let countUpAbbreviate: Int = numbers.reduce(0){
return $0 + $1
}
print(countUp) // 実行結果: 62
print(countUpAbbreviate) // 実行結果: 62
x.reduce(y){m z n}の部分では、Sequence型レシーバであるxの構成要素を集計(aggregate)する処理を宣言しています。その変換処理は、初期値をyとしてmに代入(substitute)した後、各構成要素のnに対して演算子(operator)であるzで演算を行います。
引数を省略した場合の演算(operation)について、第一引数$0には、1回目の処理では初期値が、2回目以降の処理では前回の処理終了時の結果が代入されます。また、第二引数$1には、前回処理で扱った構成要素の、次の構成要素が代入されます。(意味が分からない方はこちらを参照してください)
今回の場合、初期値は0、演算子として+を用いているため、初期値0に対して1つ目の構成要素である20から最後の構成要素である4まで順番に、加法(addition)による処理が行われています。
compactMap
compactMapは「各構成要素の処理およびnilでない値の抽出」を実現します。サンプルプログラムは以下の通りです。
var numbers: [Int] = [20, 19, 7, 12, 4]
let lessThan10Triple: [Int] = numbers.compactMap{(number: Int) -> Int? in
return number < 10 ? 3 * number : nil
}
// 引数の省略
let lessThan10TripleAbbreviate: [Int] = numbers.compactMap{
return $0 < 10 ? 3 * $0 : nil
}
// compactMapの内部処理
let compMapInnerProcedure: [Int] = numbers.filter{ return $0 < 10 }.map{ return 3 * $0 }
print(lessThan10Triple)
// 実行結果: [21, 12]
print(lessThan10TripleAbbreviate)
// 実行結果: [21, 12]
print(compMapInnerProcedure)
// 実行結果: [21, 12]
compactMapは、.mapと.filterを一度に実行するような処理を行うため、compactMapという名称になっています。
x.compactMap{y}の部分では、Sequence型レシーバであるxの各構成要素に対して、条件式yで**処理(.map)およびnilでない値の抽出(.filter{ $0 != nil })**を実行しています。
条件式yの部分では、三項条件演算子(ternary operator)である?:を用いてx ? y : zと記述し、条件式xがtrueであればyを評価した値、falseであればzを評価した値を生成(generate)します。
今回の場合、各構成要素$0が条件式$0 < 10を満たしている(=true)場合は3 * $0、満たしていない(=false)場合はnilを返しています。
compactMapでは、マッピングを実行する前に**.filter{ $0 != nil }が自動的に実行**され、nilの値は処理されません。
この性質を利用し、三項条件演算子?:を用いて、処理条件を満たさない構成要素にはnilを参照させ、nilでない(=処理条件を満たす)構成要素にのみ処理が実行されるように設定しましょう。
flatMap
flatMapは「多次元配列の次元削減(dimensionality reduction; 平坦化, flat)」を実現します。サンプルプログラムは以下の通りです。
var numbers: [[[Int]]] = [ [[1, 2], [3, 4]], [[5, 6], [7, 8]] ]
let reductDimension: [[Int]] = numbers.flatMap{(number: [[Int]]) -> [[Int]] in
return number
}
// 引数の省略
let reductDimensionAbbreviate: [[Int]] = numbers.flatMap{
return $0
}
// 1次元化
let oneDimensionAbbreviate: [Int] = numbers.flatMap{return $0}.flatMap{return $0}
print(reductDimension)
// 実行結果:
// [[1, 2], [3, 4], [5, 6], [7, 8]]
print(reductDimensionAbbreviate)
// 実行結果:
// [[1, 2], [3, 4], [5, 6], [7, 8]]
print(oneDimensionAbbreviate)
// 実行結果:
// [1, 2, 3, 4, 5, 6, 7, 8]
x.flatMap{y}の部分では、n次元のSequence型レシーバであるxの各構成要素を取り出して(n-1)次元のSequence型変数に格納しています。
そのため、n次元配列を1次元配列にする場合は、.flatMap{return $0}を**(n-1)回繋げて記述**することで平坦化することができます。
また、compactMapで自動的に実行される.filter{ $0 != nil }は、非推奨とされていますが.flatMap{ $0 }としても記述することができます。
この場合のflatMapは、平坦化するflatMapではなく**nilを除外するflatMap**です。
flatMapは以下のように多重定義されています。
extension Sequence {
// 平坦化のflatMap
public func flatMap<SegmentOfResult: Sequence>(
_ transform: (Self.Element) throws -> SegmentOfResult
) rethrows -> [SegmentOfResult.Element] where SegmentOfResult: Sequence {
var result: [SegmentOfResult.Element] = []
for element in self {
result.append(contentsOf: try transform(element))
}
return result
}
// nil除外のflatMap
public func flatMap<ElementOfResult>(
_ transform: (Self.Element) throws -> ElementOfResult?
) rethrows -> [ElementOfResult] {
return try _compactMap(transform)
}
}
extension Sequence{...}の部分ではクラスSequenceの型の拡張(extension)を行っています。つまり、flatMapはクラスSequenceのクラスメソッドとして定義されています。そのため、Sequence型でない変数に対してはflatMapを用いることはできません。
このように、ユーザが定義した要件に応じて任意の型で動作可能な、柔軟で再利用が可能な関数のことをジェネリクス関数(generics function; 総称関数, 汎用関数)と呼びます。
flatMap(平坦化)の定義
public func flatMap<SegmentOfResult: Sequence>の部分では、publicが付いていることから、flatMap関数はパッケージとは無関係に利用できる公開アクセス(public access)であることが分かります。
また、将来的に値が入る領域を確保するプレースホルダ(placeholder; 型パラメータ)としてSequenceクラスの型パラメータSegmentOfResultを設定しています。
(_ transform: (Element) throws -> SegmentOfResult)の部分では、Sequence型変数の構成要素(Self.Element)を引数としてSequence型の値SegmentOfResultを返すクロージャtransformを省略可能なパラメータとしながら、クロージャtransformは、場合によってはエラーの種類を出力する例外(exception)を投げる(=throw)と宣言しています。
(...) rethrows -> [SegmentOfResult.Element] where SegmentOfResult: Sequence {...}の部分では、上記の部分で投げられた例外の例外処理(exception handling)を宣言しています。
クロージャtransformによって例外が投げられた場合、その例外処理はクロージャtransformで行うのではなく、クロージャtransformの呼び出し元であるflatMapで行う(=rethrow)としています。
また、高階関数flatMapは最終的にSequence型のクラス型変数SegmentOfResultの構成要素SegmentOfResult.Elementで構成される(=where ...)配列[SegmentOfResult.Element]を返すと宣言しています。
次は、flatMapの内部処理{}に注目します。
var result: [SegmentOfResult.Element] = []の部分では、配列[SegmentOfResult.Element]に、空の配列[]を参照させることで初期化しています。
そして、for element in self{result.append(contentsOf: try transform(element))}の部分では、配列の中でも**flatMap自身の型(=self)である[SegmentOfResult.Element]型配列の各構成要素を、配列に複数の値を追加するappend(contentsOf:)メソッドを用いて、空の配列[](=result)に次々と追加しています。この部分で、-1次元の次元削減が処理されています。
このとき、配列に値を追加するappend(contentsOf: x)の引数xは配列でなければならない**ため、メソッドtransform(element)によって各構成要素を配列に変換しています。ただし、先述したようにメソッドtransform(element)は場合によってはエラーを出力する可能性があるため、メソッドの宣言の直前にtry文(try statement)を付加しています。
最終的に、return resultで-1次元の次元削減が行われた(=平坦化された)[SegmentOfResult.Element]型の配列が返却されます。
flatMap(nil除外)の定義
外部処理については、平坦化のflatMapとほぼ同様の処理が行われています。その差異は、メソッドtransform()はOptional<ElementOfResult>型である配列を経由しますが、最終的に高階関数compactMapによって[ElementOfResult]型の配列が返却されることです。
内部処理{return try _compactMap(transform)}に注目すると、内部的にfilter{ return $0 != nilが行われるcompactMapが用いられているのが分かります。
この部分で、nilの値は配列から除外されるのです。
オブジェクトとクラス
Swiftのようなオブジェクト指向プログラミング言語(OOPL; Object-Oriented Programming Language)では、値を表すデータ(data)と値を操作するメソッドを枠組みとしたオブジェクトを中心に処理が行われます。
また、オブジェクト同士で同様のデータ・メソッドを持つようなオブジェクトの概念をクラスと呼びます。クラスはあくまで概念(scheme)であり、クラスが持つデータ・メソッドの実体はインスタンス(instance)と呼ばれます。
クラスを宣言する以下のサンプルプログラムに注目しましょう。
class Shape {
var numberOfSides = 0
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}
イニシャライザを定義するクラスShapeでは、「図形の頂点の数」を値とするプロパティ(property; 属性)であるnumberOfSidesを宣言し、初期化子(initializer)として0を参照させています。イニシャライザを使用しない場合、nil安全(nil safe)のために初期化子で初期化(initialize)する必要があります。
また、「図形の頂点の数」を出力するメソッド(method)をsimpleDescriptionと宣言しています。
この時点では、クラスShapeの実体であるインスタンスはまだ生成されていません。あくまで概念が宣言されただけの状態です。
var shape: Shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()
print(shapeDescription)
// 実行結果:
// A shape with 7 sides.
var shape: Shape = Shape()の部分では、クラスShapeで定義したプロパティ・メソッドを持つクラス型変数shapeが、クラスShapeのインスタンスShape()を参照するよう宣言しています。
<クラス>()は既定イニシャライザ(default initializer)と呼ばれ、イニシャライザが設定されていないクラスのインスタンスを生成します。ただし、イニシャライザを設定しない(=既定イニシャライザを使用する)場合は、それぞれのインスタンスプロパティに初期化子が必要です。
イニシャライザと継承(inheritance)
既定イニシャライザを使用せず、イニシャライザをクラス宣言に組み込む場合のサンプルプログラムは以下の通りです。
shape.numberOfSides = 7の部分では、クラス型変数shapeのプロパティshape.numberOfSidesにInt型データである7を参照させています。
var shapeDescription = shape.simpleDescription()の部分では、クラス型変数shapeのメソッドshape.simpleDescription()を参照し実行する変数shapeDescriptionを宣言しています。
class NamedShape {
var numberOfSides: Int = 0
var name: String
// イニシャライザ
init(name: String) {
self.name = name
}
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}
イニシャライザを定義するクラスNamedShapeでは、「図形の頂点の数」を値とする初期化済みプロパティnumberOfSidesに加え、「図形の名前」を値とする未初期化のプロパティnameを宣言しています。
未初期化のプロパティnameは、イニシャライザによって値が代入される必要があります。今回の場合、指定イニシャライザ(designated initializer)となるinit(name: String){ self.name = name }で、仮引数(parameter)であるnameをインスタンスプロパティself.nameに代入し初期化しています。
定義されるクラス自身のプロパティを表す時は、selfで自身のクラスを表現します。selfというキーワードは、仮引数のnameとクラスShapeが持つプロパティnameと区別するために用いられています。
また、このselfは子クラス(サブクラス)を表すときにも使用され、親クラス(スーパークラス)を表すsuperと対照されます。
クラスには、継承(inheritance)という概念があります。動物の遺伝のように、親クラスの持つプロパティやメソッドを、子クラスが引き継ぎます。
クラスの継承を行うサンプルプログラムは以下の通りです。
// クラスNamedShapeの子クラスSquareのクラス宣言
class Square: NamedShape {
var sideLength: Double
// イニシャライザ
init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 4 // 'self.'キーワードの省略
}
func area() -> Double {
return sideLength * sideLength
}
override func simpleDescription() -> String {
return "A square with sides of length \(sideLength)."
}
}
// 子クラスSquareのインスタンス生成
let sampleSquare: Square = Square(sideLength: 5.2, name: "my sample square")
print(sampleSquare.area())
// 実行結果: 27.040000000000003
print(sampleSquare.simpleDescription())
// 実行結果:
A square with sides of length 5.2.
class Square: NamedShape {...}の部分では、クラスNamedShapeを親クラスとする子クラスSquareを定義しています。
var sideLength: Doubleの部分では、子クラスSquareのプロパティとして、親クラスNamedShapeが持っているnumberOfSides・nameに加えて、新たにsideLengthを宣言しています。
init(sideLength: Double, name: String) {...}の部分では、子クラスSquareのイニシャライザを定義しています。このイニシャライザでは、クラスSquareが持つプロパティsideLength・name・numberOfSidesの初期化を行っています。
プロパティsideLengthについては、子クラスSquareで新たに追加したプロパティであり、仮引数をプロパティと同名のselfLengthにしていますが、仮引数とプロパティを区別するためにselfをプロパティのレシーバとして初期化を行います。
一方で、プロパティnameは親クラスNamedShapeのイニシャライザを使い回せるため、super.init(name: name)のように記述して親クラスのイニシャライザを呼び出します。
また、プロパティnumberOfSidesは親クラスNamedShapeのプロパティを継承しているので、子クラスのプロパティであることを明示するためにself.numberOfSidesと記述しますが、子クラスのイニシャライザであることは自明なため、self.キーワードを省略することもできます。
func area{...}の部分では、親クラスNamedShapeにはないメソッドareaを宣言しています。
一方で、override func simpleDescription{...}の部分では、親クラスNamedShapeのメソッドsimpleDescriptionを子クラスSquareでオーバーライド(override)しています。オーバーライドする際は、明示するためにoverride func ...のようにoverrideを先に記述します。
また、メソッドarea{...}ではプロパティsideLengthを2乗することで図形の面積(Squareは「正方形」)を算出しています。
let sampleSquareでは、1辺の長さsideLengthを5.2と設定しているものの、演算結果が27.040000000000003(本当の値は27.04)となっています。これは、10進数表記の小数を2進数に正確に変換できない丸め誤差(rounding error)から誤差が生じています。
10進数の5.2は、整数部と小数部に分けて2進数に基数変換(radix conversion)が行われます。整数部の5は2進数で101と表現することができますが、小数部の0.2を小数で表そうとすると、0.001100110011...のように循環小数(recurring decimal)となるため、計算時に端数調整が必要となります。この端数調整によって誤差が生じます。
ゲッタ(getter)とセッタ(setter)
プロパティには、変数の値や属性を取得するゲッタ(getter)や変数の値や属性を設定するセッタ(setter)としての役割を持たせることもできます。
プロパティにゲッタとセッタの役割を持たせたサンプルプログラムは以下の通りです。
// 親クラスNamedShapeの子クラスEquilateralTriangleのクラス宣言
class EquilateralTriangle: NamedShape {
var sideLength: Double = 0.0 // エラー防止のため'0.0'で初期化
// イニシャライザ
init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 3 // 'self.'キーワードの省略
}
// ゲッタ・セッタのプロパティperimeter
var perimeter: Double {
// 「1辺の長さ」から「周囲の長さ」を取得
get {
return 3.0 * sideLength
}
// 「周囲の長さ」から「1辺の長さ」を設定
set {
sideLength = newValue / 3.0
}
}
// 1辺の長さを出力するようオーバーライドした関数simpleDescription
override func simpleDescription() -> String {
return "An equilateral triangle with sides of length \(sideLength)."
}
}
// 子クラスEquilateralTriangleのインスタンス生成
var sampleTriangle: EquilateralTriangle = EquilateralTriangle(sideLength: 3.1, name: "sample triangle")
print(sampleTriangle.perimeter)
// 実行結果: 9.3
// プロパティperimeterの値を変更
sampleTriangle.perimeter = 9.9
print(sampleTriangle.sideLength)
// 実行結果: 3.3000000000000003
クラスEquilateralTriangleは、親クラスNamedShapeを継承(inherit)した子クラス(child class)です。
var sideLength: Double = 0.0の部分では、プロパティsideLengthは親クラスNamedShapeにも存在するプロパティであり、イニシャライザによって値が代入されるため= 0.0と初期化する必要はありませんが、エラー防止のために0.0で初期化しています。
var perimeter: Double {...}の部分では、正三角形の「周囲の長さ」を表すクラスEquilateralTriangleで新たに追加するプロパティperimeterを宣言しています。
プロパティperimeterは、「1辺の長さ」を元に「周囲の長さ」を取得するゲッタとしての役割と、プロパティperimeterに値が代入された場合に、与えられた**「周囲の長さ」を元に「1辺の長さ」を設定**するセッタとしての役割を持っています。
set { sideLength = newValue / 3.0 }の部分では、プロパティperimeterの値が変更された場合の値をnewValueとし、newValueの値を元に「1辺の長さ」を表すプロパティsideLengthが設定されるようにしています。
また、親クラスNamedShapeは図形が「四角形」であることを前提として作られているため、1辺の長さを出力する際は**A square** with sides of length ...と出力されるように関数simpleDescriptionを定義していました。
子クラスEquilateralTriangleに属する図形は「正三角形」であるため、親クラスNamedShapeのsimpleDescription()関数をoverride func simpleDescription ...と記述することでオーバーライドしています。
var sampleTriangle: EquilateralTriangle = ...の部分では、クラスの実体(インスタンス)であるsampleTriangleを生成し、初期値として「1辺の長さ」を与え、その「1辺の長さ」を元に「周囲の長さ」を取得するゲッタperimeterが機能しているか確認しています。
また、sampleTriangle.perimeter = 9.9以降の部分では、インスタンスsampleTriangleの「周囲の長さ」を表すプロパティperimeterの値を変更し、変更された「周囲の長さ」の値を元に「1辺の長さ」を設定するセッタperimeterが機能しているか確認しています。
ただし、先述した丸め誤差によって、出力される値に誤差が生じています。
プロパティオブザーバ(property observer)
上記のサンプルプログラムにおいて、プロパティperimeterは「(1辺の長さ)⇔(周囲の長さ)」を演算によって求め合う計算型プロパティ(computed property)でした。
一方で、計算型プロパティと対照的な存在である、値を保持する格納型プロパティ(stored property)という種類のプロパティも存在します。
そして、格納型プロパティの値が更新された場合に処理を行うプロパティオブザーバ(property observer)という仕組みがあります。プロパティオブザーバは、プロパティの変更前や変更後の値を処理に使用したい場合に有効な手法です。
プロパティオブザーバを用いたサンプルプログラムは、以下の通りです。
// クラスTriangleAndSquareのクラス宣言
class TriangleAndSquare {
//
var triangle: EquilateralTriangle {
willSet {
square.sideLength = newValue.sideLength
}
}
//
var square: Square {
willSet {
triangle.sideLength = newValue.sideLength
}
}
// イニシャライザ
init(size: Double, name: String) {
//
triangle = EquilateralTriangle(sideLength: size, name: name)
//
square = Square(sideLength: size, name: name)
}
}
// インスタンス生成
var sampleTriangleAndSquare: TriangleAndSquare = TriangleAndSquare(size: 10, name: "sample shape")
print(sampleTriangleAndSquare.square.sideLength)
// 実行結果: 10.0
print(sampleTriangleAndSquare.triangle.sideLength)
// 実行結果: 10.0
// クラスSquare側での値変更
sampleTriangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(sampleTriangleAndSquare.square.sideLength)
// 実行結果: 50.0
print(sampleTriangleAndSquare.triangle.sideLength)
// 実行結果: 50.0
print(sampleTriangleAndSquare.square.name)
// 実行結果: larger square
print(sampleTriangleAndSquare.triangle.name)
// 実行結果: sample shape
クラスTriangleAndSquareのインスタンスは、内部に別クラスEquilateralTriangle・Squareのインスタンスを有するコンポジション(composition; 合成)の構造を取っています。
クラスTriangleAndSquareとクラスTriangle・Squareのようなクラス関係をhas-A関係と呼びます。
一方で、親クラスNamedShapeと、クラスNamedShapeを継承した子クラスEquilateralTriangle・Squareのようなクラス関係をis-A関係と呼びます。
var triangle ...とvar square ...の部分では、クラスTriangleAndSquareのプロパティtriangle・squareにプロパティオブザーバとしての役割を持たせています。
willSet {...}の部分では、インスタンスの生成時やプロパティtriangle・squareの参照先の変更時に、もう一方のプロパティ(triangleまたはsquare)に対して、自身の変更されたプロパティが保持するプロパティsideLengthを代入させ、互いのプロパティsideLengthが同じ値になるように設定しています。
参照先が変更された「後」の値を基準としてもう一方のプロパティの値を設定するため、セッタの定義では**willSet {...}**を用いています。
var sampleTriangleAndSquare: TriangleAndSquare ...の部分では、クラスTriangleAndSquareのインスタンスsampleTriangleAndSquareを生成しています。
このインスタンスの生成時に、クラスTriangleAndSquareのプロパティtriangle・squareが実体として生成され、互いに持つプロパティsideLengthが同じ値をとるようにセッタが働き合います。
sampleTriangleAndSquare.square = ...の部分では、クラスTriangleAndSquareのインスタンスsampleTriangleAndSquareの参照先をプロパティsideLength: 50, name: "larger square"のクラスSquareのインスタンスSquare(...)に変更しています。
参照先の変更によって、クラスTriangleAndSquareのプロパティsquareが持つプロパティsideLengthの変更後の値50(.0)が変数newValueに代入されます。そして、クラスTriangleAndSquareのプロパティtriangleのwillSet {...}の部分で、プロパティtriangle.sideLengthの値が元々格納されていた10.0から50.0に変更されます。
一方で、セッタとしての機能を持つプロパティtriangle・squareのwillSet {...}の部分では、それぞれが持つプロパティnameの値には言及されていないため、プロパティtriangle.nameの値は最初に設定された"sample shape"のままですが、プロパティsquare.nameの値は"larger shape"に変更されていることが分かります。
オプショナル型インスタンス
Optional型変数の宣言時には、型の直後に?を付けることで変数がOptional型であることを宣言していました。
Optional型のクラス型変数(=インスタンス)の宣言も同様に、インスタンスのクラス型の直後に?を付けることでOptinal型インスタンスを宣言することができます。
また、Optional型インスタンスが持つプロパティを変数に参照し、print()関数で出力する場合は、参照先を記述する際にOptional型インスタンス名の直後にオプショナルチェイニング?または強制アンラップ!を行う必要があります。print()関数は、非Optional型変数のみでしか引数として認めていないためです。
Optional型インスタンスを宣言するサンプルプログラムは以下の通りです。
let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
// オプショナルチェイニング'?'
let implicitlyUnwrappedOptionalSideLength = optionalSquare?.sideLength
// 強制アンラップ'!'
let forcedUnwrappedOptionalSideLength = optionalSquare!.sideLength
print(implicitlyUnwrappedOptionalSideLength)
// 実行結果: Optional(2.5)
print(forcedUnwrappedOptionalSideLength)
// 実行結果: 2.5
列挙型と構造体
参照型(reference type)と値型(value type)
なお、クラスのデータ型は、参照型(reference type)であったのに対し、以下で紹介する列挙型および構造体のデータ型は値型(value type)です。
※詳しくは[こちら]
(https://qiita.com/koher/items/bcdbf6578b6edd1f9e0c)
参照型と値型の挙動の違いを表すサンプルプログラムは、以下の通りです。
// 参照型のクラス
class Reference {
var a, b: Int
// イニシャライザ
init(a: Int, b: Int) { self.a = a; self.b = b }
}
// 参照型の構造体
struct Value {
var a, b: Int
}
// インスタンスの生成
let reference = Reference(a: 3, b: 5)
let value = Value(a: 3, b: 5)
// 参照型の挙動
reference.a = 6
print("Reference(a: \(reference.a), b: \(reference.b))")
// 実行結果:
// Reference(a: 6, b: 5)
reference = Reference(a: 6, b: 5)
// 実行結果:
// error: cannot assign to value
// 値型の挙動
value.a = 6
// error: cannot assign to property
value = Value(a: 3, b: 5)
// error: cannot assign to value
let ...の部分では、参照型であるクラスと、値型である構造体のインスタンスを**定数として生成**しています。
にもかかわらず、reference.a = 6の部分では、参照型のインスタンスreferenceのプロパティaを再設定することができています。これは、インスタンスreferenceが、初期値であるReference(a: 3, b: 5)が確保している**領域(space)**しか見ていないためです。
つまり、参照型であるクラスにとって、「定数」としてイミュータブル(immutable; 変更不可)であってほしいのはその「領域」であって、領域に格納されている値ではないのです。このことは、reference = Reference(a: 6, b: 5)の部分で、格納される値は同じではあるものの、領域が異なるデータを参照させようとして実行時エラーを吐いていることからも分かります。
一方で、値型のインスタンスvalueは、value.a = 6およびvalue = Value(a: 3, b: 5の部分で「領域に格納されている値」および「領域」を変更しようとして実行時エラーが吐き出されました。
このことから、値型である構造体にとって、「定数」としてイミュータブル(immutable; 変更不可)であってほしいのはその「領域」および「領域に格納されている値」であることが分かります。
また、先述したように、構造体のストアドプロパティ(stored property)はデフォルトでイミュータブルですが、var・mutating・inoutのキーワードを用いることで、ミュータブル(mutable; 変更可能)な状態に変えることもできます。
さらに、参照型と値型では、インスタンスをコピーした場合に、元インスタンスからコピーインスタンスに対して「渡される(be passed)もの」が異なります。
それを確かめる以下のサンプルプログラムに注目しましょう。
// 「参照型」のクラス
class Reference {
var a: String
var b: String
init(a: String, b: String){
self.a = a
self.b = b
}
// スワップメソッド
func swap() {
var tmp = a
a = b
b = tmp
}
}
// 「値型」の構造体
struct Value {
var a: String
var b: String
init(a: String, b: String){
self.a = a
self.b = b
}
// スワップメソッド
mutating func swap2() {
var tmp = a
a = b
b = tmp
}
}
// インスタンス生成
var referenceType = Reference(a: "瀧", b: "三葉")
var valueType = Value(a: "瀧", b: "三葉")
// インスタンスのコピー
var referenceTypeCopied = referenceType
var valueTypeCopied = valueType
// 事前チェック
print("事前チェック: \(referenceTypeCopied.a), \(referenceTypeCopied.b)")
// 実行結果: 事前チェック: 瀧, 三葉
print("事前チェック: \(valueTypeCopied.a), \(valueTypeCopied.b)")
// 実行結果: 事前チェック: 瀧, 三葉
// コピーインスタンスのスワップ
referenceTypeCopied.swap()
valueTypeCopied.swap2()
// 検証結果
print("元データ: \(referenceType.a), \(referenceType.b)")
// 実行結果: 元データ: 三葉, 瀧
print("元データ: \(valueType.a), \(valueType.b)")
// 実行結果: 元データ: 瀧, 三葉
print("変更後データ: \(referenceTypeCopied.a), \(referenceTypeCopied.b)")
// 実行結果: 元データ: 三葉, 瀧
print("変更後データ: \(valueTypeCopied.a), \(valueTypeCopied.b)")
// 実行結果: 元データ: 三葉, 瀧
スワップメソッドの定義では、参照型はデフォルトでミュータブルな値を取るため、メソッドの宣言時に、メソッドによってプロパティの値が変更されることを明示するための**mutating**キーワードは付加されていませんが、値型はデフォルトでイミュータブルな値を取るため、メソッドの宣言時にmutatingキーワードを付加しています。
インスタンス生成の部分では、クラス・構造体の元インスタンスをそれぞれreferenceType・valueTypeとして生成しています。
インスタンスのコピーの部分では、クラス・構造体のコピーインスタンスをそれぞれreferenceTypeCopied・valueTypeCopiedとして生成しています。
コピーインスタンスのスワップの部分では、コピーインスタンスに対してスワップメソッドを実行した結果、検証結果の出力結果から分かるように、参照型と値型で元データに差異が生じています。
参照型のインスタンスは、「参照先」を渡しています。そのため、参照先に格納されているデータ(=ストアドプロパディ)が変更されると、元インスタンス・コピーインスタンスの両方で値の変更が生じます。
一方で、値型のインスタンスは、「値」を渡しています。そのため、コピーインスタンスのストアドプロパティが変更されても、元インスタンスのストアドプロパティは変更されません。
列挙型(enumeration)
列挙型(enumeration)は、関連するデータを一つにまとめる定数です。Swiftにおける列挙型は、列挙型の定義にメソッドを含め、動作や機能を実現することもできます。ただし、列挙型によって列挙されたデータの値を操作することはできません。
値を操作する必要のない、関連性の高いデータが複数ある場合は、var x, y ...のようにバラバラに書くのではなく、列挙型で一まとめにして記述することで、可読性を向上させ、管理しやすくしておきましょう。
ただし、前述したように列挙型のデータの値はそもそも変更することができないため、サイズの大きなデータを取り扱う時に、メモリアドレスを渡し合うクラスはプログラムのサイズがさほど膨らまないのに対し、データをコピーする列挙型はプログラムのサイズが顕著に膨らむ、くらいの理解に留めておきましょう。
値型(value type)の列挙型
列挙ケースそれぞれが値を持つような列挙型を、「値型(value type)の列挙型」と呼びます。
列挙型を宣言するサンプルプログラムは、以下の通りです。
enum Rank: Int {
case ace = 1
case two, three, four, five, six, seven, eight, nine, ten
case jack, queen, king
func simpleDescription() -> String {
switch self {
case .ace: return "ace"
case .jack: return "jack"
case .queen: return "queen"
case .king: return "king"
default: return String(self.rawValue)
}
}
}
let ace = Rank.ace
let aceRawValue = ace.rawValue
print(ace)
// 実行結果: ace
print(aceRawValue)
// 実行結果: 1
enum Rank: Int {...}の部分では、Int型の列挙型Rankを宣言し、関連するデータを一まとめにしています。
ここで、列挙型の列挙されたデータは列挙ケース(enumeration case)と呼ばれます。列挙ケースは、case xと記述して宣言します。
case ace = 1の部分では、列挙ケースaceに対して、Int型の数値1を代入しています。
列挙ケースに割り当てられた値(=整数リテラル(integer literal))を、実体値(raw value)と呼び、実体値の型は実体型(raw type)と呼びます。
また、今回のように実体型をInt型とする列挙型では、列挙ケースそれぞれに実体値を設定しない場合、1番目の列挙ケースの値を基に、2番目以降の列挙ケースにも自動的に値が連番で代入されます。つまり、ace = 1と代入された時点で、自動的にtwo = 2, three = 3, ...のように値が代入されています。なお、Int型の列挙型で1番目の列挙ケースにも値を設定しない場合は、1番目の列挙ケースから順に、0, 1, 2, ...と既定の実体値が設定されます。
func simpleDescription ... {...}の部分では、それぞれの列挙ケースに対する処理を記述することで、列挙ケースと列挙ケースに依存するメソッドを一体のものとして列挙型を定義しています。
let ace = Rank.aceの部分では、定数aceに対して、列挙型Rankの列挙ケースaceを参照させています。
また、let aceRawValue = ace.rawValueの部分では、定数aceRawValueに対して、列挙ケースaceの持つ実体値ace.rawValueを参照させています。
上記プログラムでは、列挙型のインスタンスを基に実体値を出力しましたが、実体値を基に列挙型のインスタンスを出力することもできます。
実体値を基にインスタンスを出力し、出力したインスタンスを基に列挙型の持つメソッドを実行するサンプルプログラムは以下の通りです。
if let convertedRank = Rank(rawValue: 13) {
let kingDescription = convertedRank.simpleDescription()
print(kingDescription)
}
// 実行結果: king
if let ...の部分では、イニシャライザinit?(rawValue:)を使って、実体値に13を持つ列挙型Rankのインスタンスを生成し、そのインスタンスを定数convertedRankに代入しています。
オプショナルバインディングを使用しているのは、例えばRank(rawValue: 38)のように、実体値に38を持つ列挙ケースがない場合はインスタンスがnilとなりますが、nilのインスタンスに対してメソッドsimpleDescriptionを実行しないようにするためです。
実体値から列挙型のインスタンスを取得する際は、if let ...のようにオプショナルバインディングを用いるようにしましょう。
シンプルな列挙型
なお、実体値を持たない列挙型も宣言することができます。
実体値を持たない列挙型を宣言するサンプルプログラムは、以下の通りです。
enum Suit {
case spades, hearts, diamonds, clubs
func simpleDescription() -> String {
switch self {
case .spades: return "knight"
case .hearts: return "priest"
case .diamonds: return "merchant"
case .clubs: return "farmer"
}
}
}
let hearts = Suit.hearts
let heartsDescription = hearts.simpleDescription()
print(hearts)
// 実行結果: hearts
print(heartsDescription)
// 実行結果: priest
共用型(union type)の列挙型
列挙型Rank・Suitなどの**値型の列挙型の列挙ケースは、実体型が一つに限定**されていました。一方で、複数の異なるタプル(tuple)の構造を併せ持つことのできる共用型(union type)の列挙型も存在します。
共用型の列挙型では、それぞれの型が異なる列挙ケースを定義することができます。そして、switch文によって、インスタンスの基となる列挙ケースを判定し、判定した列挙ケースによって処理を分岐させることもできます。
以下のサンプルプログラムでは、インスタンスの列挙ケースに基づいて分岐処理を実行しています。
enum ServerResponse {
case result(String, String)
case failure(String)
}
let success = ServerResponse.result("6:00 am", "8:09 pm")
let failure = ServerResponse.failure("Out of cheese.")
switch success {
case let .result(sunrise, sunset):
print("Sunrise is at \(sunrise), sunset is at \(sunset).")
case let .failure(message):
print("Failure... \(message).")
}
// 実行結果:
// Sunrise is at 6:00 am and sunset is at 8:09 pm.
let ...の部分では、列挙型ServerResponseのインスタンスを生成しています。定数successは列挙ケースresultの、定数failureは列挙ケースfailureのインスタンスです。
そして、switch success {...}の部分では、定数successが列挙ケースresult・failureのどちらの性質を有するかによって処理を分岐させています。
case x:のラベルxの部分では、今回のようにラベルxの持つ値が入力によって変化するため、case let y(z ...):と記述することで、列挙ケースyの部分だけを判定材料にするようにします。そして、列挙ケースyに合致すれば、仮引数zの部分に、ラベルxの持つ値(=実引数(argument))が代入されます。
構造体(structure)
構造体も、列挙型と同様に値型のデータであるため、構造体のデータは代入や関数呼び出しの際、値がコピーされ、元のデータの値は変更されません。
struct Card {
var rank: Rank
var suit: Suit
func simpleDescription() -> String {
return "The \(rank.rawValue) of \(suit)"
}
}
let threeOfSpades = Card(rank: .three, suit: .spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()
print(threeOfSpades)
// 実行結果:
// Card(rank: main.Rank.three, suit: main.Suit.spades)
print(threeOfSpadesDescription)
// 実行結果:
// The 3 of spades.
let threeOfSpades ...の部分では、構造体Cardのインスタンスを生成していますが、出力結果を見ると、プロパティrank・suitの中身にそれぞれmain.と付いているのが分かります。
これは、プロパティRank.three・Suit.spadesがそれぞれ筆者が普段使っているmain.swiftのファイルに属していることを表しています。つまり、ここでのmainは「ファイル名」を指しています。
プロトコル(protocol)と拡張(extension)
プロトコル(protocol)は、型の持つメソッドやプロパティをまとめる抽象的な概念であり、C言語やJavaでいうインタフェースに相当します。
プロトコル同様、メソッドやプロパティを一まとめにするクラスとの違いは、その「抽象性」にあります。
クラスは具体性が高いため、インスタンスの生成が可能です。
一方で、プロトコルはあくまで「概念」であるため、プロトコルの宣言時には、メソッドやプロパティの中身までは記述せず、メソッドやプロパティの持つ性質だけを記述します。
例えば、メソッドの宣言では、ストアドプロパティの値が変更される場合、その旨を明示するmutatingキーワードだけを付加し、{...}にあたるコードブロックは記述しません。
また、プロパティの宣言では、プロパティの性質である定数・変数および型以外に、「アクセス制御(accessibility)」を「読み出し(read)」のget、「書き出し(write)」のsetで記述するだけであり、プロパティの持つ値までは記述しません。
プロトコルの宣言
プロトコルを宣言するサンプルプログラムは、以下の通りです。
protocol SampleProtocol {
var simpleDescription: String { get }
mutating func adjust()
}
プロトコルの採用(adopt)
宣言したプロトコルのプロパティ・メソッドをオブジェクトに定義することをプロトコルを「採用(adopt)する」といいます。また、プロトコルを採用されたオブジェクトは、そのプロトコルに「**適合(confirm; 準拠)**している」といいます。
プロトコルをオブジェクトに採用するサンプルプログラムは、以下の通りです。
class SimpleClass: SampleProtocol {
var simpleDescription: String = "A very simple class."
var anotherProperty: Int = 69105
func adjust() {
simpleDescription += " Now 100% adjusted."
}
}
// インスタンスの生成
var a = SimpleClass()
print(a.simpleDescription)
// 実行結果:
// A very simple class.
// プロパティa.simpleDescriptionに" Now 100% adjusted."を追加
a.adjust()
print(a.simpleDescription)
// 実行結果:
// A very simple class. Now 100% adjusted.
struct SimpleStructure: SampleProtocol {
var simpleDescription: String = "A simple structure."
mutating func adjust() {
simpleDescription += "(adjusted)"
}
}
// インスタンスの生成
var b = SimpleStructure()
print(b.simpleDescription)
// 実行結果:
// A simple structure.
// プロパティb.simpleDescriptionに"(adjusted)"を追加
b.adjust()
print(b.simpleDescription)
// 実行結果:
// A simple structure.(adjusted)
プロトコルの採用は、クラスや構造体の宣言時に、型アノテーションとして採用するプロトコルを記述するだけです。
プロトコルの宣言では、プロトコルが持つプロパティ・メソッドの中身を定義しないため、プロトコルを採用する各オブジェクト宣言の中で、プロパティ・メソッドを**実装(implement)**します。
オブジェクトの宣言では、プロトコルにないプロパティ・メソッドを独自に追加することも可能です。ただし、各オブジェクトは、プロトコルで定めたプロパティ・メソッドを必ず含んでいなければなりません。
ここで、プロトコルSampleProtocolのメソッドadjust()の実装部分に注目しましょう。
参照型であるクラスSimpleClassでは、実装する際にfunc adjust() {...}と今まで通りの定義を行っています。
一方で、値型である構造体SimpleStructureでは、実装時にmutating func adjust() {...}と、mutatingキーワードを付加しています。
mutatingキーワードは、プロパティの「値」を変更するメソッドに対して記述します。なお、ここでの「変更」は、「暗黙的(implecit)な再代入(reassign)」を指しています。そのため、定数に格納されている値型のインスタンス(=ストアドプロパティ)には実行できません。
クラスは「参照型」であるため、adjust()メソッドによって変更されるのは、値ではなく参照先です。
今回の場合は、プロパティsimpleDescriptionは初期値として"A very simple class"を参照していましたが、adjust()メソッドによって、参照先を"A very simple class. Now 100% adjusted."に変更しています。初期値として指定されたメモリアドレスには"A very simple class"という値が残り続け、新たに"A very simple class. Now 100% adjusted."の値を格納するメモリアドレスを参照しているのです。
以上の理由から、mutatingキーワードは記述しません。
プロトコルの拡張(extension)
プロトコルは、クラスや構造体といったオブジェクトに限らず、データ型に対しても採用することができます。ただし、データ型は既定で定義されているため、「定義を拡げる」という意味で「拡張(extension)」と呼ばれます。
データ型を拡張するサンプルプログラムは、以下の通りです。
extension Int: SampleProtocol {
var simpleDescription: String {
return "The number \(self)"
}
mutating func adjust() {
self += 42
}
}
var num: Int = 7
print(num)
// 実行結果: 7
print(num.simpleDescription)
// 実行結果: The number 7
num.adjust()
print(num)
// 実行結果: 49
print(num.simpleDescription)
// 実行結果: The number 49
変数(variable)・定数(constant)へのプロトコルの採用
プロトコルは、様々なオブジェクトに採用することができます。
変数・定数もオブジェクトの一つであるため、プロトコルを採用することができます。
以下のサンプルプログラムに注目しましょう。
let protocolValue: SampleProtocol = a // aはクラスSimpleClassのインスタンス
print(protocolValue.simpleDescription)
// 実行結果:
// A very simple class. Now 100% adjusted.
print(protocolValue.anotherProperty)
// 実行結果:
// error: value of type 'SampleProtocol' has no member 'anotherProperty'
定数protocolValueのデータ型はSampleProtocolであるため、クラスSimpleClassの値を持つインスタンスaが代入されていても、プロトコルSampleProtocolが持つプロパティしか格納されていません。
そのため、定数protocolValueは、クラスSimpleClassで独自に作られたプロパティanotherPropertyを有していません。
エラー処理(error handling)
システムを作成するにあたって、発生する可能性のあるエラー(error)に対して、その発生と内容を通知するような処理を定義しておくのが望ましいです。
Swiftでは、プログラムの実行中に何らかの原因によって通常処理が継続できなくなった場合、適切な呼び出し位置に一気に戻り、状況に応じた処理を行うエラー処理構文(error handling syntax)という仕組みが存在します。
エラーの定義
まずは、エラーを定義するサンプルプログラムに注目しましょう。
enum PrinterError: Error {
case outOfPaper
case noToner
case onFire
}
エラーの種類は多種多様ですが、あらかじめ想定されるエラーを区分し、その種類ごとに列挙型でまとめておくと、可読性が向上します。
エラー通報関数(throwing function)の定義
また、エラーを投げる(throw)可能性のある関数を、エラー通報関数(throwing function)と呼びます。
エラー通報関数を定義するサンプルプログラムは、以下の通りです。
func send(job: Int, toPrinter printerName: String) throws -> String {
switch printerName {
case "Never Has Paper": throw PrinterError.outOfPaper
case "Never Has Toner": throw PrinterError.noToner
case "Power Strip": throw PrinterError.onFire
default: break
}
return "Job sent."
}
引数リストのtoPrinter printerName: Stringの部分では、toPrinterを仮引数ラベル(parameter label)として、仮引数printerNameを設定しています。実引数(argument)を定義する際は、仮引数ラベルであるtoPrinter:を使用します。
エラー通報関数の定義では、引数リスト(...)の直後にthrowsキーワードを付ける必要があります。また、実際にエラーを投げる部分ではthrowを記述し、その直後に投げるエラーの種類を記述します。
func send(...) throws -> Stringの部分では、関数send(...)がエラーを投げる可能性があることを示唆しています。
if ... { throw PrinterError.noToner}の部分では、条件に一致する場合はエラーPrinterError.noTonerを投げるように定義しています。
エラーの捕捉(catch)
関数によってエラーが投げられた場合、そのエラーを捕捉(catch)してエラーに対処する必要があります。
また、エラーを投げる関数を実行する際は、tryキーワードを付けて呼び出す必要があります。
エラーの捕捉は、do-catch構文(do-catch syntax)のdo節(do clause)内部で関数の呼び出しを行うことで、エラーに対応するcatch節(catch clause)でエラーを捕捉することができます。
エラーが投げられると、投げられたエラーの値とcatch節の条件でパターンマッチングを行います。catch節は複数個連続して定義することができますが、パターンマッチングは上から順に実行されます。そのため、具体的な条件を指定するcatch節を上に置き、包括的な条件を指定するcatch節は下に置くようにしましょう。
なお、エラーが発生した場合、そのエラーが捕捉されるまで処理の呼び出し元に戻り続けますが、この動作をエラーの伝播(propagation)と呼びます。
エラーの捕捉を行うサンプルプログラムは、以下の通りです。
do {
let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")
print(printerResponse)
} catch {
print(error)
}
// 実行結果: Job sent.
do {
let printerResponse = try send(job: 1050, toPrinter: "Never Has Toner")
print(printerResponse)
} catch {
print(error)
}
// 実行結果: noToner
上記プログラムでは、エラーパターンに関わらず全てのエラーを捕捉するcatch節でエラーを捕捉しています。全てのエラーを捕捉するcatch節では、errorという名前の定数でエラーの値を参照することができます。
do {
let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")
print(printerResponse)
} catch PrinterError.onFire {
print("I'll just put this over here, with the rest of fire.")
} catch let printerError as PrinterError {
print("PrinterError: \(printerError).")
} catch {
print(error)
}
// 実行結果: Job sent.
do {
let printerResponse = try send(job: 1440, toPrinter: "Power Strip")
print(printerResponse)
} catch PrinterError.onFire {
print("I'll just put this over here, with the rest of fire.")
} catch let printerError as PrinterError {
print("PrinterError: \(printerError).")
} catch {
print(error)
}
// 実行結果:
// I'll just put this over here, with the rest of fire.
do {
let printerResponse = try send(job: 1440, toPrinter: "Never Has Paper")
print(printerResponse)
} catch PrinterError.onFire {
print("I'll just put this over here, with the rest of fire.")
} catch let printerError as PrinterError {
print("PrinterError: \(printerError).")
} catch {
print(error)
}
// 実行結果:
// PrinterError: outOfPaper.
catch PrinterError.onFire {...}の部分では、捕捉するエラーパターンを、エラーPrinterError.onFireと指定しています。
catch let printerError as PrinterErrorの部分では、捕捉するエラーパターンを、エラーPrinterErrorと指定し、条件に該当する場合は、そのエラーを定数printerErrorに代入しています。
また、キャスト演算子(cast operator)であるasによって、定数printerErrorのデータ型をPrinterErrorにキャスト(=型変換)しています。
最後のcatchの部分では、上記2パターンに該当しないエラーパターンを捕捉しています。なお、catchはcatch let errorと同義です。
オプショナル型への変換(convert)によるエラー処理
エラー通報関数の呼び出し方として、try?を使って呼び出す方法も存在します。
try?を使うことで、返却される値がオプショナル型に変換されます。つまり、エラーが発生する場合は**nil、発生しない場合はオプショナル型で値が返却**されます。
この方法は容易に扱うことができますが、エラーの原因(=エラーパターン)は特定できません。
let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")
print(printerSuccess)
// 実行結果: Optional("Job sent.")
print(printerFailure)
// 実行結果: nil
エラーが発生する場合にnilが返却される理由は、関数sendのコードブロックにあります。
関数sendのコードブロックに注目すると、エラーが発生しない場合はreturn "Job sent."と、返却値が存在します。一方で、エラーが発生する場合はthrow <エラー>のように、エラーが投げられるだけで返却値はありません。
よって、返却値がないため、実行結果がnilになります。
処理の中断・終了時の処理(defer文)
エラーの発生に伴い、コードブロック内の処理が中断される場合があります。一方で、エラーが発生せず、コードブロック内の処理が正常に終了する場合もあります。
どちらの場合でも最後に必ず実行してほしい処理がある場合は、defer文(defer statement)を使いましょう。
defer文を使用したサンプルプログラムは、以下の通りです。
var fridgeIsOpen: Bool = false
let fridgeContent: [String] = ["milk", "eggs", "leftovers"]
func fridgeContains(_ food: String) -> Bool {
fridgeIsOpen = true
defer {
fridgeIsOpen = false
}
let result: Bool = fridgeContent.contains(food)
return result
}
print(fridgeContains("banana"))
// 実行結果: false
print(fridgeContains("milk"))
// 実行結果: true
print(fridgeContent)
// 実行結果: ["milk", "eggs", "leftovers"]
print(fridgeIsOpen)
// 実行結果: false
変数fridgeIsOpenは、冷蔵庫の開閉状態を示すフラグです。
関数fridgeContainsは、指定した食べ物が冷蔵庫内に入っているか確認するメソッドで、確認のために最初に冷蔵庫を開けます(=fridgeIsOpen = true)。
let result ...部分の.contains(x)メソッドは、対象のプロパティに引数xが含まれているかどうかをBool型で返すメソッドです。
defer {...}の部分では、defer文が記述される、1つ外側のコードブロック内の処理が中断または終了した時に、必ずdefer文のコードブロック{...}を実行するよう宣言しています。
今回の場合は、確認のために開けた冷蔵庫を閉める(fridgeIsOpen = false)ように宣言しています。
ジェネリクス(generics)
ジェネリクス(generics)とは、型をパラメータとしてプログラムを記述するための機能です。
これまでのオブジェクトは、特定の型を指定して宣言していました。
ジェネリクスは、山括弧<>を用いることで、任意の型を型パラメータ(プレースホルダ)として指定することができます。この型パラメータによって、プログラムの中でオブジェクトに対して、具体的な型をパラメータとして与えながら、新しい型を作成することができます。
ジェネリクス関数(generics function)
ジェネリクスの機能を用いて宣言する関数を、ジェネリクス関数(generics function; 総称関数, 汎用関数)と呼びます。
ジェネリクス関数を宣言するサンプルプログラムは、以下の通りです。
func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
var result = [Item]()
for _ in 0..<numberOfTimes {
result.append(item)
}
print(type(of: result))
// 実行結果: Array<String>
return result
}
print(makeArray(repeating: "knock", numberOfTimes: 4))
// 実行結果:
// ["knock", "knock", "knock", "knock"]
func makeArray<Item>(...)の部分では、型パラメータとしてItemを与えながら、新たな型Itemを作成しています。作成した時点では、型Itemが宣言されただけであり、実体であるインスタンスは生成されていません。
var result = [Item]()の部分では、新たに作成したItem型の配列[Item]のインスタンスを、既定イニシャライザ()を用いて生成し、変数resultに代入しています。
for _ in 0..<numberOfTimes {...}の部分では、定数をワイルドカード(wildcard)である_として、範囲演算子(range operator)である..<を用いて、0回目からnumberOfTimes - 1回目までnumberOfTimes回の繰り返し処理を行っています。
result.append(item)の部分では、Array型のメソッド.append(x)を用いて、仮引数xを配列の要素に追加しています。
ここで、変数resultには型パラメータがItemの配列[Item]を代入しましたが、型推論によって変数resultの型がArray<String>(=[String])になっていることが分かります。
型パラメータはあくまでプレースホルダとして領域を確保しているだけであり、型推論によって適切な型に変換されるのです。
ジェネリクスによる型定義
ジェネリクス機能を用いた関数であるジェネリクス関数の次は、ジェネリクス機能を用いた型の定義に注目しましょう。
標準ライブラリで定義されているOptional型の型の再定義を行うサンプルプログラムは、以下の通りです。
enum OptionalValue<Wrapped> {
case none
case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
print(possibleInteger)
// 実行結果: none
print(type(of: possibleInteger))
// 実行結果: OptionalValue<Int>
possibleInteger = .some(100)
print(possibleInteger)
// 実行結果: some(100)
print(type(of: possibleInteger))
// 実行結果: OptionalValue<Int>
Optional型は、値が存在しなければnilを、値xが存在すればOptional(x)を出力する型でした。
enum OptionalValue<Wrapped>の部分では、共用型の列挙型の名前をOptionalValue、型パラメータとしてWrappedを設定しながら、値が存在しない場合の列挙ケースをnone、値が存在する場合の列挙ケースをsome(Wrapped)と定義しています。
var possibleInteger ...の部分では、型パラメータを<Wrapped>とする列挙型OptionalValue<Wrapped>のインスタンス変数possibleIntegerに対して、データ型をOptionalValue<Int>、列挙ケースを.noneに指定して代入しています。
<Wrapped>は、領域を確保するだけで機能を持たない型パラメータのため、実際のデータ型として(OptionalValue)<Int>を指定しています。
Int(OptionalValue<Int>)型であるにも関わらず、列挙ケース名と同値であるnoneを値として格納できているのは、変数possibleIntegerが列挙型OptionalValue<Wrapped>を採用しており、その列挙ケースnoneにはWrappedの記述がなく、Wrappedから独立しているためです。
型パラメータ(type parameter)の記述
型パラメータの書き方は以下の通りです。
-
<T>:Tを型パラメータとして定義 -
<T,U>:TおよびUを型パラメータとして定義 -
<T: OtherType>:Tは、プロトコルOtherTypeに適合またはクラスOtherTypeまたはクラスOtherTypeのサブクラス -
T: OtherType: 同上 -
where 条件: 型パラメータの条件指定 -
T == U: 「型(パラメータ)T == 型(パラメータ)U」が条件
様々な条件を指定した型パラメータを定義するサンプルプログラムは、以下の通りです。
func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool where T.Element: Equatable, T.Element == U.Element {
for lhsItem in lhs {
for rhsItem in rhs {
if lhsItem == rhsItem {
return true
}
}
}
return false
}
print(anyCommonElements([1, 2, 3], [3]))
// 実行結果: true
func anyCommonElements<T: Sequence, U: Sequence> ...の部分では、型パラメータT, Uは、それぞれプロトコルSequenceに準拠していなければならないという1つ目の条件を指定しています。
where T.Element: Equatable, T.Element == U.Elementの部分では、2つ目の条件として、「型パラメータTの要素はプロトコルEquatableに準拠している」かつ「型パラメータTの要素のデータ型は、型パラメータUの要素のデータ型と一致している」という**論理積(logical conjunction)**の条件を指定しています。
プロトコルEquatableは、オブジェクトを比較するプロトコルであり、標準ライブラリでInt型やString型など多くのデータ型が適合するよう定義されています。
補足ですが、関数anyCommonElementsは2つのSequence型の値を比較し、共通する要素があればtrueを、共通する要素がなければfalseを返しています。
以上で`A Swift Tour`の説明は終わりです。
かなり長くなりましたが、A Swift Tourで紹介されているサンプルプログラムを補足しながら説明していきました。
独学のため、誤りもあると思いますが、コメントにて教えていただけると幸いです。
ここまでお読みいただき、ありがとうございました。
