Swift: Swiftプロパティ詳解:ストアド、コンピューテッド、プロパティオブザーバ、遅延読み込み
プロパティはSwiftの型に値を関連付けます。基本的なストアドプロパティを超えて、Swiftはコンピューテッドプロパティ、プロパティオブ��ーバ、遅延読み込みなどの強力な機能を提供し、プロパティの読み書き動作を精密に制御できます。
1. 学習目標
- ストアドプロパティとコンピューテッドプロパティの違いと使い方
willSetとdidSetオブザーバによるプロパティ変更の監視lazy読み込みによるパフォーマンスとリソース使用の最適化static型プロパティによる型レベルでのデータ共有- プロパティラッパーの基本概念
2. モバイル開発者の実話
(1) 課題:表示のたびにユーザーアバターをネットワークからダウンロード
Bobはユーザープロフィールページを実装しましたが、表示のたびにネットワークからアバターを取得しています:
class UserProfile {
var avatarUrl: String
func displayAvatar() {
// Downloads avatar from network every time
downloadImage(from: avatarUrl) { image in
// Display image
}
}
}
プロフィールを開くたびにアバターが再ダウンロードされます。ユーザーが繰り返し画面を行き来すると、同じネットワークリクエストが何度も実行され、帯域幅を浪費し、読み込みが遅くなります。
(2) 解決策:遅延読み込み + コンピューテッドプロパティ
class UserProfile {
var avatarUrl: String
lazy var cachedAvatar: UIImage? = {
// Executes only once — downloads on first access
return downloadImage(from: avatarUrl)
}()
}
リアルタイム計算用のコンピューテッドプロパティと組み合わせることで、遅延読み込みは一度だけ実行され — 以降のアクセスはキャッシュされた値を直接返します。
(3) 利点:オンデマンド読み込み、自動キャッシング
| 観点 | 直接ダウンロード | 遅延読み込み |
|---|---|---|
| 読み込みタイミング | 毎回アクセス | 初回アクセス |
| 重複リクエスト | 毎回ダウンロード | 一度だけダウンロード |
| メモリ使用量 | 表示されなくても読み込み済み | 必要に応じて割り当て |
| 起動速度 | 遅い(無関係なデータを事前読み込み) | 速い(初期化を遅延) |
| コードの複雑さ | 手動キャッシュ管理 | Swiftが自動管理 |
3. ストアドプロパティとコンピューテッドプロパティ
(1) ストアドプロパティ
ストアドプロパティは、クラスまたは��造体のインスタンス��一部として格納される定数または変数です:
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内で代入必須 | 初期化不要 |
▶ サンプル: ストアドプロパティ
// ============================================
// 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) コンピューテッドプロパティ
コンピューテッドプロパティは値を格納せず、アクセスされるたびに計算して返します:
// ============================================
// 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
// ============================================
// 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警告: プロパティオブザーバはコンピューテッドプロパティでは使用できません。コンピューテッドプロパティには「監視」する格納値がないためです。
willSetやdidSetの中で同じプロパティを設定しないでください — 無限ループの原因になります。
5. 遅延読み込みと型プロパティ
(1) lazy 読み込み
lazy var はプロパティが最初にアクセスされたときにのみ初期化され、作成コストが高いか常に必要でないプロパティに最適です:
graph LR
A["lazy var プロパティを宣言"] --> B["インスタンス作成"]
B --> C["プロパティは未初期化"]
C --> D["プロパティへの初回アクセス"]
D --> E["初期化クロージャを実行"]
E --> F["以降のアクセス: キャッシュ値を返す"]
| シナリオ | 非lazy | lazy |
|---|---|---|
| 初期化タイミング | インスタンス作成時 | 初回アクセス時 |
| 大きなファイル読み込み | 使用するかどうかに関わらず読み込み | 必要なときのみ読み込み |
| ネットワークリクエスト | 事前読み込み | オンデマンド読み込み |
| 複雑な計算 | 即座に計算 | 遅延計算 |
▶ サンプル: 遅延読み込みによる設定
// ============================================
// 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警告:
lazyはvarと共に使用する必要があります(letは遅延初期化できません)。lazyプロパティはスレッドセーフではありません — 複数スレッドからの同時初回アクセスは複数回の初期化を引き起こす可能性があります。
(2) 型プロパティ(static)
型プロパティは特定のインスタンスではなく、型自体に属します。すべてのインスタンスが同じデータを共有します:
// ============================================
// 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. 完全な例:プロフィール管理システム
// ============================================
// 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
❓ よくある質問
init 内でプロパティが設定された場合、willSet と didSet は呼び出されません。既に初期化されたインスタンスへの代入時にのみトリガーされます。これはSwiftの設計上の決定です。get キーワードと波括弧を省略できます。例:var doubled: Int { value * 2 }。static プロパティはオーバーライドできません。クラスでオーバーライド可能な型プロパティが必要な場合は、static の代わりに class キーワードを使用してください。📖 まとめ
- ストアドプロパティは値を直接格納し、コンピューテッドプロパティはgetter/setterを介して間接的に値を提供する
willSetは値が格納される前にトリガーされ、didSetは格納後にトリガーされるlazy varプロパティは初回アクセス時に初期化され、高コストなリソース作成に最適staticプロパティは型自体に属し、すべてのインスタンスで共有される- 読み取り専用コンピューテッドプロパティは
getとreturnの波括弧を省略できる - プロパティオブザーバは
init中には発火しない
📝 練習問題
- 基本:
radiusストアドプロパティとareaコンピューテッドプロパティ(面積を返す、πr²)を持つCircle構造体を定義してください。半径5の円を作成し、面積を出力してください。 - 中級:
balanceストアドプロパティとdidSetオブザーバを持つBankCardクラスを作成してください — 残高が0未満になったら警告を、10未満になったら低残高アラートを出力します。withdraw(_:)とdeposit(_:)メソッドを��装してください。 - 発展: ログレベルとログ履歴配列を保存する
static型プロパティを使ったLoggerクラスを実装してください。履歴配列にエントリを追加するstaticメソッドlog(_:level:)と、フォーマット済みログテキストを返すstaticコンピューテッドプロパティformattedLogを提供してください。新しいログ追加時にエントリ数の上限を自動チェックするようdidSetを使用してください。