SwiftのOptionalとDictionary

# 経緯

ここ最近、久しぶりに iOS, Swift と向き合っており、思い出しながら勉強しながら開発を進めていたある日、Swift の `[String: Any]` な Dictionary 型を Optional な値で初期化しようとすると警告が表示されることに気がつきました。

```swift
let strValue: String? = "someValue"
let intValue: Int? = 2
let dict: [String: Any] = [
  "key": strValue,  // Expression implicitly coerced from 'String?' to 'Any'
  "key2": intValue  // Expression implicitly coerced from 'Int?' to 'Any'
]
```

この警告自体は `as Any` すると解消されます。

```swift
// 警告が出なくなる
let dict: [String: Any] = [
  "key": strValue as Any,
  "key2": intValue as Any
]
```

が、これで良いのかと少しモヤモヤしていたところ、気になるタイトルの記事も見つけました。

https://qiita.com/tarunon/items/fcd3a24084ed953e0e46

なんだって...？でも今のコードはOptionalをAnyに変換しても問題なく動いてる...？
昔の記事だから今は違うのか...？

かなり初歩的なトピックではありますが、気になったので詳しく調べてみることにしました。

# 気になった記事と同じことをやってみる

簡単なコードを書いてみました。

```swift
struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text(text())
        }
    }

    private func text() -> String {
        let optionalGreeting: String? = "Hello, Swift!"
        return cast(optionalGreeting) ?? "Who are you?"
    }

    private func cast(_ value: Any) -> String? {
        if let string = value as? String {
            return string
        }
        return nil
    }
}

```

コードの概要としては、

- `text()`で画面に表示する文字列を取得する
- `text()`内でOptionalなString型の変数を用意し、先述の記事と同じ方法でAnyに突っ込んでStringにキャスト
- Stringにキャストできたら画面に `Hello, Swift!`, できなければ `Who are you?` と表示される

という感じの簡単なコードです。

![Pasted image 20230804050926.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238072/7e31bfe8-1024-8c85-4862-aeb17ddaabcc.png)

ちゃんと `Hello, Swift!` してくれてる。

昔の話すぎるのかSwift公式では関連するような記載は見つけられなかったのですが、先述の記事のコメントに

> Swift 2.2以降では func a(value: Any) -> String? で期待通り動くようでした

とありました。ひとまず安心。

ただ、最初に記載したものと同様の

```
Expression implicitly coerced from 'String?' to 'Any'
```

という警告が表示されています。

# 警告の解消

こちらの警告、

```
Expression implicitly coerced from 'String?' to 'Any'
```

日本語に訳してみると

```
式が暗黙的に String? から Any に強制的に変換されます。
```

という感じです。

この手の警告が表示される原因としては、言語仕様によって下記の2パターンがあります。

- `String` からより広い型の `Any` に変換するのは危険（ダウンキャストの警告）
- `Optional` な値から `Optional` ではない値にキャストするのは危険

今回のパターンだと「`as Any` すると警告が消える」という点からも分かる通り、ダウンキャストの警告ではなく

> `Optional` な値から `Optional` ではない値にキャストするのは危険
> 

という点で警告が表示されていました。

`as Any` だと明示的に変換しているので警告自体は消えますが、「`Optional` ではない値にキャストするのは危険」という懸念点が解消できていないため、そもそものDictionary型の変数宣言を `[String: Any?]` としてあげて対処するのが正しそうに見えます。

```swift
let strValue: String? = "someValue"
let intValue: Int? = 2
let notOptionalValue: String = "notOptionalValue"
let dict: [String: Any?] = [
  "key": strValue,
  "key2": intValue,
  "key3": notOptionalValue  // Optionalでない値もOK
]

```

ですがこれだと、Swiftの `Optional`, `Dictionary` それぞれの仕様により、少し扱いづらい `Dictionary` 変数になってしまいます。

# Optional と Dictionary の仕様

Swiftの `Optional` は明示的にenum型として存在し、値が `nil` でない場合は `Optional<Wrapped>.some` に、`nil`の場合は `Optional<Wrapped>.none` にラップされます。

nilでないとき

https://github.com/apple/swift/blob/a11baafb26f800b12b48b3e874c3ab3ea28ad9cf/stdlib/public/core/Optional.swift#L135-L136

nilのとき

https://github.com/apple/swift/blob/a11baafb26f800b12b48b3e874c3ab3ea28ad9cf/stdlib/public/core/Optional.swift#L211-L214

つまり、

```swift
let strValue: String? = "someValue"
let strValue2: String? = nil
```

は

```swift
let strValue: Optional<String> = Optional.some("someValue")
let strValue2: Optional<String> = Optional.none
```

の省略形と捉えると分かりやすいです。
見た目上は `String or nil` 的な型に見えますが、実際には `Optional` というenum型が扱われているイメージです。

また、Swiftの `Dictionary` にはキーを指定して値を取得すると、値を `Optional` 型でラップして返却する、という仕様があります。

https://github.com/apple/swift/blob/f674e56a0a5f0a1079583ab2972e7303b102997d/stdlib/public/core/Dictionary.swift#L788-L804

そのため、`Dictionary` 型のValueに `Optional` な型を指定してしまうと、いざキーを指定して値を取得しようとした場合に二重で `Optional` にラップされてしまいます。

```swift
let optionalValue: String? = "someValue"
let dict: [String: Any?] = [
  "key": optionalValue
]
let value: String?? = dict["key"]

// ↑ の ? を全てOptionalに展開するとこんな感じ

let optionalValue: Optional<String> = Optional.some("someValue")
let dict: [String: Optional<Any>] = [
  "key": optionalValue
]
let value: Optional<Optional<String>> = dict["key"]
```

`value`から安全に実際の値を取得しようとすると、こんな感じで二重に値の存在チェックをする必要があります。

```swift
if let valueFromDict = value, let actualValue = valueFromDict {
	// Optionalでない値 actualValue が取り出せた
}
```

# Dictionary の仕様から考える解決策

先述のリンクにヒントがあるのですが、Swiftの `Dictionary` では値に `nil` がセットされるとキーごと消す、という仕様があります。

> https://github.com/apple/swift/blob/f674e56a0a5f0a1079583ab2972e7303b102997d/stdlib/public/core/Dictionary.swift#L788-L804

```swift
let strValue: String = "someValue"
let intValue: Int = 2
var dict: [String: Any] = [
  "key": strValue,
  "key2": intValue
]

dict["key"] = nil

print(dict)  // "["key2": 2]"
```

ただし、これは初期化の時には適用されません。

https://github.com/apple/swift/blob/f674e56a0a5f0a1079583ab2972e7303b102997d/stdlib/public/core/Dictionary.swift#L399-L401C4

そのため、最初のコードは下記のように書き換えれば警告が消え、値を取得するときの二重のunwrapも不要になります。

```swift
// 最初のコード
let strValue: String? = "someValue"
let intValue: Int? = 2
let dict: [String: Any] = [
  "key": strValue,  // Expression implicitly coerced from 'String?' to 'Any'
  "key2": intValue  // Expression implicitly coerced from 'Int?' to 'Any'
]

// ↓ 書き換え

let strValue: String? = "someValue"
let intValue: Int? = 2
// 一旦空で初期化
var dict: [String: Any] = [:]

// 後からキーを指定して代入する
dict["key"] = strValue
dict["key2"] = intValue

print(dict)  // "["key": "someValue", "key2": 2]"

// nilならキー自体が生成されない
let nilValue: String? = nil
dict["key3"] = nilValue

print(dict)  // "["key": "someValue", "key2": 2]"
```

# 余談: Kotlinだとどうだっけ

Android/Kotlin歴が長いので、Swiftを触っていても「Kotlinだとどうなんだっけ」とよく気になります。
Swiftの `Dictonary` はKotlinだと `Map` なのですが、Kotlinで `Map<String, Any?>` 的なことをしたい時ってどうやってたかなと気になったのでやってみました。

```kotlin
val strValue: String? = "someValue"
val intValue: Int? = 2
// Map<String, Comparable<*>?>
val map = mapOf("key" to strValue, "key2" to intValue)
```

Kotlinだと、`String` や `Int` が `Comparable` (Swiftでいう`Equatable`) を継承しているので、`Map<String, Comparable<*>?>` と型推論されていました。

Kotlinでは何か別の型でラップするというよりは `or null` 的な考え方なので、値を取得する際も二重の存在チェック等は不要でした。

```kotlin
val someValue: Comparable<*>? = map["key"]
someValue?.let { actualValue ->
	// nullでない値 actualValue が取り出せた
}
```

試しにSwiftで `Equatable` を使ってみるとこんな感じでも書けました。`Equatable` である旨みは少なそうですが、`dict`の値同士を比較したいケースなどではありかも？

```swift
let strValue: String = "someValue"
let intValue: Int = 2
let dict: [String: any Equatable] = [
	"key": value1,
	"key2": value2
]
```