Swift: Swiftプロパティ詳解:ストアド、コンピューテッド、プロパティオブザーバ、遅延読み込み

プロパティはSwiftの型に値を関連付けます。基本的なストアドプロパティを超えて、Swiftはコンピューテッドプロパティ、プロパティオブ��ーバ、遅延読み込みなどの強力な機能を提供し、プロパティの読み書き動作を精密に制御できます。

1. 学習目標


2. モバイル開発者の実話

(1) 課題:表示のたびにユーザーアバターをネットワークからダウンロード

Bobはユーザープロフィールページを実装しましたが、表示のたびにネットワークからアバターを取得しています:

SWIFT
class UserProfile {
    var avatarUrl: String
    func displayAvatar() {
        // Downloads avatar from network every time
        downloadImage(from: avatarUrl) { image in
            // Display image
        }
    }
}

プロフィールを開くたびにアバターが再ダウンロードされます。ユーザーが繰り返し画面を行き来すると、同じネットワークリクエストが何度も実行され、帯域幅を浪費し、読み込みが遅くなります。

(2) 解決策:遅延読み込み + コンピューテッドプロパティ

SWIFT
class UserProfile {
    var avatarUrl: String
    lazy var cachedAvatar: UIImage? = {
        // Executes only once — downloads on first access
        return downloadImage(from: avatarUrl)
    }()
}

リアルタイム計算用のコンピューテッドプロパティと組み合わせることで、遅延読み込みは一度だけ実行され — 以降のアクセスはキャッシュされた値を直接返します。

(3) 利点:オンデマンド読み込み、自動キャッシング

観点 直接ダウンロード 遅延読み込み
読み込みタイミング 毎回アクセス 初回アクセス
重複リクエスト 毎回ダウンロード 一度だけダウンロード
メモリ使用量 表示されなくても読み込み済み 必要に応じて割り当て
起動速度 遅い(無関係なデータを事前読み込み) 速い(初期化を遅延)
コードの複雑さ 手動キャッシュ管理 Swiftが自動管理

3. ストアドプロパティとコンピューテッドプロパティ

(1) ストアドプロパティ

ストアドプロパティは、クラスまたは��造体のインスタンス��一部として格納される定数または変数です:

100%
graph TB
    A[プロパティタイプ] --> B[ストアドプロパティ]
    A --> C[コンピューテッドプロパティ]
    B --> D["let 定数ストアドプロパティ"]
    B --> E["var 変数ストアドプロパティ"]
    C --> F["getter — 読み取り時に計算"]
    C --> G["setter — 書き込み時に処理"]
特徴 ストアドプロパティ コンピューテッドプロパティ
メモリ インスタンスメモリを消費 インスタンスメモリを消費しない
読み書き 直接読み書き getter/setter経由
let/var varのみ
オブザーバ willSet/didSet対応 非対応
初期化 初期化またはinit内で代入必須 初期化不要

▶ サンプル: ストアドプロパティ

SWIFT
// ============================================
// Basic stored property usage
// ============================================
struct User {
    let id: Int           // Constant stored property
    var name: String       // Variable stored property
    var email: String
}
var user = User(id: 1, name: "Alice", email: "alice@example.com")
user.name = "Alice Smith"  // Variable property can be modified
// user.id = 2             // Compile error! Constant property cannot be modified
print("\(user.name) (\(user.email))")

出力:

TEXT 📖 参照専用
Alice Smith (alice@example.com)

(2) コンピューテッドプロパティ

コンピューテッドプロパティは値を格納せず、アクセスされるたびに計算して返します:

SWIFT
// ============================================
// Computed property — Celsius/Fahrenheit conversion
// ============================================
struct Temperature {
    var celsius: Double
    // Computed property — calculates fahrenheit from celsius
    var fahrenheit: Double {
        get {
            return celsius * 9 / 5 + 32
        }
        set(newFahrenheit) {
            celsius = (newFahrenheit - 32) * 5 / 9
        }
    }
}
var temp = Temperature(celsius: 25)
print("Celsius: \(temp.celsius)°C")
print("Fahrenheit: \(temp.fahrenheit)°F")
// Setting fahrenheit indirectly modifies celsius
temp.fahrenheit = 100
print("After setting to 100°F:")
print("Celsius: \(temp.celsius)°C")

出力:

TEXT 📖 参照専用
Celsius: 25°C
Fahrenheit: 77.0°F
After setting to 100°F:
Celsius: 37.77777777777778°C

ヒント: 読み取り専用のコンピューテッドプロパティは get と波括弧を省略できます:var doubled: Int { value * 2 }


4. プロパティオブザーバ

プロパティオブザーバはストアドプロパティの変更を監視し、値が変更される前後にトリガーされます。

オブザーバ タイミング パラメータ 一般的な用途
willSet 値が格納される前 newValue 更新前のバリデーション、ログ記録
didSet 値が格納された後 oldValue UI更新、データ同期

▶ サンプル: willSetとdidSet

SWIFT
// ============================================
// Property observers monitoring score changes
// ============================================
class Player {
    var name: String
    var score: Int = 0 {
        willSet {
            print("Score will change from \(score) to \(newValue)")
        }
        didSet {
            print("Score changed from \(oldValue) to \(score)")
            if score > 100 {
                print("Congrats \(name) on the high score!")
            }
        }
    }
    var level: Int {
        // Read-only computed property based on score
        switch score {
        case 0..<50: return 1
        case 50..<100: return 2
        default: return 3
        }
    }
    init(name: String) {
        self.name = name
    }
}
let player = Player(name: "Alice")
player.score = 60
print("Level: \(player.level)")
player.score = 120
print("Level: \(player.level)")

出力:

TEXT 📖 参照専用
Score will change from 0 to 60
Score changed from 0 to 60
Score will change from 60 to 120
Score changed from 60 to 120
Congrats Alice on the high score!
Level: 3

警告: プロパティオブザーバはコンピューテッドプロパティでは使用できません。コンピューテッドプロパティには「監視」する格納値がないためです。willSetdidSet の中で同じプロパティを設定しないでください — 無限ループの原因になります。


5. 遅延読み込みと型プロパティ

(1) lazy 読み込み

lazy var はプロパティが最初にアクセスされたときにのみ初期化され、作成コストが高いか常に必要でないプロパティに最適です:

100%
graph LR
    A["lazy var プロパティを宣言"] --> B["インスタンス作成"]
    B --> C["プロパティは未初期化"]
    C --> D["プロパティへの初回アクセス"]
    D --> E["初期化クロージャを実行"]
    E --> F["以降のアクセス: キャッシュ値を返す"]
シナリオ 非lazy lazy
初期化タイミング インスタンス作成時 初回アクセス時
大きなファイル読み込み 使用するかどうかに関わらず読み込み 必要なときのみ読み込み
ネットワークリクエスト 事前読み込み オンデマンド読み込み
複雑な計算 即座に計算 遅延計算

▶ サンプル: 遅延読み込みによる設定

SWIFT
// ============================================
// lazy loading: Database configuration
// ============================================
class DatabaseManager {
    let configFile: String
    // Lazy — only initializes connection when a query is executed
    lazy var connection: String = {
        print("Establishing database connection (one-time only)...")
        // Simulate establishing a connection
        return "Connected to \(configFile)"
    }()
    init(configFile: String) {
        self.configFile = configFile
    }
    func query(_ sql: String) {
        print("Using connection: \(connection) executing query: \(sql)")
    }
}
let db = DatabaseManager(configFile: "app.db")
print("DatabaseManager created, connection not initialized")
db.query("SELECT * FROM users")
db.query("SELECT * FROM orders")
// Second access to connection does not re-initialize

出力:

TEXT 📖 参照専用
DatabaseManager created, connection not initialized
Establishing database connection (one-time only)...
Using connection: Connected to app.db executing query: SELECT * FROM users
Using connection: Connected to app.db executing query: SELECT * FROM orders

警告: lazyvar と共に使用する必要があります(let は遅延初期化できません)。lazyプロパティはスレッドセーフではありません — 複数スレッドからの同時初回アクセスは複数回の初期化を引き起こす可能性があります。

(2) 型プロパティ(static)

型プロパティは特定のインスタンスではなく、型自体に属します。すべてのインスタンスが同じデータを共有します:

SWIFT
// ============================================
// static type properties
// ============================================
struct AppConfig {
    static let appName = "MySwiftApp"
    static var version = "1.0"
    static var launchCount = 0
    static func incrementLaunch() {
        launchCount += 1
    }
}
// Access directly via type name, no instance needed
print(AppConfig.appName)
AppConfig.version = "1.1"
AppConfig.incrementLaunch()
AppConfig.incrementLaunch()
print("Version: \(AppConfig.version), Launches: \(AppConfig.launchCount)")

出力:

TEXT 📖 参照専用
MySwiftApp
Version: 1.1, Launches: 2

6. 完全な例:プロフィール管理システム

SWIFT
// ============================================
// Complete example: Profile management system
// Features: Stored/computed properties + observers + lazy + static
// ============================================
import Foundation
// 1. Global configuration (type properties)
struct Config {
    static let maxAvatarSizeMB = 5.0
    static var apiBaseURL = "https://api.example.com"
    static var userCount = 0
}
// 2. User profile
class UserProfile {
    // Stored properties
    let id: Int
    var name: String {
        didSet {
            print("Name updated: \(oldValue) -> \(name)")
        }
    }
    var avatarUrl: String
    // Computed property — generate initials from name
    var initials: String {
        name.split(separator: " ").compactMap { $0.first }.map { String($0) }.joined()
    }
    // Lazy — compute age description on first access
    lazy var ageDescription: String = {
        print("Computing age description for the first time...")
        return "\(name)'s profile"
    }()
    // Property observer
    var email: String {
        willSet {
            print("Updating email...")
        }
        didSet {
            print("Email updated to \(email)")
        }
    }
    init(id: Int, name: String, email: String, avatarUrl: String) {
        self.id = id
        self.name = name
        self.email = email
        self.avatarUrl = avatarUrl
        Config.userCount += 1
    }
    deinit {
        Config.userCount -= 1
    }
}
// 3. Usage
let profile = UserProfile(
    id: 1,
    name: "Alice Johnson",
    email: "alice@example.com",
    avatarUrl: "https://example.com/avatar.jpg"
)
print("Initials: \(profile.initials)")
print("Age: \(profile.ageDescription)")
print("Active users: \(Config.userCount)")
// Triggers observers
profile.email = "alice@newdomain.com"
profile.name = "Alice Smith"

出力:

TEXT 📖 参照専用
Initials: AJ
Computing age description for the first time...
Age: Alice Johnson's profile
Active users: 1
Updating email...
Email updated to alice@newdomain.com
Name updated: Alice Johnson -> Alice Smith

❓ よくある質問

Q lazyとコンピューテッドプロパティの違いは何ですか?
A lazyプロパティは初回アクセス時に計算して結果を保存し(以降はキャッシュ値を返す)、コンピューテッドプロパティは毎回再計算します(キャッシュなし)。lazyは一度限りのコストに適し、コンピューテッドは動的な値に適しています。
Q プロパティオブザーバはイニシャライザ内で発火しますか?
A いいえ。init 内でプロパティが設定された場合、willSetdidSet は呼び出されません。既に初期化されたインスタンスへの代入時にのみトリガーされます。これはSwiftの設計上の決定です。
Q コンピューテッドプロ��ティはsetterなしにできますか?
A はい。読み取り専用のコンピューテッドプロパティは get キーワードと波括弧を省略できます。例:var doubled: Int { value * 2 }
Q 型プロパティはサブクラスでオーバーライドできますか?
A static プロパティはオーバーライドできません。クラスでオーバーライド可能な型プロパティが必要な場合は、static の代わりに class キーワードを使用してください。
Q プロパティにオブザーバとsetterの両方を持てますか?
A いいえ。プロパティオブザーバはストアドプロパティにのみ適用されます。コンピューテッドプロパティの変更はsetterで処理され、オブザーバは不要です。両者は相互に排他的です。

📖 まとめ


📝 練習問題

  1. 基本: radius ストアドプロパティと area コンピューテッドプロパティ(面積を返す、πr²)を持つ Circle 構造体を定義してください。半径5の円を作成し、面積を出力してください。
  2. 中級: balance ストアドプロパティと didSet オブザーバを持つ BankCard クラスを作成してください — 残高が0未満になったら警告を、10未満になったら低残高アラートを出力します。withdraw(_:)deposit(_:) メソッドを��装してください。
  3. 発展: ログレベルとログ履歴配列を保存する static 型プロパティを使った Logger クラスを実装してください。履歴配列にエントリを追加する static メソッド log(_:level:) と、フォーマット済みログテキストを返す static コンピューテッドプロパティ formattedLog を提供してください。新しいログ追加時にエントリ数の上限を自動チェックするよう didSet を使用してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%