Swift: Swiftのextensionとアクセス制御:extensionとアクセスレベルの完全ガイド
extensionを使うと既存のクラス、構造体、列挙型、プロトコルに新しい機能を追加できます。アクセス制御はコードのインターフェースを明確にし、カプセル化を強化します。このレッスンでは両者を組み合わせて、よりエレガントなSwiftコードを書きます。
1. 学習目標
extensionを使って既存の型にメソッドとプロパティを追加する- extension経由でプロトコルに準拠する
- コンピューテッドプロパティのextension
- Swiftの5つのアクセス制御レベル
- アクセス制御のベストプラクティス
2. バックエンド開発者の実話
(1) 課題:FoundationのStringに機能を追加できない
日付フォーマットの作業中、Bobは Date に人間が読みやすい「どれくらい前か」形式に変換するメソッドがないことに気づきました。彼はユーティリティ関数を書きました:
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による解決策
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 |
許可 |
graph TB
A["extension SomeType {"] --> B["コンピューテッドプロパティ"]
A --> C["メソッド"]
A --> D["イニシャライザ"]
A --> E["サブスクリプト"]
A --> F["ネスト型"]
A --> G["プロトコル準拠"]
A --> H["}"]
▶ サンプル: Stringへのextensionの追加
// ============================================
// 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で実装します:
// ============================================
// 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つのアクセス制御レベルを提供します:
graph TB
A[アクセス制御ピラミッド] --> B["open - 最もオープン、誰でもアクセス/継承/オーバーライド可能"]
A --> C["public - 誰でもアクセス可能だが、継承/オーバーライド不可"]
A --> D["internal - 現在のモジュール内(デフォルトレベル)"]
A --> E["fileprivate - 現在のファイル内"]
A --> F["private - 囲んでいる宣言の波括弧内"]
| レベル | キーワード | アクセス範囲 | 使用可能な対象 |
|---|---|---|---|
| 最もオープン | open |
任意のモジ��ール、継承とオーバーライド可能 | クラス、クラスメソッド |
public |
任意のモジュール、ただし継承/オーバーライド不可 | クラス、メソッド、プロパティ | |
| デフォルト | internal |
現在のモジュール(アプリまたはフレームワーク) | すべての型 |
fileprivate |
現在のファイル | 型、メソッド、プロパティ | |
| 最もクローズド | private |
現在の宣言スコープ(波括弧内) | プロパティ、メソッド |
▶ サンプル: アクセス制御の実践
// ============================================
// 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からアクセス可能です:
// ============================================
// 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による最適な構成パターン
// 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はメソッドやコンピューテッドプロパティだけでなく、ネスト型やイニシャライザも追加できます:
// ============================================
// 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 + アクセス制御)
// ============================================
// 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
❓ よくある質問
internal です。アクセス制御修飾子を何も書かない場合、デフォルトは internal で、現在のモジュール内でアクセス可��です。アプリ開発(単一モジュール)では、internal は public と同等です。フレームワーク��発では、外部モジュールに公開するには明示的に public または open を書く必要があります。public は外部モジュールからのアクセスを許可しますが、継承やオーバーライドは許可しません。open は外部モジュールがクラスを継承したりメソッドをオーバーライドしたりすることを許可します。クラスとクラスメソッドのみ open を使用できます。安全原則:必要な場合を除き open は使用しないでください。📖 まとめ
extensionは既存の型にコンピューテッドプロパティ、メソッド、イニシャライザ、サブスクリプト、ネスト型を追加できる- Extensionはストアドプロパティを追加できず、既存のメソッドをオーバーライドできない
- extension経由でプロトコルに準拠するのはSwiftの標準的なコード構成パターン
- 5つのアクセスレベル:open > public > internal > fileprivate > private
public private(set)パターンは「パブリックに読み取��可能、プライベートに書き込み可能」を実現- 同じファイル内のextensionは
privateメンバーにアクセス可能
📝 練習問題
- 基本:
IntにsquaredコンピューテッドプロパティとisEvenコンピューテッドプロパティを追加するextensionを書いてください。4.squared(16を出力)と3.isEven(falseを出力)をテストしてください。 - 中級:
DateにformattedDateメソッド("yyyy-MM-dd"フォーマットの文字列を返す)とisTodayコンピューテッドプロパティを追加するextensionを書いてください。まだ準拠していなければComparableに準拠させるためにextensionを使用してください。 - 発展: アクセス制御を使って機密データを管理する
SecureStoreクラスを作成してください。要件:publicなsave(key:value:)とread(key:)メソッド、internalなキャッシュクリアメソッド、privateな暗号化/復号化ロジック、fileprivateなログ記録メソッド。extensionを使ってプロトコル準拠とユーティリティメソッドを異なるMARKブロックにグループ化してください。