13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SwiftのOptionalとDictionary

Last updated at Posted at 2023-08-04

経緯

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

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 すると解消されます。

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

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

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

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

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

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

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

ちゃんと 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?] としてあげて対処するのが正しそうに見えます。

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でないとき

nilのとき

つまり、

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

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

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

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

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

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

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

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

dict["key"] = nil

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

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

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

// 最初のコード
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?> 的なことをしたい時ってどうやってたかなと気になったのでやってみました。

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

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

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

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

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

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

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
13
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?