WWDC Quick Look 💓 By SwiftGGTeam
Embrace Swift generics

Embrace Swift generics

元の動画を見る

ハイライト

Swift 5.7 により、汎用コードの記述と読み取りが容易になります。 Holly は、ファーム シミュレーターの例を使用して、具象型から関数のオーバーロード、クラスの継承、プロトコルやジェネリックに至るまでの完全な抽象化プロセスを示し、次のように紹介しました。someそしてanyパラメーターとコレクションをさまざまに使用するための 2 つの新しい構文。

主要内容

ジェネリックは、コードの複雑さを管理するために不可欠なツールです。ホリーはファーム シミュレーターを全体的に使用して、具体的なコードから抽象的なコードへの完全な進化を実証しました。 (00:17)

出発点は単純です。Cow構造体、Hay構造体、Alfalfa構造体、Farm struct。Cow.eat受け入れるHayHay.grow戻るAlfalfaAlfalfa.harvest戻るHayFarm.feedまずアルファルファを植え、次に干し草を収穫し、最後に牛に餌を与えます。 (01:48)

さらに動物を追加したい場合、最も直接的な方法は、関数をオーバーロードすることです。Farm追加feed(horse:)feed(chicken:)。ただし、各オーバーロードの実装はほとんど同じで、型が異なるだけです。オーバーロードが多すぎると定型コードになってしまいます。 (02:46)

次にクラス継承を試してみます: はじめにAnimal基本クラス、CowHorseChickenそれを継承します。しかし、すぐに問題が発生します。それぞれの動物が異なる餌を食べます。Animal.eatパラメーターの型について何かを書くのは面倒です。書くAny型の安全性が失われ、実行時に間違ったフードが渡される可能性があります。ジェネリック クラス パラメータを記述すると、すべての参照が発生します。Animal食品タイプのパラメータをあらゆる場所に指定する必要があり、これは非常に面倒です。 (05:04)

プロトコルはより優れた抽象化ツールです。Animalプロトコルには関連付けられたタイプがありますFeedそして方法eat(_ food: Feed)。それぞれの特定の動物タイプが一致しますAnimalコンパイラによって自動的に推論されるプロトコルFeedアソシエーションタイプ。Coweat受け入れるHay、コンパイラはそれを認識しますCow.Feed == Hay。(08:44

Swift 5.7が導入されましたsomeキーワードを使用すると、ジェネリック パラメーターの宣言がより簡潔になります。func feed(_ animal: some Animal)where 句を使用した明示的なジェネリック パラメーターと同等ですが、構文がより簡潔になります。some「プロトコルに準拠した特定の型」を表します。基礎となる型は呼び出し時に決定され、その後は変更されません。 (12:52)

さまざまな種類の動物を保存する必要がある場合 (たとえば、配列内に牛と馬の両方が存在する場合)、次を使用します。someいや、だってsome基礎となる型を修正する必要があります。この時間を利用してくださいany[any Animal]「動物プロトコルに準拠する任意のタイプ」を表します。any存在型であり、基になる型は実行時に変更される可能性があります。 (21:04)

any Animalそしてsome Animalの間で自動的に変換できます。存在するfeedAllメソッドでは、トラバースします[any Animal]、各要素は自動的にアンボックス化されます。some Animalに渡すfeedこのような方法feed関連する型は引き続き内部で使用できます。 (23:00)

ホリーの提案は次のとおりです: デフォルトで書くsome、任意の型を保存する必要がある場合にのみ、代わりに使用してください。any。これは、「デフォルトでは let が使用され、変更が必要な場合は var が使用される」という考え方と同じです。 (26:12)

詳細

具体から抽象へ: 繰り返しのパターンを特定する

(01:48) 最初のコードには特定のタイプのみがあります。

struct Cow {
    func eat(_ food: Hay) {}
}

struct Hay {
    static func grow() -> Alfalfa {
        Alfalfa()
    }
}

struct Alfalfa {
    func harvest() -> Hay {
        Hay()
    }
}

struct Farm {
    func feed(_ cow: Cow) {
        let crop = Hay.grow()
        let produce = crop.harvest()
        cow.eat(produce)
    }
}

キーポイント:

  • それぞれのタイプには明確な責任があります: 牛が食べる、干し草が育つ、アルファルファを収穫する、農場の飼料を与える
  • しかしfeed餌を与えられるのは牛のみなので、馬と鶏を追加するには書き直す必要があります

関数のオーバーロードの試み:

struct Farm {
    func feed(_ cow: Cow) { ... }
    func feed(_ horse: Horse) { ... }
    func feed(_ chicken: Chicken) { ... }
}

キーポイント:

  • 各オーバーロードの実装はほぼ同じです
  • 新しい動物タイプを追加するには、新しいオーバーロードを追加する必要があります
  • これは「アドホックなポリモーフィズム」であり、一般的な解決策ではありません

クラス継承がこのシナリオに適さない理由

(05:04) クラス継承を使用してみてください。

class Animal {
    func eat(_ food: Any) {} // 问题:参数类型写什么?
}

class Cow: Animal {
    override func eat(_ food: Any) {
        guard let hay = food as? Hay else { ... }
        // 喂干草
    }
}

キーポイント:

  • 参照セマンティクスを強制しますが、動物インスタンスは状態を共有する必要はありません
  • サブクラスはオーバーライドする必要があります。忘れた場合は、実行時にのみ見つかります。
  • Anyパラメータは型の安全性を失い、実行時にチェックされます。
  • ジェネリッククラスパラメータを使用する場合Animal<Food>、すべての参照には型パラメータが必要です

プロトコルを使用して機能インターフェイスを定義する

(08:44) このプロトコルは、「動物が食べ物を食べる」ことをインターフェイスに抽象化します。

protocol Animal {
    associatedtype Feed: AnimalFeed
    func eat(_ food: Feed)
}

protocol AnimalFeed {
    associatedtype CropType: Crop where CropType.Feed == Self
    static func grow() -> CropType
}

protocol Crop {
    associatedtype Feed: AnimalFeed where Feed.CropType == Self
    func harvest() -> Feed
}

具体的なタイプは次のプロトコルに準拠します。

struct Cow: Animal {
    func eat(_ food: Hay) {}
}

struct Hay: AnimalFeed {
    static func grow() -> Alfalfa {
        Alfalfa()
    }
}

struct Alfalfa: Crop {
    func harvest() -> Hay {
        Hay()
    }
}

キーポイント:

  • アソシエーションタイプFeed特定の適合タイプに依存します
  • Cow.eat受け入れるHay、コンパイラは推測しますCow.Feed == Hay
  • プロトコルはクラス、構造体、列挙型、アクターに限定されず、すべて準拠可能
  • コンパイラは、適合する各型がすべての要件を実装していることをチェックします。

いくつかのキーワード: 汎用パラメータを簡素化する

(12:52) 従来の一般的な記述:

func feed<A: Animal>(_ animal: A) {
    let crop = type(of: animal).Feed.grow()
    let produce = crop.harvest()
    animal.eat(produce)
}

Swift 5.7 の簡略化された記述:

func feed(_ animal: some Animal) {
    let crop = type(of: animal).Feed.grow()
    let produce = crop.harvest()
    animal.eat(produce)
}

キーポイント:

  • some Animal明示的な汎用パラメータと同等A: Animal
  • some「具体的なプロトコル準拠型」を表します。
  • 基礎となる型は固定されており、コンパイラは関数本体で関連する型を使用できます。
  • パラメーターの位置と戻り位置 (SwiftUI など) に適用されます。some View

明示的な型パラメータが必要になるのはどのような場合ですか?署名内で同じ型を複数回参照する必要がある場合:

func buildHabitat<A: Animal>(for animal: A) -> A.Habitat {
    // 参数类型和返回类型都依赖 A
}

任意のキーワード: 任意のタイプを保存

21:04some基礎となる型を修正する必要があるため、[some Animal]配列には同じ動物のみを保存できます。混合型を保存するには、次を使用します。any

struct Farm {
    func feed(_ animal: some Animal) {
        let crop = type(of: animal).Feed.grow()
        let produce = crop.harvest()
        animal.eat(produce)
    }

    func feedAll(_ animals: [any Animal]) {
        for animal in animals {
            feed(animal) // 自动解包
        }
    }
}

キーポイント:

  • any Animalこれは既存のタイプであり、Animal に準拠する特定のタイプを格納できます。
  • 型消去により、具体的な型間の静的な型の区別が排除されます。
  • しかしany Animal基になる型が固定されていないため、関連付けられた型に直接アクセスできません
  • Swift 5.7 は自動解凍をサポートしています。any Animalパラメータを渡すことができますsome Animalパラメータ
  • 開梱後、feed関数本体内では、基になる型は固定されており、関連する型は通常どおり使用できます。

someそしてany比較:

特長いくつか任意
基礎となるタイプ修正済み変数
関連タイプ利用可能利用できません (開梱されていない場合)
異種コレクションサポートされていませんサポートされている
オプションの値基礎となる型を指定する必要があります直接使用可能any Animal?
パフォーマンス型なし消去のオーバーヘッド型付き消去のオーバーヘッド

完全な例

(27:10) すべての概念を組み合わせた完全なコード:

protocol AnimalFeed {
    associatedtype CropType: Crop where CropType.Feed == Self
    static func grow() -> CropType
}

protocol Crop {
    associatedtype Feed: AnimalFeed where Feed.CropType == Self
    func harvest() -> Feed
}

protocol Animal {
    associatedtype Feed: AnimalFeed
    func eat(_ food: Feed)
}

struct Farm {
    func feed(_ animal: some Animal) {
        let crop = type(of: animal).Feed.grow()
        let produce = crop.harvest()
        animal.eat(produce)
    }

    func feedAll(_ animals: [any Animal]) {
        for animal in animals {
            feed(animal)
        }
    }
}

struct Cow: Animal {
    func eat(_ food: Hay) {}
}

struct Hay: AnimalFeed {
    static func grow() -> Alfalfa {
        Alfalfa()
    }
}

struct Alfalfa: Crop {
    func harvest() -> Hay {
        Hay()
    }
}

キーポイント:

  • AnimalFeedそしてCrop関連付けられたタイプは、飼料と作物が確実に一致するように相互に制約します。
  • Farm.feed使用some Animal個々の動物に餌を与える
  • Farm.feedAll使用[any Animal]混合動物グループに餌を与える
  • コンパイラは、間違った食べ物を与えないことを保証します。Hay食べに行くCornChicken

重要ポイント

  • 対処方法: プロジェクト内の関数のオーバーロードを確認し、重複した実装を汎用関数に変更します。 実行する価値がある理由: ホリーの農場の例、3 匹の動物feedオーバーロードされた実装もほぼ同じです。に変更しますfunc feed(_ animal: some Animal)その後、Sheepタイプを変更する必要はありませんFarm任意のコード。 開始方法: プロジェクト内で同様のオーバーロード関数を検索し、それらの操作の共通機能 (呼び出しなど) を見つけます。eatメソッド)、それをプロトコルに抽出し、使用しますsomeパラメータの汎用バージョンを作成します。

  • やるべきこと: を使用しますsome明示的な汎用パラメータを置き換えて、コードを読みやすくします。 実行する価値がある理由:func feed(_ animal: some Animal)比較するfunc feed<A: Animal>(_ animal: A)半分の時間で、セマンティック情報がパラメータ宣言に直接表示されます。 Holly 氏は、これは Swift 5.7 でよくあるパターンであると強調しました。 開始方法: プロジェクト内の汎用関数を調べます。 type パラメータがパラメータ リストに 1 回しか表示されない場合は、次のように変更します。some文法。型パラメーターがパラメーター型と戻り値の型の両方に出現する場合、明示的なパラメーターは保持されます。

  • やるべきこと: を使用しますany異種混合のコレクションを構築しても、できるだけ早く解凍して戻すsome
    実行する価値がある理由:[any Animal]混合型を格納することは可能ですが、内部操作で関連する型が必要な場合、型の消去が邪魔になります。ホーリーさんfeedAllベスト プラクティスを示します: を使用したコレクションany、要素を横断するときに受信者に渡されます。some関数本体内の完全な型機能を復元する関数。 開始方法: プロジェクトの確認に使用しますany(または型が暗黙的に存在する場合)、関連付けられた型にアクセスする必要があるコードを見つけて、操作を accept に抽出します。some関数を使用して、コンパイラーが自動的に解凍できるようにします。

関連セッション

コメント

GitHub Issues · utterances