Swift UIの開発で多くの人がよくわからないまま使っているであろうsome
キーワード。some
と並んで紹介されるany
キーワード。
オブジェクト指向経験者なら1度は考えが浮かぶ、「some
もany
も記述しなくても問題なくない?」という疑問。Swiftに詳しくない人が調べるとほぼ深みにハマると思うので、ケンヂまるが代わりに深みにハマって調べてきた情報を、いい感じにまとめました。
想定読者は、some
やany
について調べる前のモヤモヤしながらSwiftUIのプログラムを書いていた頃の自分です。1ヶ月ほど前の自分に向けた記事にしました。SwiftUIのサンプルコードをいくつか実行させて雰囲気はわかっているけどよく理解できていないんだよねって方に読んでもらえると、何か得るものがあるかもしれません。
まずは「まとめ」から
時間に追われている人も多いと思うので、some
とany
の使い分け方から。考えるより感じる派の人はこれで十分だと思います。
コンパイル時に戻り値が1種類に特定できる場合はsome
関数の実装内容から、コンパイラが「戻り値は常にInt型やな」と判断できる場合は戻り値型にsome
を付けます。
return Int(1)
}
コンパイル時に戻り値が1種類に特定できない場合はany
プログラムを実行してみるまで戻り値の型が定まらない場合は戻り値型にany
を付けます。
if value.contains(".") {
return Double(value) ?? 0.0
} else {
return Int(value) ?? 0
}
}
型情報が関連付けられていない場合はsomeもanyも不要
型情報が関連付いていないプロトコルやクラスに対しては、通常の多様性(Polymorphism)が問題なく運用できるため、some
もany
も不要です。
}
struct IntImplementation : A {
}
struct StringImplementation : A {
}
func returnProtocolA(_ name: String) -> A {
if name == "IntImplementation" {
return IntImplementation()
} else {
return StringImplementation()
}
}
次の章からは、some
、any
、どちらも使わない場合の具体的な部分を解説していきます。
someキーワード
「some
、アイツとの初めての出会いは、それはSwiftUIの勉強で最初に出くわすサンプルコード。いきなり出てきて挫折させようとしてくる。正直最初は、なんて嫌なヤツなんだって思ったよ。」とケンヂまるさん。(自分の記事に自分が登場して語っていく新しいスタイル)
struct MyApp: App {
var body: some Scene {
WindowGroup {
Text("Hello, world!")
}
}
}
「だけど、some
とちゃんと向き合おうと思ったんだ。そして俺はsome
探しの旅に出た。長い長い、旅だった。」とケンヂまるさんは続けます。(自分で語るスタイルもうええわ)
旅という表現は間違っていなくて、some
はSwift言語に馴染んでいないと理解できない部分だと思います。深掘りするには、しばらくはSwift UIの勉強はおあずけくらう覚悟が必要です。
本題に戻ってsome
の使い方ですが、抽象型を使ってはいるものの、コンパイルされる時には1つの特定の型で定まるような場合に使います。
例えばNumeric
プロトコル型を戻り値に返すcreateInteger()
関数を定義する場合、実際の戻り値はIntかもしれないし、Double
かもしれないことになります。
(Int
もDouble
もNumeric
プロトコルを実装しているため)
// 関数の実際の実装
}
しかし実際の関数実装がIntしか戻り値として返さないことが保証できる場合があります。
そういった場合は、戻り値をsome Numeric
と記述することができます。
return Int(12)
}
Intしか戻り値に返さないなら、そもそも戻り値はInt
と書けばいいじゃないかと思うかもしれません。次のような感じで。
return Int(12)
}
確かに自分でシグネチャを決めれる状況ではそうなんですけど、Int
を返してもいいしDouble
を返してもいいし、とにかく共通のプロトコルであるNumeric
なら許容できるぜってフレームワークを作る場合なんかは、some Numeric
のようにプロトコル定義しておいて、フレームワークを使って処理を実装する人が特定の1つの型だけを戻り値として返すように記述する、といった状況でsome Numeric
のように定義する価値が出てきます。
SwiftUIもフレームワークなので、同じことが言えます。ここでもう一度SwiftUIのサンプルコードを見てみましょう。
struct MyApp: App {
var body: some Scene {
WindowGroup {
Text("Hello, world!")
}
}
}
SwiftUIフレームワークは、@main属性が付けられた、App
プロトコル実装の構造体がエントリポイントになります。この例ではMyApp
構造体がエントリポイントです。
MaApp
構造体は、App
プロトコルの実装としてbody
というcomputed propertyを我々アプリ開発者が実装します。そして、フレームワークとしてのbody
プロパティの型はSceneプロトコルにしてね、とルール化されています。
そのルールに則って、body
プロパティの中でWindowGroupを作成する実装を考えてみましょう。(WindowGroup
はScene
プロトコルの実装型です)
次の例ではbody
の型はsome Scene
と記述しているものの、実際はWindowGroup
としてコンパイルされます。
@SceneBuilder
func createScene() -> some Scene {
WindowGroup {
}
}
let createdScene = createScene()
print(type(of: createdScene)) // 出力:WindowGroup&lgt;EmptyView>
いきなり@SceneBuilderが登場しましたが、@SceneBuilderはApp.body定義の属性でもあります。
@SceneBuilder
属性が付けられたブロック内でインスタンス化されたScene
は、それが例え複数あっても1つのラップされた戻り値になります。
次の例だと、createdScene()
の戻り値型はWindowGroup<_ConditionalContent<Text, EmptyView>
になります。
@SceneBuilder
func createScene() -> some Scene {
WindowGroup {
if Int.random(in: 0...1) == 0 {
Text("sample")
} else {
}
}
}
let createdScene = createScene()
print(type(of: createdScene)) // 出力:WindowGroup<_ConditionalContent<Text, EmptyView>>
anyキーワード
先ほどサンプルコードで登場したcreateInteger()
関数は、関数の実装を確認すると戻り値がInt
型であることが保証されていました。
それに対し、関数の戻り値がInt
型かもしれないし、Double
型かもしれないような場合にはsome
キーワードではなくany
キーワードを使います。
createNumber(value)
関数は、文字列をNumeric型に変換して返す関数です。
文字列に”.”が含まれている場合は小数点であると仮定してDouble型を、”.”が含まれていない場合はInt型を戻り値にします。
if value.contains(".") {
return Double(value) ?? 0.0
} else {
return Int(value) ?? 0
}
}
let intNumeric = createNumber("12")
print("intNumeric = \(intNumeric) and type is \(type(of: intNumeric))")
// 出力:intNumeric = 12 and type is Int
let doubleNumeric = createNumber("12.")
print("doubleNumeric = \(doubleNumeric) and type is \(type(of: doubleNumeric))”)
// 出力:doubleNumeric = 12.0 and type is Double
この実装では、プログラムが実行されてみるまで戻り値の型が定まりません。そのため戻り値some Numeric
と記述するとエラーになります。この場合は、戻り値any Numeric
と記述する必要があります。
someとany、どちらも指定しない場合は?
戻り値にsome
もany
も指定しない場合はどうなるでしょうか?次のように、先述の例のcreateNumber()
の戻り値any Numeric
をNumeric
に変更した場合です。
if value.contains(".") {
return Double(value) ?? 0.0
} else {
return Int(value) ?? 0
}
}
この場合は、コンパイルエラーになります。
じゃあ次の例でもコンパイルエラーになるかというと、この場合は問題なくコンパイルできます。
}
struct IntImplementation : A {
}
struct StringImplementation : A {
}
func returnProtocolA(_ name: String) -> A {
if name == "IntImplementation" {
return IntImplementation()
} else {
return StringImplementation()
}
}
これは、オブジェクト指向言語でよく見られる多様性(Polymorphism)を活かした実装です。
先ほどのreturnn Numeric
だとエラーで、自作プロトコルA
だと問題ない理由。それは型情報が関連付けられているかどうかによります。
通常、クラス継承やプロトコル実装は、呼び出しの互換性が保たれるものですが、ジェネリクス型など型情報が関連付けられたクラスやインスタンスは、継承元が同じでも呼び出しの互換性がなくなってしまいます。
先ほどのサンプルコードに、associatedtype
やtypealias
を使って、型情報を指定してみましょう。(associatedtype
の詳細はSwift Programming Language > Generics > Associated Typesを参照)
associatedtype T
}
struct IntImplementation : A {
typealias T = Int
}
struct DoubleImplementation : A {
typealias T = Double
}
func returnProtocolA(_ name: String) -> A { // コンパイルエラー(anyが必要になる)
if name.contains(".") {
return DoubleImplementation()
} else {
return IntImplementation()
}
}
この場合IntImplementation
クラスにはInt
型が関連付けられ、DoubleImplementation
クラスにはDouble
型が関連付けられるため、それぞれ互換性なしとみなされて、一般的なクラス継承による多様性が通用しなくなり、クラス継承としては互換性があるけれど関連付けられた型の互換性はないよということを示すany
を付ける必要があります。
コンパイルエラーにならないreturnProtocolA(name)
関数の定義:
if name.contains(".") {
return DoubleImplementation()
} else {
return IntImplementation()
}
}
anyじゃなくてsome使うと何かいいことあるのか?
some
は1つの型に特定できる場合に使い、any
は1つの型に特定できない場合に使います。
つまりsome
よりany
のほうがゆるい感じのキーワードになります。
関数の戻り値定義でsome
が使えるところであれば、any
を使ってもコンパイルは通ります。
じゃあany
だけ使っていればいいじゃないかというと、そうでもありません。逆にsome
を使わないと不都合が出る場面もあります。
先ほどのサンプルコードで紹介したcreateIntNumeric()
関数は、戻り値をsome Numeric
と定義していたわけですが、実装は常にInt(1)
を返すため、コンパイル時にsome Numeric
というプログラムがInt
に置き換えられます。
return 1
}
let n1 = createIntNumeric()
let n2 = createIntNumeric()
n1 == n2 // 出力:true(問題なく比較できる)
戻り値がInt
であることが保証されているので、比較演算も成り立ちます。
ではcreateNumeric()
関数の戻り値型をany Numeric
に変えるとどうなるでしょうか?Int
型同士の比較演算がされるという保証がなくなってしまうため、コンパイルエラーになります。
return 1
}
let n1 = createIntNumeric()
let n2 = createIntNumeric()
n1 == n2 // コンパイルエラー(any Numeric同士の比較はできない)
あとがき
いやー、難しい。ケンヂまるは15時間ほどの長旅をしてようやく、理解できた気がします。
イカしたアプリ作るのが目的なのにsome
だany
だに数日間費やすのはどうかと思いましたが、頑張って理解したことでモヤモヤがとれました。こういうのは費やした時間以上に得るものが大きいので、これからも積極的に遠回りしていこうと思います。