0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Swift】Value Semanticsについて

Posted at

Swiftにおける全ての値は、値型または参照型に分類されます。
値型とはIntなどのSwift標準型やstructなど、変数への代入がなされた際に値がコピーされるもの、参照型とはclassなど代入がなされた際には値はコピーされず同じ参照先が代入されるものです。
値型は代入が起こった際に値のコピーが入るという性質上、生成された値に変更を加えない限りは常に同じ値を保つというメリットがあります。これをValue Semanticsといいます。
ここでは値型と参照型のメリット、Value Semanticsの実現方法について解説したいと思います。

値型 vs 参照型

値型とそのメリット

struct ValueType {
    var intValue: Int

    init(intValue: Int = 0) {
        self.intValue = intValue
    }
}

let c = ValueType(intValue: 0)
var d = c
d.intValue = 1
print(c.intValue) // 0

dに変更を加えても、cは何も変更を受けません。
dにはcがコピーされて代入されるためです。

値型は

  • 複数生成してもそれぞれの指し示す値の意味は全く同じになる場合
  • 生成後は独立している

ような値に適しています。一度アサインした値を変更するためには、そのものを置き換える必要があります。
数字や文字、データ、およびそれらを保持するだけの構造体等に使用されます。
SwiftUIの宣言的UIも、ここにこういうViewを配置する、という情報を持った構造体の集合と捉えれば、この値型の特徴をよく表していると言えます。

参照型とそのメリット

class ReferenceTypeClass {
    var intValue: Int

    init(intValue: Int = 0) {
        self.intValue = intValue
    }

    func increment() {
        intValue += 1
    }
}

let a = ReferenceTypeClass()
let b = a
b.increment()
print(a.intValue) // 1

bに変更を加えたにも関わらず、aの値にまで変更が及んでしまっています。
これは、baに代入されている値が同じ参照先となっているためです。

参照型は

  • 同種であっても複数生成時、一つ一つの値が区別できる
  • 生成された後、他のものと相互に作用し合う

ような値に適しています。一度アサインした値を変更することは、内部変数への代入または直接書き換える関数等を呼び出すことで可能になっています。
UIApplicationなどのUIKitフレームワーク等、生成された後に互いに相互作用し合うようなclass等に用いられます。

Value Semanticsの実装方法

値型しか持たないstructenum、tuple、Array等はその生成時に自動で値がコピーされて入ります。そのため、ここでは以下の2パターンについて解説します。

  1. 参照型の値でValue semanticsを満たす場合
  2. 参照型の値を含む値型の値でValue semanticsを満たす場合

1. 参照型の値でValue semanticsを満たす場合

この場合のポイントは、内包している変数に変更可能な箇所がないということです。そのため、内包している変数全てがletで、かつ値型であればValue semantics条件を満たします。

class ReferenceTypeClass {
    let intValue: Int

    init(intValue: Int = 0) {
        self.intValue = intValue
    }

    func increment() {
        intValue += 1 // コンパイルエラー: Left side of mutating operator isn't mutable: 'intValue' is a 'let' constant
    }
}

let a = ReferenceTypeClass()
let b = a
b.intValue = 1 // コンパイルエラー: Cannot assign to property: 'intValue' is a 'let' constant
print(a.intValue)

こうすることで、変数の外部から値を変えることができないことを保証でき、Value Semanticsの条件を満たしたと言えるでしょう。

2. 参照型の値を含む値型の値でValue semanticsを満たす場合

値型であるstructに参照型プロパティを持たせた場合、以下のようになります。

struct ValueType {
    var intValue = 0
    // 追加
    var reference = ReferenceTypeClass(intValue: 0)

    init(intValue: Int = 0) {
        self.intValue = intValue
    }
}
let c = ValueType(intValue: 0)
let d = c
d.reference.intValue = 1
print(c.reference.intValue) // 1

dcのコピーではありますが、reference自体は同じ参照先が登録されているので、dで変更した値はcにも反映されてしまいます。
これを防ぐためには、以下のように適切にaccess controlの設定、getterとsetterの設定をしてあげる必要があります。

struct ValueType {
    var intValue = 0
    // privateに変換
    private var reference = ReferenceTypeClass(intValue: 0)

    // getされる場合はprivate変数の値を、setされる場合は新しいインスタンスを生成する。
    var referenceIntValue: Int {
        get {
            return reference.intValue
        }
        set {
            reference = ReferenceTypeClass(intValue: newValue)
        }
    }

    init(intValue: Int = 0) {
        self.intValue = intValue
    }
}

let c = ValueType(intValue: 0)
var d = c
d.referenceIntValue = 1
print(c.referenceIntValue) // 0

このようにすることで、referenceに何か値の変更が入った際に別の参照先に入れ替わるため、dreferenceに変更を加えてもcreferenceに影響が及ぶことはありません。このように、setterでのみ参照先を変更するこの仕組みをCopy of Writing(COW)といいます。

しかし、dがない場合(つまり、同じ値を代入した変数が他にない場合)はsetのたびにインスタンスを入れ替える必要がありません。その場合は、内部のreferenceの値を直接書き換えてやれば良いです。
その際、下記のようにする必要があります。

struct ValueType {
    var intValue = 0
    private var reference = ReferenceTypeClass(intValue: 0)

    var referenceIntValue: Int {
        get {
            return reference.intValue
        }
        set {
            // 追加
            if isKnownUniquelyReferenced(&reference) {
                reference.intValue = referenceIntValue
            } else {
                reference = ReferenceTypeClass(intValue: newValue)
            }
        }
    }

    init(intValue: Int = 0) {
        self.intValue = intValue
    }
}

let c = ValueType(intValue: 0)
var d = c
d.referenceIntValue = 1
print(c.referenceIntValue) // 0

結果は変わらずですが、インスタンス生成コスト分、set処理が軽くなります。

余談: Sendableについて

SwiftではValue Semanticsを満たしていることを示すprotocolが存在します。それがSendableです。公式ドキュメント
Sendableに準拠するプロトコルは全て、Value Semanticsを満たしています(逆に全てのValue Semanticsを満たしているものがSendableを満たすわけではありません)。
これはConcurrencyで複数の箇所で非同期処理を行っても、その値が変更されることがないため安全にデータを送信できるという意味でSendableという名前がつけられています。
Sendableである変数にValue Semanticsを満たしていない値を代入した際には、コンパイルエラーが発生します。

まとめ

  • 生成された値に変更を加えない限りは常に同じ値を保つ機能をValue Semanticsという。
  • 数字や文字、データ、およびそれらを保持するだけの構造体等、一度生成した値を書き換えるにはそのものを代入し直す必要があるものを値型といい、デフォルトでValue Semanticsを満たす。
  • 逆に、classのように内部変数への代入または直接書き換える関数等を呼び出すことで値の書き換えが可能であるものを参照型といい、Value Semanticsを満たすためには以下条件が必要となる。
    • 内包している変数全てがletで、かつValue Semanticsを満たしていること。
  • また、参照型を含むstructがValue Semanticsを満たすためには、access control, getterとsetterを適切に設定してあげる必要がある。
  • Sendableであればvalue semanticsを満たす。

最後に

こちらは私が書籍で学んだ学習内容をアウトプットしたものです。
わかりにくい点、間違っている点等ございましたら是非ご指摘お願いいたします。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?