← Swift言語ページに戻る
🐦

Swift言語の文法解説

基本文法から、Optional型によるnil安全性、ARCによるメモリ管理、プロトコル指向プログラミング、データ構造・アルゴリズムの実装まで、基本情報技術者試験の合格を目指せるレベルで解説します。

文法解説

1. コメント

1行コメントは//、複数行コメントは/* */で記述します。///はドキュメンテーションコメントで、Xcodeの補完に説明を表示できます。

SAMPLE
// これは1行コメントです

/*
  これは複数行コメントです。
  複数行にわたる説明を書けます。
*/

/// この関数は2つの整数の和を返します。
func add(_ a: Int, _ b: Int) -> Int {
    return a + b
}

2. 変数と定数(var / let)

再代入可能な変数はvar、再代入不可の定数はletで宣言します。Swiftでは変更の必要がない限りletの使用が推奨され、意図しない変更によるバグを防ぎます。

SAMPLE
var age = 20        // 変数(再代入可能)
let name = "太郎"   // 定数(再代入不可)

age += 1
// name = "次郎"    // コンパイルエラー:letは再代入できない

print("\(name)さんは\(age)歳です")

3. 静的型付けと型推論・基本型

Swiftは静的型付け言語で、変数の型はコンパイル時に決定されます。型注釈を省略しても初期値から型が自動的に推論されます(型推論)。基本型にはIntDoubleBoolStringCharacter、複数の値をまとめるTupleなどがあります。

SAMPLE
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-inwhileによる繰り返しがあります。

SAMPLE
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 処理 }の形式で記述し、関数の引数や戻り値として扱えます。

SAMPLE
// デフォルト引数
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 letguard let、値がnilの場合の代替値を指定する??(nil合体演算子)を使います。

SAMPLE
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のみ)。

SAMPLE
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では継承よりもプロトコルと合成(コンポジション)を重視する設計を「プロトコル指向プログラミング」と呼びます。

SAMPLE
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と組み合わせることで網羅的なパターンマッチングができます。

SAMPLE
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>型もよく使われます。

SAMPLE
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(非所有参照。常に値があると保証できる場合)にして回避します。

SAMPLE
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)で行えます。

SAMPLE
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)(配列先頭からの削除を除く)で実装できます。

SAMPLE
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²)になります。

SAMPLE
// 線形探索: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によるアクセス制御でモジュール内外への公開範囲を制限できます。

SAMPLE
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(自動参照カウント)や値型/参照型の使い分け、コレクション操作の書き方を意識すると更なる高速化が可能です。

サンプルページで実践例を見る →