Help us understand the problem. What is going on with this article?

Swiftのプロパティを三つの分類で整理してみた

OSSのコードを読んでいたり、他の人の講演やLTを聞いていると、あれ?lazyってなんだったっけ?と、「プロパティについてちゃんと理解していないよな。。。」って思うことがよくあります。
今回はそんなプロパティについてまとめましたので、同じような悩みをもった方の参考となれば嬉しいです。

対象とする読者

  • プロパティについてなんとなく使えるけど、聞かれると困る人
  • lazyってなんだっけ?コンピューテッドプロパティってなんだっけ?って人

目次

  • プロパティの分類方法
  • 分類①:定数と変数
  • 分類②:インスタンスプロパティとスタティックプロパティ
  • 分類③:ストアドプロパティとコンピューテッドプロパティ

検証環境

  • Swift : 5.0.1
  • Xcode : 11.0(Playground)

プロパティの分類方法

プロパティとは

型に紐づく値のこと。プロパティと言ったり、属性と言ったりする。
型はプロパティとメソッド(関数/ふるまい)で構成される。

分類①:定数と変数

値の再代入が不可能な定数と、再代入が可能な変数での分け方。letとvar。

分類②:インスタンスプロパティとスタティックプロパティ

インスタンスに紐づくプロパティと型自身に紐づくプロパティでの分類。
インスタンスによらず、型共通の値をもたせたいときには、スタティックプロパティを用いる。
スタティックプロパティにはlet/varの前にstaticを付ける。

分類③:ストアドプロパティとコンピューテッドプロパティ

値を保持するプロパティと値を都度計算するプロパティ
値を保持するストアドプロパティに対して、コンピューテッドプロパティは既存のプロパティを用いて都度用意されるため、計算もととの整合性が常に保たれるという特徴がある。

分類①:定数と変数

プロパティは値の名前の前に必ずletかvarを付けて、プロパティが定数か変数かを宣言しなければならない。変数には値の再代入が可能だが、定数への値の再代入は不可能。

struct Message {
    let greeting = "Hello," // 定数
    var name = "Everyone!"  // 変数
}

var message = Message()
message.name = "@ichikawa7ss!" // 変数には再代入可能だが
message.greeting = "Goodbye,"  // 定数には再代入不可能 -> コンパイルエラー

分類②:インスタンスプロパティとスタティックプロパティ

インスタンスに紐づくプロパティと型自身に紐づくプロパティでの分類。

次のソースコードでの例のように、インスタンスプロパティではプロパティがインスタンスに紐づいているので、インスタンスごとにプロパティの設定が可能。

struct Message {
    let greeting = "Hello," // インスタンスプロパティ(定数)
    var name = "Everyone!"  // インスタンスプロパティ(変数)
}

// Message構造体のインスタンスを生成
var message1 = Message()
let name1 = message1.name        // Everyone!

// 別のMessage構造体のインスタンスを生成
var message2 = Message()
// インスタンスプロパティに値を代入
message2.name = "@ichikawa7ss!"
let name2 = message2.name        // @ichikawa7ss!

一方でスタティックプロパティでは、プロパティが型自身に紐づくため、インスタンスによらずプロパティの値は一定である。
また、呼び出し方法もインスタンスプロパティがインスタンス名.プロパティ名であるのに対して、型に紐づくスタティックプロパティは型名.プロパティ名で値の呼び出しを行う必要がある。

struct Student {
    static let className = "3-A"          // スタティックプロパティ
    static var teacherName = "Ms.Tanaka"  // スタティックプロパティ
    var name = "Shoma Ichiakwa"           // インスタンスプロパティ
}

// 一つ目のインスタンスを生成
var student1 = Student()
let teacherName1 = Student.teacherName  // Ms.Tanaka (スタティックプロパティ)
let name1 = student1.name               // Shoma Ichiakwa

// 別のインスタンスを生成
var student2 = Student()
// インスタンスプロパティ(インスタンスに紐づくプロパティ)に値を代入
student2.name = "Taro Yamada"
// スタティックプロパティも変数であれば変更可能
// スタティックプロパティは'型.プロパティ名'で値を呼び出す
Student.teacherName = "Ms.Suzuki"

let teacherName2 = Student.teacherName  // Ms.Suzuki (スタティックプロパティ)
let name2 = student2.name               // Taro Yamada

Student.className = "3-B"               // 定数は再代入不可能 -> コンパイルエラー

letとstaticは最初混同するかもしれないが、staticは型に共通の変数を持たせるだけなので、値の変更は可能。

分類③:ストアドプロパティとコンピューテッドプロパティ

ストアドプロパティ

これまで見てきたプロパティはインスタンスや型に値を保持することができた。これをストアドプロパティという。一方で値を保持せずにアクセスするたびに値を計算するプロパティをコンピューテッドプロパティと呼ぶ。
まずはストアドプロパティに特有なプロパティオブザーバーいう機能とレイジーストアドプロパティについて確認する。

プロパティオブザーバー

ストアドプロパティにはプロパティオブザーバーという、値の変更を監視する処理を定義することができる。(コンピューテッドプロパティは変更前の値を保持しないので、変更の監視という考え方自体が存在しない。)
以下は、インスタンス化したStudentのnameプロパティに値が更新された際に、オブザーバープロパティが機能している例。

struct Student {
    static let className = "3-A"
    // プロパティオブサーバーの用いてプロパティの変更を監視
    var name = "Shoma Ichiakwa" { // ストアドプロパティ
        // 値変更の"直前"に実行
        willSet {
            // 代入された値は'newValue'として使用可能
            print("**Student name will chenge to \(newValue) from \(name)**")
        }
        // 値変更の"直後"に実行
        didSet {
            // didSetの場合は代入された値はプロパティを指定して呼び出し
            print("**Student name had chenged to \(name)**")
        }
    }
}

// Student構造体のインスタンスを作成
var student = Student()
print("Created Instance: \(student)")
student.name = "Taro Yamada"
print("Student name: \(student.name)")

この結果は以下の通り。

【実行結果】
Created Instance: Student(name: "Shoma Ichiakwa")
**Student name will chenge to Taro Yamada from Shoma Ichiakwa**
**Student name had chenged to Taro Yamada**
Student name: Taro Yamada

インスタンス生成後、studentのnameプロパティに値の変更を行うとwillSet->didSetの順に呼び出される。

レイジーストアドプロパティ

varの前にlazyをつけることで、レイジーストアドプロパティを宣言することができる。
通常のプロパティはインスタンスが生成され初期化されるときに値が取得されるのに対して、レイジーストアドプロパティではインスタンス生成後、実際にプロパティへのアクセスが行われるまで値の取得を遅延できる。
これにより初期化コストの高いプロパティの初期化を遅延させることができるため、アプリケーションのパフォーマンス向上に有効。
ちなみにコンピューテッドプロパティでは、そもそも毎回アクセスのたびに値が取得されるため、アクセスを遅延させる必要がない。

struct SomeStruct {

    // ストアドプロパティ
    var value : Int = {
        print("** Accessed and set 'value' **")
        return 1
    }()

    // レイジーストアドプロパティ
    lazy var lazyValue: Int = {
        print("** Accessed and set 'lazyValue' **")
        return 2
    }()
}

var someStruct = SomeStruct()
print("Instance Created")
print("value is \(someStruct.value)")
print("lazyValue is \(someStruct.lazyValue)")

この出力結果は以下の通り。

【実行結果】
** Accessed and set 'value' **
Instance Created
value is 1
** Accessed and set 'lazyValue' **
lazyValue is 2

インスタンス作成時にはlazyValueへのアクセスは遅延されており、値をプリントするときに初めてlazyValueへのアクセスが行われている。

また以下の例のように、レイジーストアドプロパティはインスタンス生成後にアクセスされるため、他のストアドプロパティを使用することができる。

struct SomeStruct {
    // ストアドプロパティ
    var value = 1
    // ストアドプロパティ
    var notLazyDoubleValue: Int = {
        return value * 2 // 通常のストアドプロパティは初期化時に他のプロパティを使うことができない -> コンパイルエラー
    }()
    // レイジーストアドプロパティ
    lazy var lazytripledValue: Int = {
        return value * 3 // レイジーストアドプロパティはインスタンス生成後にアクセスされるため、他のプロパティを使用可能
    }()
}

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

コンピューテッドプロパティはすでにあるプロパティを用いて、アクセスするたびに計算を行うため、計算元のプロパティと常に値の整合性が取れることが特徴。
コンピューテッドプロパティには値を呼び出すときに、他のプロパティの値を用いて値を返してくれるゲッタと、プロパティに値が代入されたときに、他のプロパティを更新してくれるセッタが存在する。

ゲッタ

ゲッタではプロパティにアクセスしたときに他のプロパティを用いて、値を返却してくれる。以下の例ではmessage.signatureにアクセスするたびにgetの処理が走り、toプロパティを用いて値を計算する。

struct Message {
    var to : String
    // コンピューテッドプロパティ
    var signature : String {
        // ゲッタを使用して、他のプロパティを用いた値の取得が可能
        get {
            return "Dear,\(String(describing: to))"
        }
    }
    init(to: String) {
        self.to = to
    }
}

let message = Message(to: "Ms.Tanaka")
message.signature // Dear,Ms.Tanaka

セッタ

セッタを定義されたプロパティは、プロパティに値が代入されると、setのスコープ部に書かれた処理を実行する。これにより他のプロパティの更新処理ができるため、ゲッタと併用することで、相関のある二つのプロパティ間での整合性を記述することが可能。
以下の処理ではkmとmという二つの距離単位をゲッタとセッタを用いることで二変数間の整合性を保っている。

struct Distance {
    var kirometers : Int = 0

    // コンピューテッドプロパティ
    var meters : Int {
        // ゲッタを使用して、他のプロパティを用いた値の取得が可能
        get {
            return kirometers * 1000
        }
        // セッタを使用して、他のプロパティを更新することが可能
        // セットされた値を変数として命名して利用可能
        set(inputMeters) {
            kirometers = inputMeters / 1000
        }
    }
}

var distanceStations = Distance()
distanceStations.kirometers = 2
distanceStations.meters          // 2000

var distanceSchoolAndHome = Distance()
distanceSchoolAndHome.meters = 5000
distanceSchoolAndHome.kirometers // 5

distanceSchoolAndHome.meters = 5000のようにと値を格納するので、直観的にはプロパティが値を保持しているように思えるが、実際にはsetキーワード定義された処理を行なっているだけで、値は保持されていない。
kirometersに値が代入されようと、metersに値が代入されようと、結果的に値の更新・保持がなされるのはストアドプロパティであるkirometersである。metersの値を取得するときにはget内の処理により、kirometersを通して正しいmetersの値取得を行なっている。

まとめ

プロパティ、ややこしくて今まで避けてきましたが、Swiftの基礎なだけに一度整理することで様々な領域の理解に繋がると感じました。三つの分類に分けて考えると、役割の明確化なプロパティの宣言ができるようになりそうです。

参考

[改訂新版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 (WEB+DB PRESS plus)
(いつも大変お世話になっております🙇‍♂️)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした