LoginSignup
2
4

More than 3 years have passed since last update.

【Swift 5.3】A Swift Tourから学ぶSwift【Xcode 12.3】

Last updated at Posted at 2020-10-25

はじめに

(参考①) A Swift Tour
↑本記事の主となるSwift公式サイトの記事。
(参考①') A Swift Tour
↑Swift.orgやApple Developer Documentationの日本語訳。
(参考②) Swift実践入門 〜 今からはじめるiOSアプリ開発! 基本文法を押さえて、簡単な電卓を作ってみよう
↑Swift 5.3に拘らない人向け。
(参考③) 【Swift入門】オプショナル型を理解しよう
↑Optional型について理解したい方向け。
(参考④) Swiftのオプショナル型の使いこなし
↑Optional型を扱う際の注意点を詳しく知りたい方向け。
(参考⑤) When to use guard let rather than if let
↑guard-let構文の使い所について知りたい方向け。(英語記事)
(参考⑥) Optional Chaining
↑オプショナルチェイニングを解説するSwift.orgの公式ページ。(英語記事)
(参考⑦) Swift Standard Library(GitHub)
↑GitHubで公開されているSwiftの標準ライブラリ。(英語)
(参考⑧) [JavaScript] 猿でもわかるクロージャ超入門 まとめ
↑クロージャについて理解を深めたい方向け。(JavaScriptで記述)
(参考⑨) イメージで理解するSwiftの高階関数(filter, map, reduce, compactMap, flatMap)
↑高階関数について理解を深めたい方向け。
(参考⑩) [Swift]flatMap・compactMapの挙動はソースコードを読んで理解しよう〜型情報と実装から難関メソッドを突破する〜
↑flatMap・compactMapの内部処理を理解したい方向け。
(参考⑪) mapとflatMapという便利メソッドを理解する
↑flatMapについて理解したい初心者の方向け。
(参考⑫) Swift Language - ジェネリックス
↑Swiftにおけるジェネリクスを理解したい方向け。
(参考⑬) [Swift] rethrowsを少し整理してみた
↑rethrowについて理解したい方向け。
(参考⑭) Swiftで値型と参照型の違いを理解する
↑値型と参照型の違いについて理解したい方向け。(@koher さんの記事は特におすすめ)
(参考⑮) Swiftにおけるclassとstructの使い分け
↑データ・メソッドをまとめる「クラス」と「構造体」の使い分け方を知りたい方向け。
(参考⑯) 【Swift】クラスの継承とプロトコルの準拠の使い分け
↑クラスとプロトコルの違いを詳しく知りたい方向け。

A Swift Tour

"Hello, world!"

どの言語を学ぶにしても、最初に必ず作成・実行するプログラムがあります。
"Hello, world!"と画面に表示させるだけのプログラムです。
Swiftでは、以下のように記述します。

print("Hello, world!")

このコードから分かることは、Swiftの簡潔性です。
Swiftと同じオブジェクト指向言語(object-oriented language)であるJavaでは、以下のように記述します。

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な変数(動的/静的),定数との違いにも触れる>

Hello,world!
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)として認識されます。

型の分類

型の一覧は以下の通りです。<編集メモ: 図を分かりやすくする>

スクリーンショット 2020-11-07 14.04.34.png

型については、いきなり気にする必要はありません。
最初のうちは、整数は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)されることはありません。
そのため、以下のようなコードを記述するとエラーが出力されます。

error
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.

複数行にわたる文字列の記述

複数行にわたる文字列を記述したい場合は、"""(ダブルクォテーション)で囲いましょう。
なお、可読性を向上させるためにSpaceTabを入力した場合は、出力結果にもSpaceTabが含まれている状態になります。""で囲っているため、SpaceTabも一つの文字列として扱われています。
可読性は落ちますが、出力結果に含みたくない場合は左づめで記述するようにしましょう。

複数行にわたる文字列の記述
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を許容しないため、以下のような実行時エラーが出力されてしまいます。

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では、以下のように紹介されています。

オプショナルバインディング

if-let構文
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でない場合の処理を実現しています。値がnilnilでないかで条件分岐を行うため、変数の宣言時に?を型の後ろに付け、Optional型であることを明示的に定義しましょう。if-let構文では、Optional<String>型変数であるoptionalNameを暗黙的にアンラップしています。

if-let構文の代わりにif文を使う場合は、以下のように記述します。

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の場合の処理節の中に、returnbreakthrowなど、関数から抜け出す(=スコープ(scope)を抜ける)処理が含まれていないと実行時エラーを吐くため、コーディングする際の安全性を向上させます。

guard-let構文
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つのクラスResidencePersonを宣言しています。

クラス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となっているのです。

上のプログラムでは、値がniljohn.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文でroomCountnilである場合の処理として、文字列"Unable to retrieve the number of rooms"を表示するようにしています。

オプショナルチェイニングは、値がnilになりうるメソッドプロパティを扱うときに有効な処理方法です。上記プログラムのように、オプショナルバインディングと組み合わせて使うこともあります。

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を返します。今回のようにnicknamenilでない、つまり条件式xtrueである場合は、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つの種類が存在します。

  1. 順次処理(sequence)
  2. 分岐処理(selection): if, switch
  3. 繰返し処理(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文)

拡張for文
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文を応用したサンプルプログラムは以下の通りです。

拡張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文

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のように型独自のメソッドを有しています。上記プログラムで用いられたhasSuffixSwift言語と密接に関連する重要クラスのクラスメソッド(class method)であるため静的インポート宣言(static import declaration)は不要でした。ただし、中にはimport文静的インポート宣言が必要なメソッドもあることに留意しましょう。

while文とrepeat文

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)であるstr1str2str3の計4つを定義しています。これらの定義によって、他の場面でgreetstrを関数・変数名として宣言すると、コード全体に紛らわしさが生まれ、可読性の低下に繋がるため使用しづらくなってしまいました。しかし、これでも関数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

クロージャを用いた上記のサンプルプログラムは、以下のように書き換えることもできます。

変数・関数greetingの書き換え
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))」を実現します。サンプルプログラムは以下の通りです。

高階関数map
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は、「条件を満たす構成要素の抽出」を実現します。サンプルプログラムは以下の通りです。

高階関数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は「各構成要素の集計」を実現します。サンプルプログラムは以下の通りです。

高階関数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でない値の抽出」を実現します。サンプルプログラムは以下の通りです。

高階関数compactMap
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と記述し、条件式xtrueであればyを評価した値、falseであればzを評価した値を生成(generate)します。

今回の場合、各構成要素$0が条件式$0 < 10を満たしている(=true)場合は3 * $0、満たしていない(=false)場合はnilを返しています。
compactMapでは、マッピングを実行する前に.filter{ $0 != nil }が自動的に実行され、nilの値は処理されません。

この性質を利用し、三項条件演算子?:を用いて、処理条件を満たさない構成要素にはnilを参照させ、nilでない(=処理条件を満たす)構成要素にのみ処理が実行されるように設定しましょう。

flatMap

flatMapは「多次元配列の次元削減(dimensionality reduction; 平坦化, flat)」を実現します。サンプルプログラムは以下の通りです。

高階関数flatMap
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は以下のように多重定義されています。

多重定義された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.numberOfSidesInt型データである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が持っているnumberOfSidesnameに加えて、新たにsideLengthを宣言しています。

init(sideLength: Double, name: String) {...}の部分では、子クラスSquareのイニシャライザを定義しています。このイニシャライザでは、クラスSquareが持つプロパティsideLengthnamenumberOfSidesの初期化を行っています。

プロパティ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辺の長さsideLength5.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 squarewith sides of length ...と出力されるように関数simpleDescriptionを定義していました。
子クラスEquilateralTriangleに属する図形は「正三角形」であるため、親クラスNamedShapesimpleDescription()関数を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のインスタンスは、内部に別クラスEquilateralTriangleSquareのインスタンスを有するコンポジション(composition; 合成)の構造を取っています。
クラスTriangleAndSquareとクラスTriangleSquareのようなクラス関係をhas-A関係と呼びます。

一方で、親クラスNamedShapeと、クラスNamedShapeを継承した子クラスEquilateralTriangleSquareのようなクラス関係をis-A関係と呼びます。

var triangle ...var square ...の部分では、クラスTriangleAndSquareのプロパティtrianglesquareプロパティオブザーバとしての役割を持たせています。

willSet {...}の部分では、インスタンスの生成時やプロパティtrianglesquareの参照先の変更時に、もう一方のプロパティ(triangleまたはsquare)に対して、自身の変更されたプロパティが保持するプロパティsideLengthを代入させ、互いのプロパティsideLengthが同じ値になるように設定しています。
参照先が変更された「後」の値を基準としてもう一方のプロパティの値を設定するため、セッタの定義ではwillSet {...}を用いています。

var sampleTriangleAndSquare: TriangleAndSquare ...の部分では、クラスTriangleAndSquareのインスタンスsampleTriangleAndSquareを生成しています。
このインスタンスの生成時に、クラスTriangleAndSquareのプロパティtrianglesquare実体として生成され、互いに持つプロパティsideLengthが同じ値をとるようにセッタが働き合います。

sampleTriangleAndSquare.square = ...の部分では、クラスTriangleAndSquareのインスタンスsampleTriangleAndSquareの参照先をプロパティsideLength: 50, name: "larger square"のクラスSquareのインスタンスSquare(...)に変更しています。

参照先の変更によって、クラスTriangleAndSquareのプロパティsquareが持つプロパティsideLengthの変更後の値50(.0)が変数newValueに代入されます。そして、クラスTriangleAndSquareのプロパティtrianglewillSet {...}の部分で、プロパティtriangle.sideLengthの値が元々格納されていた10.0から50.0に変更されます。

一方で、セッタとしての機能を持つプロパティtrianglesquarewillSet {...}の部分では、それぞれが持つプロパティ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)です。
※詳しくはこちら

参照型値型の挙動の違いを表すサンプルプログラムは、以下の通りです。

参照型と値型
// 参照型のクラス
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)はデフォルトでイミュータブルですが、varmutatinginoutのキーワードを用いることで、ミュータブル(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キーワードを付加しています。

インスタンス生成の部分では、クラス・構造体の元インスタンスをそれぞれreferenceTypevalueTypeとして生成しています。
インスタンスのコピーの部分では、クラス・構造体のコピーインスタンスをそれぞれreferenceTypeCopiedvalueTypeCopiedとして生成しています。

コピーインスタンスのスワップの部分では、コピーインスタンスに対してスワップメソッドを実行した結果、検証結果の出力結果から分かるように、参照型値型元データに差異が生じています。

参照型のインスタンスは、「参照先」を渡しています。そのため、参照先に格納されているデータ(=ストアドプロパディ)が変更されると、元インスタンス・コピーインスタンスの両方で値の変更が生じます。

一方で、値型のインスタンスは、「」を渡しています。そのため、コピーインスタンスのストアドプロパティが変更されても、元インスタンスのストアドプロパティは変更されません。

列挙型(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)の列挙型

列挙型RankSuitなどの値型の列挙型の列挙ケースは、実体型が一つに限定されていました。一方で、複数の異なるタプル(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が列挙ケースresultfailureのどちらの性質を有するかによって処理を分岐させています。

case x:のラベルxの部分では、今回のようにラベルxの持つ値が入力によって変化するため、case lety(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のインスタンスを生成していますが、出力結果を見ると、プロパティranksuitの中身にそれぞれmain.と付いているのが分かります。
これは、プロパティRank.threeSuit.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パターンに該当しないエラーパターンを捕捉しています。なお、catchcatch 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文を使用したサンプルプログラムは、以下の通りです。

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で紹介されているサンプルプログラムを補足しながら説明していきました。

独学のため、誤りもあると思いますが、コメントにて教えていただけると幸いです。

ここまでお読みいただき、ありがとうございました。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4