Swift言語の文法解説
基本文法から、Optional型によるnil安全性、ARCによるメモリ管理、プロトコル指向プログラミング、データ構造・アルゴリズムの実装まで、基本情報技術者試験の合格を目指せるレベルで解説します。
文法解説
1. コメント
1行コメントは//、複数行コメントは/* */で記述します。///はドキュメンテーションコメントで、Xcodeの補完に説明を表示できます。
// これは1行コメントです
/*
これは複数行コメントです。
複数行にわたる説明を書けます。
*/
/// この関数は2つの整数の和を返します。
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
2. 変数と定数(var / let)
再代入可能な変数はvar、再代入不可の定数はletで宣言します。Swiftでは変更の必要がない限りletの使用が推奨され、意図しない変更によるバグを防ぎます。
var age = 20 // 変数(再代入可能)
let name = "太郎" // 定数(再代入不可)
age += 1
// name = "次郎" // コンパイルエラー:letは再代入できない
print("\(name)さんは\(age)歳です")
3. 静的型付けと型推論・基本型
Swiftは静的型付け言語で、変数の型はコンパイル時に決定されます。型注釈を省略しても初期値から型が自動的に推論されます(型推論)。基本型にはInt、Double、Bool、String、Character、複数の値をまとめるTupleなどがあります。
let count: Int = 10 // 型注釈あり
let price = 980.0 // 型推論によりDoubleになる
let isActive = true // Bool
let city = "東京" // String
let initial: Character = "T"
// タプル:複数の値を1つにまとめる
let point: (x: Int, y: Int) = (3, 4)
print(point.x, point.y)
// count = "文字列" // コンパイルエラー:型が違う
4. 制御構文(if / switch / for / while)
if / elseによる分岐、switchによる多分岐(Swiftではfallthroughしないのが標準で、範囲やwhere句も使えます)、for-inやwhileによる繰り返しがあります。
let score = 75
if score >= 80 {
print("優")
} else if score >= 60 {
print("良")
} else {
print("再履修")
}
switch score {
case 80...100:
print("優")
case 60..<80:
print("良")
case let s where s < 0:
print("不正な値")
default:
print("再履修")
}
for i in 0..<3 {
print("繰り返し\(i)回目")
}
var n = 3
while n > 0 {
print(n)
n -= 1
}
5. 関数(デフォルト引数・可変長引数・クロージャ)
funcで関数を定義します。引数にはデフォルト値を設定でき、...で可変長引数を受け取れます。無名関数である「クロージャ」は{ 引数 in 処理 }の形式で記述し、関数の引数や戻り値として扱えます。
// デフォルト引数
func greet(name: String, honorific: String = "さん") -> String {
return "\(name)\(honorific)、こんにちは"
}
// 可変長引数
func sum(_ numbers: Int...) -> Int {
return numbers.reduce(0, +)
}
// クロージャ
let square: (Int) -> Int = { x in x * x }
print(greet(name: "花子")) // 花子さん、こんにちは
print(sum(1, 2, 3, 4)) // 10
print(square(5)) // 25
// 末尾クロージャ構文
let doubled = [1, 2, 3].map { $0 * 2 }
print(doubled) // [2, 4, 6]
6. Optional型とnil安全性
Swift最大の特徴の一つが「Optional型」です。値が存在しない可能性がある場合、型の後ろに?を付けて宣言します。強制的に取り出すには!を使いますが、値がnilだとクラッシュするため注意が必要です。安全に取り出すにはif letやguard let、値がnilの場合の代替値を指定する??(nil合体演算子)を使います。
var nickname: String? = nil // Optional型(値がないかもしれない)
nickname = "たろちゃん"
// if let:nilでなければアンラップして使う
if let nick = nickname {
print("ニックネーム: \(nick)")
} else {
print("ニックネームはありません")
}
func checkNickname(_ nickname: String?) {
// guard let:条件を満たさなければ早期return
guard let nick = nickname else {
print("ニックネームが未設定です")
return
}
print("ニックネーム: \(nick)")
}
// nil合体演算子:nilの場合の代替値を指定
let displayName = nickname ?? "名無しさん"
print(displayName)
// オプショナルチェイニング:途中がnilでも安全にアクセス
let length = nickname?.count
print(length ?? 0)
7. structとclassの違い(値型と参照型)
structは値型で、代入や関数への引き渡し時に値がコピーされます。classは参照型で、インスタンスが複数の変数から共有されます。プロパティを変更するメソッドにはmutatingを付ける必要があります(structのみ)。
struct Point {
var x: Int
var y: Int
mutating func moveX(by dx: Int) {
x += dx
}
}
class Counter {
var count = 0
func increment() {
count += 1
}
}
var p1 = Point(x: 1, y: 2)
var p2 = p1 // 値がコピーされる(別々のインスタンス)
p2.x = 100
print(p1.x, p2.x) // 1 100 -- p1には影響しない
let c1 = Counter()
let c2 = c1 // 参照が共有される(同じインスタンス)
c2.increment()
print(c1.count, c2.count) // 1 1 -- 同じインスタンスなので両方変わる
8. プロトコル指向プログラミング
protocolは他言語のインターフェースに相当し、準拠する型が実装すべきプロパティやメソッドを定義します。classの継承は1つしかできませんが、プロトコルには複数同時に準拠できます。extensionを使うとプロトコルにデフォルト実装を与えたり、既存の型に後から機能を追加したりできます。Swiftでは継承よりもプロトコルと合成(コンポジション)を重視する設計を「プロトコル指向プログラミング」と呼びます。
protocol Speaker {
func speak() -> String
}
// extensionでデフォルト実装を与える
extension Speaker {
func introduce() -> String {
return "私は「\(speak())」と鳴きます"
}
}
struct Dog: Speaker {
func speak() -> String { "ワン!" }
}
struct Cat: Speaker {
func speak() -> String { "ニャー" }
}
let animals: [Speaker] = [Dog(), Cat()]
for animal in animals {
print(animal.introduce()) // extensionのデフォルト実装が使える
}
// 既存の型(Int)にextensionで機能を追加する
extension Int {
var isEven: Bool { self % 2 == 0 }
}
print(4.isEven) // true
9. enumとパターンマッチング
enumは複数の値のいずれか1つを表す型です。各caseに追加のデータを持たせる「associated values(関連値)」を使うと、状態ごとに異なる付随情報を型安全に表現できます。switchと組み合わせることで網羅的なパターンマッチングができます。
enum NetworkResult {
case success(data: String)
case failure(errorCode: Int)
case loading
}
func handle(_ result: NetworkResult) {
switch result {
case .success(let data):
print("成功: \(data)")
case .failure(let code):
print("失敗: エラーコード\(code)")
case .loading:
print("読み込み中...")
}
}
handle(.success(data: "ユーザー情報"))
handle(.failure(errorCode: 404))
10. エラーハンドリング(do / try / catch、Result型)
Errorプロトコルに準拠したenumでエラーの種類を表現し、throwsを付けた関数からエラーを送出します。呼び出し側はdo / try / catchで受け止めます。非同期処理などでは、成功と失敗を1つの値として表現するResult<Success, Failure>型もよく使われます。
enum CalculationError: Error {
case divisionByZero
}
func divide(_ a: Int, _ b: Int) throws -> Int {
guard b != 0 else {
throw CalculationError.divisionByZero
}
return a / b
}
do {
let result = try divide(10, 2)
print("結果: \(result)")
} catch CalculationError.divisionByZero {
print("ゼロ除算はできません")
} catch {
print("不明なエラー: \(error)")
}
// Result型で成功/失敗を表現する
func divideAsResult(_ a: Int, _ b: Int) -> Result<Int, CalculationError> {
guard b != 0 else { return .failure(.divisionByZero) }
return .success(a / b)
}
switch divideAsResult(10, 0) {
case .success(let value):
print("結果: \(value)")
case .failure(let error):
print("エラー: \(error)")
}
11. ARC(自動参照カウント)によるメモリ管理
SwiftのclassインスタンスはARC(Automatic Reference Counting)によってメモリ管理されます。インスタンスが強参照(strong reference)される回数をコンパイラが自動的に計算し、参照カウントが0になった時点でメモリを解放します。2つのクラスが互いを強参照すると参照カウントが0にならず「循環参照(メモリリーク)」が発生するため、片方をweak(弱参照。nilになりうるためOptional)またはunowned(非所有参照。常に値があると保証できる場合)にして回避します。
class Person {
let name: String
var pet: Pet?
init(name: String) { self.name = name }
deinit { print("\(name)が解放されました") }
}
class Pet {
let name: String
weak var owner: Person? // weakにして循環参照を防ぐ
init(name: String) { self.name = name }
deinit { print("\(name)が解放されました") }
}
var person: Person? = Person(name: "太郎")
var pet: Pet? = Pet(name: "ポチ")
person?.pet = pet
pet?.owner = person // ownerがweakなので循環参照にならない
person = nil // Personの参照カウントが0になり解放される
pet = nil // Petも正しく解放される
12. コレクション(Array, Dictionary, Set)
Arrayは順序付きのコレクションで、インデックスでのアクセスはO(1)ですが、特定の値を探す場合は先頭から順に調べるためO(n)です。Dictionary(キーと値のペア)とSet(重複のない集合)はハッシュテーブルで実装されており、追加・検索・削除が平均O(1)で行えます。
var fruits: [String] = ["りんご", "バナナ", "みかん"]
fruits.append("ぶどう")
print(fruits.contains("バナナ")) // O(n)の線形探索
var prices: [String: Int] = ["りんご": 150, "バナナ": 100]
prices["みかん"] = 80
print(prices["りんご"] ?? 0) // 平均O(1)で検索
var uniqueTags: Set<String> = ["swift", "ios"]
uniqueTags.insert("swift") // 重複は無視される
print(uniqueTags.contains("ios")) // 平均O(1)で検索
13. データ構造の実装:スタックとキュー
ジェネリクス(<T>)を使うと、任意の型に対応するデータ構造を実装できます。スタックは後入れ先出し(LIFO)、キューは先入れ先出し(FIFO)のデータ構造で、push/pop・enqueue/dequeueはいずれもO(1)(配列先頭からの削除を除く)で実装できます。
struct Stack<T> {
private var items: [T] = []
var isEmpty: Bool { items.isEmpty }
mutating func push(_ item: T) { // O(1)
items.append(item)
}
mutating func pop() -> T? { // O(1)
return items.popLast()
}
}
struct Queue<T> {
private var items: [T] = []
mutating func enqueue(_ item: T) { // O(1)
items.append(item)
}
mutating func dequeue() -> T? { // O(n):先頭要素の削除で後続を詰め直すため
guard !items.isEmpty else { return nil }
return items.removeFirst()
}
}
var stack = Stack<Int>()
stack.push(1)
stack.push(2)
print(stack.pop() ?? 0) // 2(後入れ先出し)
var queue = Queue<String>()
queue.enqueue("A")
queue.enqueue("B")
print(queue.dequeue() ?? "") // "A"(先入れ先出し)
14. アルゴリズムと計算量(Big-O):探索と整列
線形探索は先頭から順に調べるため計算量はO(n)です。データが昇順にソート済みであれば、中央の値と比較して探索範囲を半分に絞り込む二分探索が使え、計算量はO(log n)に改善されます。整列アルゴリズムであるクイックソートは平均O(n log n)ですが、最悪の場合(既にソート済みの配列を単純なピボット選択で処理する場合など)はO(n²)になります。
// 線形探索:O(n)
func linearSearch(_ array: [Int], target: Int) -> Int? {
for (index, value) in array.enumerated() {
if value == target { return index }
}
return nil
}
// 二分探索:O(log n)(配列がソート済みであることが条件)
func binarySearch(_ array: [Int], target: Int) -> Int? {
var low = 0
var high = array.count - 1
while low <= high {
let mid = (low + high) / 2
if array[mid] == target {
return mid
} else if array[mid] < target {
low = mid + 1
} else {
high = mid - 1
}
}
return nil
}
// クイックソート:平均O(n log n)、最悪O(n^2)
func quickSort(_ array: [Int]) -> [Int] {
guard array.count > 1 else { return array }
let pivot = array[array.count / 2]
let less = array.filter { $0 < pivot }
let equal = array.filter { $0 == pivot }
let greater = array.filter { $0 > pivot }
return quickSort(less) + equal + quickSort(greater)
}
let sorted = [1, 3, 5, 7, 9, 11, 13]
print(binarySearch(sorted, target: 9) ?? -1) // 4
print(quickSort([5, 2, 8, 1, 9, 3])) // [1, 2, 3, 5, 8, 9]
15. モジュール・パッケージ管理とアクセス制御
importで標準ライブラリ(Foundation)やUIKit等のフレームワークを取り込みます。プロジェクト規模が大きくなると、依存ライブラリの管理にはSwift Package Manager(SwiftPM)を使い、Package.swiftで依存関係を宣言します。またpublic / internal(デフォルト) / fileprivate / privateによるアクセス制御でモジュール内外への公開範囲を制限できます。
import Foundation
struct BankAccount {
private var balance: Int = 0 // 同じ型の外からは変更不可
mutating func deposit(_ amount: Int) {
balance += amount
}
func currentBalance() -> Int {
return balance
}
}
var account = BankAccount()
account.deposit(1000)
print(account.currentBalance()) // 1000
// account.balance = 999999 // コンパイルエラー:privateなのでアクセス不可
処理速度改善のポイント
Swiftはコンパイル型言語として高速ですが、ARC(自動参照カウント)や値型/参照型の使い分け、コレクション操作の書き方を意識すると更なる高速化が可能です。
- structを積極的に使う:値型のstructは参照カウント(ARC)の対象外のため、classより軽量に扱えるケースが多いです。
- 強参照の循環(サイクル)を避ける:クラス同士が強参照し合うとメモリが解放されず、weak/unownedを使って回避します。循環参照が残るとARCのオーバーヘッドだけでなくメモリリークにもつながります。
- 配列などのCollectionのCopy-on-Writeを理解する:コピーは値が変更されるまで実際には発生しませんが、複数箇所で頻繁に変更するとコピーコストが発生する点に注意します。
- lazyプロパティ・lazy評価を活用する:
lazy varやlazyシーケンスは、実際に必要になるまで計算を遅延させ、無駄な処理を避けられます。 - 適切なコレクション型を選ぶ:検索が多い場合はArrayではなくSetやDictionary(平均O(1)検索)を使いましょう。
- 頻繁な文字列連結は避ける:ループ内で
+=を繰り返すより、あらかじめ配列に格納してjoined()でまとめる方が効率的です。 - finalクラスを活用する:継承を想定しないクラスに
finalを付けるとコンパイラが仮想テーブル呼び出しを省略でき最適化しやすくなります。 - Instrumentsでプロファイリングする:Xcode付属のInstrumentsを使い、推測ではなく計測に基づいてボトルネックを特定しましょう。