Swift

Swiftのsome、any、指定しない場合の使い分け

Swift UIの開発で多くの人がよくわからないまま使っているであろうsomeキーワード。someと並んで紹介されるanyキーワード。
オブジェクト指向経験者なら1度は考えが浮かぶ、「someanyも記述しなくても問題なくない?」という疑問。Swiftに詳しくない人が調べるとほぼ深みにハマると思うので、ケンヂまるが代わりに深みにハマって調べてきた情報を、いい感じにまとめました。

想定読者は、someanyについて調べる前のモヤモヤしながらSwiftUIのプログラムを書いていた頃の自分です。1ヶ月ほど前の自分に向けた記事にしました。SwiftUIのサンプルコードをいくつか実行させて雰囲気はわかっているけどよく理解できていないんだよねって方に読んでもらえると、何か得るものがあるかもしれません。

まずは「まとめ」から

時間に追われている人も多いと思うので、someanyの使い分け方から。考えるより感じる派の人はこれで十分だと思います。

コンパイル時に戻り値が1種類に特定できる場合はsome

関数の実装内容から、コンパイラが「戻り値は常にInt型やな」と判断できる場合は戻り値型にsomeを付けます。

func createIntNumeric() -> some Numeric {
    return Int(1)
}

コンパイル時に戻り値が1種類に特定できない場合はany

プログラムを実行してみるまで戻り値の型が定まらない場合は戻り値型にanyを付けます。

func createNumber(_ value: String) -> any Numeric {
    if value.contains(".") {
        return Double(value) ?? 0.0
    } else {
        return Int(value) ?? 0
    }
}

型情報が関連付けられていない場合はsomeもanyも不要

型情報が関連付いていないプロトコルやクラスに対しては、通常の多様性(Polymorphism)が問題なく運用できるため、someanyも不要です。

protocol A {
}

struct IntImplementation : A {
}

struct StringImplementation : A {
}

func returnProtocolA(_ name: String) -> A {
   if name == "IntImplementation" {
       return IntImplementation()
   } else {
       return StringImplementation()
   }
}

次の章からは、someany、どちらも使わない場合の具体的な部分を解説していきます。

someキーワード

some、アイツとの初めての出会いは、それはSwiftUIの勉強で最初に出くわすサンプルコード。いきなり出てきて挫折させようとしてくる。正直最初は、なんて嫌なヤツなんだって思ったよ。」とケンヂまるさん。(自分の記事に自分が登場して語っていく新しいスタイル)

@main
struct MyApp: App {
   var body: some Scene {
       WindowGroup {
           Text("Hello, world!")
       }
   }
}

「だけど、someとちゃんと向き合おうと思ったんだ。そして俺はsome探しの旅に出た。長い長い、旅だった。」とケンヂまるさんは続けます。(自分で語るスタイルもうええわ)

旅という表現は間違っていなくて、someはSwift言語に馴染んでいないと理解できない部分だと思います。深掘りするには、しばらくはSwift UIの勉強はおあずけくらう覚悟が必要です。

本題に戻ってsomeの使い方ですが、抽象型を使ってはいるものの、コンパイルされる時には1つの特定の型で定まるような場合に使います。

例えばNumericプロトコル型を戻り値に返すcreateInteger()関数を定義する場合、実際の戻り値はIntかもしれないし、Doubleかもしれないことになります。
IntDoubleNumericプロトコルを実装しているため)

func createInteger() -> some Numeric {
    // 関数の実際の実装
}

しかし実際の関数実装がIntしか戻り値として返さないことが保証できる場合があります。
そういった場合は、戻り値をsome Numericと記述することができます。

func createInteger() -> some Numeric {
    return Int(12)
}

Intしか戻り値に返さないなら、そもそも戻り値はIntと書けばいいじゃないかと思うかもしれません。次のような感じで。

func createInteger() -> Int {
    return Int(12)
}

確かに自分でシグネチャを決めれる状況ではそうなんですけど、Intを返してもいいしDoubleを返してもいいし、とにかく共通のプロトコルであるNumericなら許容できるぜってフレームワークを作る場合なんかは、some Numericのようにプロトコル定義しておいて、フレームワークを使って処理を実装する人が特定の1つの型だけを戻り値として返すように記述する、といった状況でsome Numericのように定義する価値が出てきます。

SwiftUIもフレームワークなので、同じことが言えます。ここでもう一度SwiftUIのサンプルコードを見てみましょう。

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            Text("Hello, world!")
        }
    }
}

SwiftUIフレームワークは、@main属性が付けられた、Appプロトコル実装の構造体がエントリポイントになります。この例ではMyApp構造体がエントリポイントです。

MaApp構造体は、Appプロトコルの実装としてbodyというcomputed propertyを我々アプリ開発者が実装します。そして、フレームワークとしてのbodyプロパティの型はSceneプロトコルにしてね、とルール化されています。

そのルールに則って、bodyプロパティの中でWindowGroupを作成する実装を考えてみましょう。(WindowGroupSceneプロトコルの実装型です)

次の例ではbodyの型はsome Sceneと記述しているものの、実際はWindowGroupとしてコンパイルされます。

import SwiftUI

@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>になります。

import SwiftUI

@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型を戻り値にします。

func createNumber(_ value: String) -> any Numeric {
    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、どちらも指定しない場合は?

戻り値にsomeanyも指定しない場合はどうなるでしょうか?次のように、先述の例のcreateNumber()の戻り値any NumericNumericに変更した場合です。

func createNumber(_ value: String) -> Numeric { // コンパイルエラー
    if value.contains(".") {
        return Double(value) ?? 0.0
    } else {
        return Int(value) ?? 0
    }
}

この場合は、コンパイルエラーになります。

じゃあ次の例でもコンパイルエラーになるかというと、この場合は問題なくコンパイルできます。

protocol A {
}

struct IntImplementation : A {
}

struct StringImplementation : A {
}

func returnProtocolA(_ name: String) -> A {
    if name == "IntImplementation" {
        return IntImplementation()
    } else {
        return StringImplementation()
    }
}

これは、オブジェクト指向言語でよく見られる多様性(Polymorphism)を活かした実装です。

先ほどのreturnn Numericだとエラーで、自作プロトコルAだと問題ない理由。それは型情報が関連付けられているかどうかによります。

通常、クラス継承やプロトコル実装は、呼び出しの互換性が保たれるものですが、ジェネリクス型など型情報が関連付けられたクラスやインスタンスは、継承元が同じでも呼び出しの互換性がなくなってしまいます。

先ほどのサンプルコードに、associatedtypetypealiasを使って、型情報を指定してみましょう。(associatedtypeの詳細はSwift Programming Language > Generics > Associated Typesを参照)

protocol A {
    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)関数の定義:

func returnProtocolA(_ name: String) -> any A {
    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に置き換えられます。

func createIntNumeric() -> some Numeric {
    return 1
}

let n1 = createIntNumeric()
let n2 = createIntNumeric()

n1 == n2 // 出力:true(問題なく比較できる)

戻り値がIntであることが保証されているので、比較演算も成り立ちます。

ではcreateNumeric()関数の戻り値型をany Numericに変えるとどうなるでしょうか?Int型同士の比較演算がされるという保証がなくなってしまうため、コンパイルエラーになります。

func createIntNumeric() -> any Numeric {
    return 1
}

let n1 = createIntNumeric()
let n2 = createIntNumeric()

n1 == n2 // コンパイルエラー(any Numeric同士の比較はできない)

あとがき

いやー、難しい。ケンヂまるは15時間ほどの長旅をしてようやく、理解できた気がします。

イカしたアプリ作るのが目的なのにsomeanyだに数日間費やすのはどうかと思いましたが、頑張って理解したことでモヤモヤがとれました。こういうのは費やした時間以上に得るものが大きいので、これからも積極的に遠回りしていこうと思います。

-Swift

© 2024 ヂまるBlog