🌾

[Swift] [Combine] @Published な変数が Class と Struct では挙動が異なる件

2022/02/26に公開

結論

@Published な変数、または、 CurrentValueSubjectOutput に、Struct または Class を設定した場合に以下のような差が生じる。

  • Struct の場合
    • 値を更新した場合、出力が走る
  • Class の場合
    • 値をを更新した場合、出力は走らない

Struct の場合

// 🌟struct🌟
struct Hoge {
    var mogemoge: Int
    
    init(mogemoge: Int) {
        self.mogemoge = mogemoge
    }
}

let hogePub: CurrentValueSubject<Hoge, Never> = .init(Hoge(mogemoge: 0))

hogePub
    .dropFirst()
    .sink { print("receive: \(String(describing: $0.mogemoge))") }
    .store(in: &cancellables)

hogePub.value = Hoge(mogemoge: 1)
hogePub.value.mogemoge = 2 // ← 注目🌟
hogePub.value = Hoge(mogemoge: 3)

// (出力)
// receive: 1
// receive: 2 ← 注目🌟
// receive: 3
  • var となる変数を更新した場合、出力が走る

ちなみに nil を許容している場合は、.value 自体が nil でも、出力がながれるので注意が必要です。

Class の場合

// 🌟class🌟
class Hoge {
    var mogemoge: Int
    
    init(mogemoge: Int) {
        self.mogemoge = mogemoge
    }
}

let hogePub: CurrentValueSubject<Hoge, Never> = .init(Hoge(mogemoge: 0))

hogePub
    .dropFirst()
    .sink { print("receive: \(String(describing: $0.mogemoge))") }
    .store(in: &cancellables)

hogePub.value = Hoge(mogemoge: 1)
hogePub.value.mogemoge = 2 // ← 注目🌟
hogePub.value = Hoge(mogemoge: 3)

// (出力)
// receive: 1
// receive: 3
↑
receive: 2 がない!!
  • var となる変数を更新した場合、出力は走らない

共通コード

import Combine

var cancellables = Set<AnyCancellable>()

struct Moge {
    var mogemoge: Int = 0
    
    init(mogemoge: Int) {
        self.mogemoge = mogemoge
    }
}

class Fuga {
    var fugafuga: Int = 0
    
    init(fugafuga: Int) {
        self.fugafuga = fugafuga
    }
}

@Published の場合

class Hoge {
    @Published var moge: Moge = .init(mogemoge: 0)
    @Published var fuga: Fuga = .init(fugafuga: 0)
    
    init() {}
}

let hoge: Hoge = .init()

Struct の場合

hoge.$moge
    .dropFirst()
    .sink { print("mogemoge: \($0.mogemoge)") }
    .store(in: &cancellables)

hoge.moge = Moge(mogemoge: 1)
hoge.moge.mogemoge = 2 // ← 注目🌟
hoge.moge = Moge(mogemoge: 3)

// (出力)
// mogemoge: 1
// mogemoge: 2 ← 注目🌟
// mogemoge: 3
  • Struct の場合
    • 値を更新した場合、出力が走る

Class の場合

hoge.$fuga
    .dropFirst()
    .sink { print("fugafuga: \($0.fugafuga)") }
    .store(in: &cancellables)

hoge.fuga = Fuga(fugafuga: 4)
hoge.fuga.fugafuga = 5 // ← 注目🌟
hoge.fuga = Fuga(fugafuga: 6)

// (出力)
// fugafuga: 4
// fugafuga: 6
↑
receive: 5 がない!!
  • Class の場合
    • 値をを更新した場合、出力は走らない

CurrentValueSubject の場合

class Hoge {
    var mogePub: CurrentValueSubject<Moge, Never> = .init(.init(mogemoge: 0))
    var fugaPub: CurrentValueSubject<Fuga, Never> = .init(.init(fugafuga: 0))
    
    init() {}
}

let hoge: Hoge = .init()

Struct の場合

hoge.mogePub
    .dropFirst()
    .sink { print("mogemoge: \($0.mogemoge)") }
    .store(in: &cancellables)

hoge.mogePub.value = Moge(mogemoge: 1)
hoge.mogePub.value.mogemoge = 2 // ← 注目🌟
hoge.mogePub.value = Moge(mogemoge: 3)

// (出力)
// mogemoge: 1
// mogemoge: 2 ← 注目🌟
// mogemoge: 3
  • Struct の場合
    • 値を更新した場合、出力が走る

Class の場合

hoge.fugaPub
    .dropFirst()
    .sink { print("fugafuga: \($0.fugafuga)") }
    .store(in: &cancellables)

hoge.fugaPub.value = Fuga(fugafuga: 4)
hoge.fugaPub.value.fugafuga = 5 // ← 注目🌟
hoge.fugaPub.value = Fuga(fugafuga: 6)

// (出力)
// fugafuga: 4
// fugafuga: 6
↑
receive: 5 がない!!
  • Class の場合
    • 値をを更新した場合、出力は走らない

考察

このような挙動の差は、Class が参照型であり、Struct が値型であること原因であると考えられます。

また、似たような話として、didSet の挙動も ClassStruct で差があります。

まとめ

@Published な変数、または、 CurrentValueSubjectOutput に、Struct または Class を設定した場合に以下のような差が生じる。

  • Struct の場合
    • 値を更新した場合、出力が走る
  • Class の場合
    • 値をを更新した場合、出力は走らない

以上になります。

GitHubで編集を提案

Discussion