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


# `@Published` / `CurrentValueSubject` に `Struct` 型かつ `nil` を許容したときの注意点

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

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

- 参考：[[Swift] [Combine] @Published な変数が Class と Struct では挙動が異なる件](https://zenn.dev/ikuraikura/articles/2022-02-26-pub4)

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

- 参考
  - [値型と参照型 - Swiftの始め方](https://swift.codelly.dev/guide/%E6%A7%8B%E9%80%A0%E4%BD%93%E3%81%A8%E3%82%AF%E3%83%A9%E3%82%B9/%E5%80%A4%E5%9E%8B%E3%81%A8%E5%8F%82%E7%85%A7%E5%9E%8B.html)

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

# サンプルコード

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

`Struct` と `Class` の比較のために、適当な `Struct` と `Class` を用意します。

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

```swift
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` の場合

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

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

let hoge: Hoge = .init()
```

## 🏗 Struct の場合

```swift
// 🏗 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 の場合

```swift
// 🏫 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` のときと同様の挙動になります。

```swift
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 の場合

```swift
// 🏗 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 の場合

```swift
// 🏫 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
```



以上になります。