🌾

[Swift] [Combine] @Published な変数として Struct 型かつ nil を許容したときの注意点

2022/02/25に公開

@Published / CurrentValueSubjectStruct 型かつ nil を許容したときの注意点

@Published な変数、または、 CurrentValueSubjectOutput に、Struct 型かつ nil を許容すると、その .valuenil の場合でも、その型が持っている変数に値を代入すると、ストリームが流れます。

ちなみに、Class 場合はそのような挙動は起こりません。

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

知らないとハマりそうですね。。。(ハマったので記事にしました😭)

サンプルコード

以下、サンプルコードです。

StructClass の比較のために、適当な StructClass を用意します。

以下を共通のコードとします。

var cancellables = Set<AnyCancellable>()

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

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

@Published の場合

class Hoge {
    // 🏗 Struct
    @Published var moge: Moge? = .init(mogemoge: 0)

    // 🏫 Class
    @Published var fuga: Fuga? = .init(fugafuga: 0)
    
    init() {}
}

let hoge: Hoge = .init()

🏗 Struct の場合

// 🏗 Struct
hoge.$moge
    .dropFirst()
    .sink { print("mogemoge: \(String(describing: $0?.mogemoge))") }
    .store(in: &cancellables)

hoge.moge = Moge(mogemoge: 1)
hoge.moge?.mogemoge = 2
hoge.moge = nil
hoge.moge?.mogemoge = 3
hoge.moge?.mogemoge = 4

// (出力)
// mogemoge: Optional(1)
// mogemoge: Optional(2)
// mogemoge: nil
// mogemoge: nil ← え❗️❓
// mogemoge: nil ← え❗️❓

🏫 Class の場合

// 🏫 Class
hoge.$fuga
    .dropFirst()
    .sink { print("mogemoge: \(String(describing: $0?.fugafuga))") }
    .store(in: &cancellables)

hoge.fuga = Fuga(fugafuga: 5)
hoge.fuga?.fugafuga = 6
hoge.fuga = nil
hoge.fuga?.fugafuga = 7
hoge.fuga?.fugafuga = 8

// (出力)
// fugafuga: Optional(5)
// fugafuga: nil

CurrentValueSubject の場合

CurrentValueSubject@Puhlished のときと同様の挙動になります。

class Hoge {
    // 🏗 Struct
    var mogePub: CurrentValueSubject<Moge?, Never> = .init(.init(mogemoge: 0))

    // 🏫 Class
    var fugaPub: CurrentValueSubject<Fuga?, Never> = .init(.init(fugafuga: 0))
    
    init() {}
}

let hoge: Hoge = .init()

🏗 Struct の場合

// 🏗 Struct
hoge.mogePub
    .dropFirst()
    .sink { print("mogemoge: \(String(describing: $0?.mogemoge))") }
    .store(in: &cancellables)

hoge.mogePub.value = Moge(mogemoge: 1)
hoge.mogePub.value?.mogemoge = 2
hoge.mogePub.value = nil
hoge.mogePub.value?.mogemoge = 3
hoge.mogePub.value?.mogemoge = 4

// (出力)
// mogemoge: Optional(1)
// mogemoge: Optional(2)
// mogemoge: nil
// mogemoge: nil ← え❗️❓
// mogemoge: nil ← え❗️❓

🏫 Class の場合

// 🏫 Class
hoge.fugaPub
    .dropFirst()
    .sink { print("fugafuga: \(String(describing: $0?.))") }
    .store(in: &cancellables)

hoge.fugaPub.value = Fuga(fugafuga: 5)
hoge.fugaPub.value?.fugafuga = 6
hoge.fugaPub.value = nil
hoge.fugaPub.value?.fugafuga = 7
hoge.fugaPub.value?.fugafuga = 8

// (出力)
// fugafuga: Optional(5)
// fugafuga: nil

以上になります。

GitHubで編集を提案

Discussion