はじめに
(参考①) [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
で紹介されているサンプルプログラムを補足しながら説明していきました。
独学のため、誤りもあると思いますが、コメントにて教えていただけると幸いです。
ここまでお読みいただき、ありがとうございました。