Swift: Swiftのextensionとアクセス制御:extensionとアクセスレベルの完全ガイド

extensionを使うと既存のクラス、構造体、列挙型、プロトコルに新しい機能を追加できます。アクセス制御はコードのインターフェースを明確にし、カプセル化を強化します。このレッスンでは両者を組み合わせて、よりエレガントなSwiftコードを書きます。

1. 学習目標


2. バックエンド開発者の実話

(1) 課題:FoundationのStringに機能を追加できない

日付フォーマットの作業中、Bobは Date に人間が読みやすい「どれくらい前か」形式に変換するメソッドがないことに気づきました。彼はユーティリティ関数を書きました:

SWIFT
func timeAgo(from date: Date) -> String {
    let seconds = Date().timeIntervalSince(date)
    if seconds < 60 { return "\(Int(seconds))s ago" }
    if seconds < 3600 { return "\(Int(seconds / 60))m ago" }
    return "\(Int(seconds / 3600))h ago"
}
// Must pass the date as argument every time
print(timeAgo(from: someDate))

呼び出すたびに引数を渡し、グローバル関数を呼び出す必要があり — コードが自然に感じられません。 Bobは Date が自身でこの形式を返せるようにしたかったのです — まるで��ルトインのメソッドのように。

(2) extensionによる解決策

SWIFT
extension Date {
    func timeAgo() -> String {
        let seconds = Date().timeIntervalSince(self)
        if seconds < 60 { return "\(Int(seconds))s ago" }
        if seconds < 3600 { return "\(Int(seconds / 60))m ago" }
        return "\(Int(seconds / 3600))h ago"
    }
}
// Looks like a built-in Date method
print(someDate.timeAgo())

extensionはDateに新しいメソッドを与えます — 継承もソースの変更も不要です。

(3) 利点:自然な構文 + 高い凝集性

観点 グローバル関数 Extension
呼び出しスタイル timeAgo(from: date) date.timeAgo()
コード構成 散在 型と密接にグループ化
発見性 ユーティリティモジュールを手動インポート Xcodeのオートコンプリート
保守性 グローバル名前空間の汚染 型固有の名前空間

3. Extension

(1) Extension構文

Extensionは既存の型に以下を追加できます:

追加可能 制限
コンピューテッドプロパティ var area: Double { ... } ストアドプロパティは追加不可
インスタンス/型メソッド func reversed() -> String 許可
イニシャライザ init?(json: [String: Any]) deinitは追加不可
サブスクリプト subscript(idx: Int) -> T 許可
ネスト型 enum Status { ... } 許可
プロトコル準拠 extension Type: Protocol 許可
100%
graph TB
    A["extension SomeType {"] --> B["コンピューテッドプロパティ"]
    A --> C["メソッド"]
    A --> D["イニシャライザ"]
    A --> E["サブスクリプト"]
    A --> F["ネスト型"]
    A --> G["プロトコル準拠"]
    A --> H["}"]

▶ サンプル: Stringへのextensionの追加

SWIFT
// ============================================
// Adding practical extensions to String
// ============================================
extension String {
    // Computed property -- check if this is a valid email
    var isValidEmail: Bool {
        return contains("@") && contains(".")
    }
    // Method -- truncate to first N characters
    func prefix(_ maxLength: Int) -> String {
        guard count > maxLength else { return self }
        return String(self.prefix(maxLength)) + "..."
    }
    // Method -- reverse the string
    func reversed() -> String {
        return String(self.reversed())
    }
}
let email = "alice@example.com"
print("\(email) is valid: \(email.isValidEmail)")
let longText = "This is a very long string that needs truncation"
print(longText.prefix(20))
print("Hello".reversed())

出力:

TEXT 📖 参照専用
alice@example.com is valid: true
This is a very lon...
olleH

注意: Extensionは既存のメソッドをオーバーライドできません。extensionが既存のメソッドと同じ名前のメソッドを定義すると、コンパイラがエラーを報告します。

(2) Extension経由のプロトコル準拠

プロトコル準拠を型定義から分離し、extensionで実装します:

SWIFT
// ============================================
// Conforming to protocols via extensions
// ============================================
// 1. Define the base type
struct User {
    let id: Int
    let name: String
    let email: String
}
// 2. Conform to Hashable via extension (for Set and Dictionary keys)
extension User: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}
// 3. Conform to Equatable via extension
extension User: Equatable {
    static func == (lhs: User, rhs: User) -> Bool {
        return lhs.id == rhs.id
    }
}
// 4. Usage
let users: Set<User> = [
    User(id: 1, name: "Alice", email: "alice@example.com"),
    User(id: 1, name: "Alice", email: "alice@example.com"),  // Duplicate -- deduplicated
    User(id: 2, name: "Bob", email: "bob@example.com")
]
print("Users count: \(users.count)")  // 2

出力:

TEXT 📖 参照専用
Users count: 2

ヒント: プロトコル準拠をextensionに配置するのはコード構成のためです — 型定義にはコアデータのみを含め、動作はextensionで追加します。これはSwift標準ライブラリで一般的なパターンです。


4. アクセス制御

(1) 5つのアクセスレベル

Swiftは最もオープンなものから最もクローズドなものまで、5つのアクセス制御レベルを提供します:

100%
graph TB
    A[アクセス制御ピラミッド] --> B["open - 最もオープン、誰でもアクセス/継承/オーバーライド可能"]
    A --> C["public - 誰でもアクセス可能だが、継承/オーバーライド不可"]
    A --> D["internal - 現在のモジュール内(デフォルトレベル)"]
    A --> E["fileprivate - 現在のファイル内"]
    A --> F["private - 囲んでいる宣言の波括弧内"]
レベル キーワード アクセス範囲 使用可能な対象
最もオープン open 任意のモジ��ール、継承とオーバーライド可能 クラス、クラスメソッド
public 任意のモジュール、ただし継承/オーバーライド不可 クラス、メソッド、プロパティ
デフォルト internal 現在のモジュール(アプリまたはフレームワーク) すべての型
fileprivate 現在のファイル 型、メソッド、プロパティ
最もクローズド private 現在の宣言スコープ(波括弧内) プロパティ、メソッド

▶ サンプル: アクセス制御の実践

SWIFT
// ============================================
// Using the five access levels
// ============================================
// Public class available to all modules
public class BankAccount {
    // Publicly readable, privately writable
    public private(set) var balance: Double
    // Private field -- only accessible within this class
    private var accountNumber: String
    private var transactionHistory: [String] = []
    // Accessible within this file -- for logging
    fileprivate var lastTransactionDate: Date?
    // Accessible within this module -- for internal auditing
    internal var branchCode: String
    public init(accountNumber: String, balance: Double, branchCode: String) {
        self.accountNumber = accountNumber
        self.balance = balance
        self.branchCode = branchCode
    }
    public func deposit(amount: Double) {
        balance += amount
        transactionHistory.append("Deposit: \(amount)")
        lastTransactionDate = Date()
    }
    // Private helper method
    private func log(_ message: String) {
        print("[Private] \(message)")
    }
}
let account = BankAccount(accountNumber: "123-456", balance: 1000, branchCode: "BR-001")
print("Balance: \(account.balance)")    // public getter is readable
// account.balance = 2000               // Compile error! public private(set) prevents external writes
// print(account.accountNumber)         // Compile error! private property
// print(account.lastTransactionDate)   // Compile error! fileprivate, inaccessible from another file
print("Branch: \(account.branchCode)")  // internal, accessible within the same module

出力:

TEXT 📖 参照専用
Balance: 1000.0
Branch: BR-001

(2) アクセス制御の原則

原則 説明
最小権限 デフォルトは private、必要な場合のみ緩和
安定したインターフェース 公開APIは一度公開されると契約となる — 慎重に変更
モジュール境界 フレームワークの public はモジュール境界を定義し、internal は内部実装
getter/setter public private(set) パターン:パブリックに読み取り可能、プライベートに書き込み可能

5. Extensionとアクセス制御

(1) 同一ファイル内のprivate共有

private メンバーは同じファイル内のextensionからアクセス可能です:

SWIFT
// ============================================
// Visibility of private in extensions
// ============================================
struct UserAccount {
    private var token: String = "secret-token"
    let name: String
    func getToken() -> String {
        return token
    }
}
// Extensions in the same file can access private members
extension UserAccount {
    func resetToken() {
        token = "new-token"  // Accessible! Same file
    }
}
var account = UserAccount(name: "Alice")
print(account.getToken())
account.resetToken()
print(account.getToken())

出力:

TEXT 📖 参照専用
secret-token
new-token

注意: Swift 4以降、同じファイル内のextensionは private メンバーにアクセスできます。ファイルを越えた共有が必要な場合は fileprivate を使用します。

(2) Extensionによる最適な構成パターン

SWIFT
// Core data definition
struct Product {
    let id: Int
    let name: String
    let price: Double
}
// MARK: - Equatable Conformance
extension Product: Equatable {
    static func == (lhs: Product, rhs: Product) -> Bool {
        return lhs.id == rhs.id
    }
}
// MARK: - Codable Conformance
extension Product: Codable { }
// MARK: - Utility Methods
extension Product {
    func formattedPrice() -> String {
        return "$\(String(format: "%.2f", price))"
    }
    var priceWithTax: Double {
        return price * 1.08
    }
}

▶ サンプル: Extensionによるネスト型と便利イニシャライザの追加

Extensionはメソッドやコンピューテッドプロパティだけでなく、ネスト型やイニシャライザも追加できます:

SWIFT
// ============================================
// Adding nested types and convenience initializers via extensions
// ============================================
// Base type
struct Book {
    let title: String
    let author: String
    let year: Int
}
// Extension: add nested types and convenience initializers
extension Book {
    // Nested enum -- genres
    enum Genre: String {
        case fiction = "Fiction"
        case nonfiction = "Non-fiction"
        case science = "Science"
        case history = "History"
    }
    // Nested struct -- reading stats
    struct ReadingStats {
        var pagesRead: Int
        var totalPages: Int
        var progress: Double {
            totalPages > 0 ? Double(pagesRead) / Double(totalPages) * 100 : 0
        }
    }
    // Convenience initializer -- create from a JSON dictionary
    init?(from dict: [String: Any]) {
        guard let title = dict["title"] as? String,
              let author = dict["author"] as? String,
              let year = dict["year"] as? Int else {
            return nil
        }
        self.init(title: title, author: author, year: year)
    }
}
// Usage
let book1 = Book(title: "Swift Programming", author: "Apple", year: 2024)
let genre = Book.Genre.science
print("\(book1.title) - Genre: \(genre.rawValue)")
let stats = Book.ReadingStats(pagesRead: 120, totalPages: 350)
print("Reading progress: \(String(format: "%.1f", stats.progress))%")
// Create from dictionary
let dict = ["title": "Advanced Swift", "author": "Alice", "year": 2025] as [String: Any]
if let book2 = Book(from: dict) {
    print("Created from dictionary: \(book2.title) (\(book2.author), \(book2.year))")
}

出力:

TEXT 📖 参照専用
Swift Programming - Genre: Science
Reading progress: 34.3%
Created from dictionary: Advanced Swift (Alice, 2025)

6. 完全な例:ユーザー管理システム(Extension + アクセス制御)

SWIFT
// ============================================
// Full example: User management system
// Features: comprehensive use of extension + access control
// ============================================
import Foundation
// 1. Core user type
public class User {
    public let id: Int
    public let name: String
    public let email: String
    // Publicly readable, internally writable
    public private(set) var score: Int = 0
    public private(set) var isActive: Bool = true
    // Accessible within the module
    internal var lastLogin: Date?
    // Private
    private var loginHistory: [Date] = []
    public init(id: Int, name: String, email: String) {
        self.id = id
        self.name = name
        self.email = email
    }
    public func login() {
        let now = Date()
        lastLogin = now
        loginHistory.append(now)
        isActive = true
    }
    public func addScore(_ points: Int) {
        // Internal validation
        guard points > 0 else { return }
        score += points
    }
}
// MARK: - Description Extension
extension User {
    public func description() -> String {
        return "User(\(id): \(name), score: \(score), active: \(isActive))"
    }
    // Determine based on last login time
    public var isOnline: Bool {
        guard let last = lastLogin else { return false }
        return Date().timeIntervalSince(last) < 300
    }
}
// MARK: - Activity Log Extension
extension User {
    // fileprivate -- accessible only within this file
    fileprivate func generateActivityReport() -> String {
        return """
        User Report - \(name)
        =================
        Score: \(score)
        Active: \(isActive)
        Last Login: \(lastLogin?.description ?? "Never")
        """
    }
    public func printReport() {
        print(generateActivityReport())
    }
}
// 3. Usage example
let user = User(id: 1, name: "Alice", email: "alice@example.com")
user.login()
user.addScore(50)
user.addScore(30)
print(user.description())
print("Online: \(user.isOnline)")
user.printReport()
// user.score = 100        // Compile error -- public private(set)
// user.loginHistory        // Compile error -- private

出力:

TEXT 📖 参照専用
User(1: Alice, score: 80, active: true)
Online: true
User Report - Alice
=================
Score: 80
Active: true
Last Login: 2026-07-30 12:00:00 +0000

❓ よくある質問

Q extensionと継承の違いは何ですか?
A Extensionは(StringやDateなどのシステム型を含む)既存の型に新しい型を作らずに機能を追加します。継承は自身のクラスにのみ機能し、動作の変更に使われます。Extensionはストアドプロパティを追加できませんが、継承は可能です。
Q extensionにストアドプロパティを追加できますか?
A いいえ。Extensionはコンピューテッドプロパティ、メソッド、イニシャライザ、サブスクリプト、ネスト型のみ追加できます。ストアドプロパティは元の型定義に含める必要があります。これはSwiftの言語設計上の決定で、既存型のメモリレイアウトを壊さないためです。
Q デフォルトのアクセスレベルは何ですか?
A internal です。アクセス制御修飾子を何も書かない場合、デフォルトは internal で、現在のモジュール内でアクセス可��です。アプリ開発(単一モジュール)では、internalpublic と同等です。フレームワーク��発では、外部モジュールに公開するには明示的に public または open を書く必要があります。
Q openとpublicの違いは何ですか?
A public は外部モジュールからのアクセスを許可しますが、継承やオーバーライドは許可しません。open は外部モジュールがクラスを継承したりメソッドをオーバーライドしたりすることを許可します。クラスとクラスメソッドのみ open を使用できます。安全原則:必要な場合を除き open は使用しないでください。
Q コードをextensionに分割すべきタイミングは?
A 型定義が大きくなった場合(200行超)、機能ごとに複数のextensionにグループ化します(それぞれにMARKコメントを付けます)。一般的なパターン:コア定義、プロトコル準拠、ユーティリティメソッド、便利イニシャライザ。1つのextensionにつき1つの関心事。

📖 まとめ


📝 練習問題

  1. 基本: Intsquared コンピューテッドプロパティと isEven コンピューテッドプロパティを追加する extension を書いてください。4.squared(16を出力)と 3.isEven(falseを出力)をテストしてください。
  2. 中級: DateformattedDate メソッド("yyyy-MM-dd"フォーマットの文字列を返す)と isToday コンピューテッドプロパティを追加する extension を書いてください。まだ準拠していなければ Comparable に準拠させるために extension を使用してください。
  3. 発展: アクセス制御を使って機密データを管理する SecureStore クラスを作成してください。要件:publicsave(key:value:)read(key:) メソッド、internal なキャッシュクリアメソッド、private な暗号化/復号化ロジック、fileprivate なログ記録メソッド。extensionを使ってプロトコル準拠とユーティリティメソッドを異なるMARKブロックにグループ化してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%