経緯
ここ最近、久しぶりに 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?
と表示される
という感じの簡単なコードです。
ちゃんと 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
がセットされるとキーごと消す、という仕様があります。
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だと、String
や Int
が Comparable
(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
]