WWDC Quick Look 💓 By SwiftGGTeam
Design protocol interfaces in Swift

Design protocol interfaces in Swift

元の動画を見る

ハイライト

Swift 5.7 では、関連付けられた型を持つプロトコルを既存のプロトコルで使用できるようになります。any型は直接相互作用し、不透明な結果型は主な関連付け型制約をサポートします。where句内の同じ型の要件により、複数の具体的な型間の等価関係を正確にモデル化できます。

主要内容

文字の消去から始めましょう

鶏や牛など、さまざまな動物がいる農場があります。鶏は卵を産み、牛は乳を出します。統一されたものを使用しますか?Animalしかし、それぞれの動物は異なる種類の食物を生産します。

以前のアプローチでは、関連する型を使用してこの違いをモデル化していました。ChickenCommodityTypeはいEggCowCommodityTypeはいMilk

しかし、これらの動物を異種混合の配列に入れると、any Animalここで問題が発生します。電話produce()コンパイラはメソッドによって返された関連型をどのように処理すべきでしょうか?

(01:14) Swift 5.7 では、関連付けられたタイプの消去が導入されています。関連する型がメソッドの生成位置に現れると、コンパイラーはそれをその上限まで型消去します。

protocol Food { }
protocol Animal {
    associatedtype CommodityType: Food
    func produce() -> CommodityType
}

struct Farm {
    var animals: [any Animal]

    func produceCommodities() -> [any Food] {
        animals.map { animal in
            animal.produce()  // 返回类型被擦除为 any Food
        }
    }
}

キーポイント:

  • animal.produce()の戻り値の型は関連付けられた型ですCommodityType
  • 存在するany Animal呼び出されると、コンパイラはCommodityTypeとして消去any Food
  • 関連する型が生成位置に表示されるため、これは Swift 5.7 の新機能です

逆に、関連する型がパラメータを消費する位置にある場合、型の消去は安全ではありません。

protocol Animal {
    associatedtype FeedType: AnimalFeed
    func eat(_ food: FeedType)  // consuming position
}

let animal: any Animal = Cow()
// animal.eat(someHay)  // 编译错误!

05:34any AnimalFeedコンクリートに変えるのは安全ではありませんHay具体的な型はコンパイル時には不明であるためです。現時点では、実存情報を不透明に解凍する必要がありますsomeタイプ。

不透明な結果タイプと主な関連タイプ

(07:54)hungryAnimals属性の場合lazy.filter一時的な配列割り当てを回避するために実装されています。ただし、戻り値の型によって実装の詳細が明らかになります。LazyFilterSequence<Array<any Animal>>

Swift 5.7 では、制約付きの不透明な結果型が導入されており、十分な型情報を公開しながら特定の型を非表示にすることができます。

struct Farm {
    var animals: [any Animal]

    var hungryAnimals: some Collection<any Animal> {
        animals.lazy.filter { $0.isHungry }
    }

    func feedAnimals() {
        for animal in hungryAnimals {
            // animal 的类型是 any Animal,可以调用 Animal 协议的方法
            animal.eat(...)
        }
    }
}

キーポイント:

  • some Collection<any Animal>隠れたLazyFilterSequence特定の種類の
  • ただし、クライアントは要素タイプがany Animal
  • これはによって異なりますCollectionプロトコルはプライマリ関連タイプを宣言しますElement

独自のプロトコルでプライマリ アソシエーション タイプを宣言することもできます。

protocol Container<Element> {
    associatedtype Element
    var count: Int { get }
}

同じタイプの要件モデリングタイプの関係

(14:54) 場面はさらに複雑になりました。動物に餌を与える前に、作物を植え、収穫し、飼料に加工する必要があります。牛はアルファルファから加工された干し草を食べます。

あなたが定義したAnimalFeedそしてCrop2 つのプロトコル。それぞれに他方のタイプが関連付けられています。しかし、このように書くと問題があります。

protocol AnimalFeed {
    associatedtype CropType: Crop
    static func grow() -> CropType
}

protocol Crop {
    associatedtype FeedType: AnimalFeed
    func harvest() -> FeedType
}

(19:40) コンパイラによって推定される型は次のとおりです。(some Animal).FeedType.CropType.FeedType、そしてeat()メソッドが期待するのは、(some Animal).FeedType。タイプが一致しません。

根本的な理由は、協定の定義が広すぎることと、栽培した作物を加工した後に本来の飼料タイプが得られる保証がないことにある。

Swift 5.7 の同型要件により、この問題は解決されます。

protocol AnimalFeed {
    associatedtype CropType: Crop
    static func grow() -> CropType
}

protocol Crop {
    associatedtype FeedType: AnimalFeed
    func harvest() -> FeedType
}

extension AnimalFeed where Self.CropType.FeedType == Self { }
extension Crop where Self.FeedType.CropType == Self { }

キーポイント:

  • Self.CropType.FeedType == Self保証: 特定の種類の飼料から栽培された作物は、加工後に同じ種類の飼料を生産します。
  • これにより、プロトコルに準拠できる特定のタイプが制限されます
  • feedAnimal()メソッドを安全に連鎖できるようになりましたgrow() -> harvest() -> eat()

詳細

型消去の方向性

Swift 5.7 の関連付けられたタイプの消去には、明確な方向ルールがあります。

// Producing position: 安全
func produce() -> CommodityType  // 返回 any Food

// Consuming position: 不安全
func eat(_ food: FeedType)       // 不能直接传入 any AnimalFeed

(04:41) 位置を生成する型変換の方向は「サブタイプから親タイプへ」であり、常に安全です。位置を消費するには「親タイプのロータータイプ」が必要ですが、コンパイラーはこれを保証できません。

### 使用some既存のものを開梱する

関連付けられた型が消費側にある場合は、ジェネリック関数または不透明型を使用してそれをアンラップする必要があります。

// 用 generic 函数
func feedAnimal<AnimalType: Animal>(_ animal: AnimalType, with feed: AnimalType.FeedType) {
    animal.eat(feed)
}

// 或者用 some
func feedAnimal(_ animal: some Animal, with feed: (some Animal).FeedType) {
    // 这里需要更精确的类型匹配
}

Constrained Existential Types

Swift 5.7 より前では、「型 X の要素を含むコレクション」を表現するには、型消去ラッパーを自分で記述する必要がありました。これで、次のように直接書くことができます。

// 以前
struct AnyCollection<Element>: Collection { /* 几十行样板代码 */ }

// Swift 5.7
func process(_ items: any Collection<MailmapEntry>) {
    for item in items {
        // item 是 MailmapEntry
    }
}

25:00any Collection<MailmapEntry>は組み込みの言語機能であり、手書き文字の消去ラッパーを必要としません。

同じ型要件の完全な例

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

protocol AnimalFeed {
    associatedtype CropType: Crop
    static func grow() -> CropType
}

protocol Crop {
    associatedtype FeedType: AnimalFeed
    func harvest() -> FeedType
}

// 关键约束
extension AnimalFeed where Self.CropType.FeedType == Self { }
extension Crop where Self.FeedType.CropType == Self { }

// 现在 feedAnimal 可以安全编译
func feedAnimal(_ animal: some Animal) {
    let crop = type(of: animal).FeedType.grow()
    let feed = crop.harvest()
    animal.eat(feed)  // 类型匹配!
}

キーポイント:

  • type(of: animal).FeedType動物に対応する飼料の種類を取得します
  • grow()戻るCropTypeharvest()戻るFeedType
  • 同型要件保証CropType.FeedType == Selfしたがって、最終的な型は次のようになります。animal.FeedType

重要ポイント

1.使用any代替の手書きタイプの消去ラッパー

以前に契約のために書かれたものAnyXxxラッパーは、Swift 開発における一般的な問題点です。スイフト5.7any文法によってこれは過去のものになります。プロジェクト内の型消去クラスをチェックして、どのクラスが直接置換できるかを確認してください。any SomeProtocol<SomeType>

入り口:any Collection<Element>any Publisher<Output, Failure>

2.カスタム収集プロトコルのメインの関連付けタイプを宣言します

次のようなものを定義するとContainerDataSourceこのようなプロトコルの場合は、プライマリ関連型宣言をプロトコルに追加します。

protocol DataSource<Item> {
    associatedtype Item
    func item(at index: Int) -> Item
}

このようにして、呼び出し側は次のように書くことができますsome DataSource<User>またはany DataSource<User>、実装を非表示にし、型情報を保持します。

3.同じタイプの要件を使用して双方向の関係をモデル化

データ モデルに「A は B に関連付けられ、B は A に関連付けられる」という循環関係がある場合は、一貫性を確保するために同じタイプの要件を使用します。典型的なシナリオ:

  • データベース リレーショナル モデル (テーブル -> 列 -> テーブル)
  • グラフ構造のエッジとノード
  • コンパイラでの AST ノードの関係

入り口:where Self.Other.AssociatedType == Self

関連セッション

コメント

GitHub Issues · utterances