このページは Cloud Translation API によって翻訳されました。
Switch to English

プロトコル指向プログラミングとジェネリック

TensorFlow.orgで表示 GoogleColabで実行 GitHubでソースを表示

このチュートリアルでは、プロトコル指向プログラミングと、それらをジェネリックスで使用する方法のさまざまな例を日常の例で説明します。

プロトコル

継承は、プログラミング言語でコードを整理するための強力な方法であり、プログラムの複数のコンポーネント間でコードを共有できます。

Swiftでは、継承を表現するさまざまな方法があります。あなたはすでに他の言語からのそれらの方法の1つに精通しているかもしれません:クラス継承。ただし、Swiftには別の方法があります。プロトコルです。

このチュートリアルでは、プロトコルについて説明します。これは、さまざまなトレードオフを通じて同様の目標を達成できるようにするサブクラス化の代替手段です。 Swiftでは、プロトコルには複数の抽象メンバーが含まれています。クラス、構造体、列挙型は複数のプロトコルに準拠でき、準拠関係は遡及的に確立できます。これらすべてにより、サブクラス化を使用してSwiftで簡単に表現できないいくつかの設計が可能になります。プロトコルの使用をサポートするイディオム(拡張機能とプロトコル制約)、およびプロトコルの制限について説明します。

Swift💖の値型!

参照セマンティクスを持つクラスに加えて、Swiftは値によって渡される列挙型と構造体をサポートします。列挙型と構造体は、クラスによって提供される多くの機能をサポートします。見てみましょう!

まず、列挙型がクラスにどのように似ているかを見てみましょう。

enum Color: String {
    case red = "red"
    case green = "green"
    case blue = "blue"
    // A computed property. Note that enums cannot contain stored properties.
    var hint: String {
        switch self {
            case .red:
                return "Roses are this color."
            case .green:
                return "Grass is this color."
            case .blue:
                return "The ocean is this color."
        }
    }

    // An initializer like for classes.
    init?(color: String) {
        switch color {
        case "red":
            self = .red
        case "green":
            self = .green
        case "blue":
            self = .blue
        default:
            return nil
        }
    }
}

// Can extend the enum as well!
extension Color {
    // A function.
    func hintFunc() -> String {
        return self.hint
    }
}

let c = Color.red
print("Give me a hint for c: \(c.hintFunc())")

let invalidColor = Color(color: "orange")
print("is invalidColor nil: \(invalidColor == nil)")
Give me a hint for c: Roses are this color.
is invalidColor nil: true

それでは、構造体を見てみましょう。構造体を継承することはできませんが、代わりにプロトコルを使用できることに注意してください。

struct FastCar {
    // Can have variables and constants as stored properties.
    var color: Color
    let horsePower: Int
    // Can have computed properties.
    var watts: Float {
       return Float(horsePower) * 745.7
    }
    // Can have lazy variables like in classes!
    lazy var titleCaseColorString: String = {
        let colorString = color.rawValue
        return colorString.prefix(1).uppercased() + 
               colorString.lowercased().dropFirst()
    }()
    // A function.
    func description() -> String {
        return "This is a \(color) car with \(horsePower) horse power!"
    }
    // Can create a variety of initializers.
    init(color: Color, horsePower: Int) {
        self.color = color
        self.horsePower = horsePower
    }
    // Can define extra initializers other than the default one.
    init?(color: String, horsePower: Int) {
        guard let enumColor = Color(color: color) else {
            return nil
        }
        self.color = enumColor
        self.horsePower = horsePower
    }
}

var car = FastCar(color: .red, horsePower: 250)
print(car.description())
print("Horse power in watts: \(car.watts)")
print(car.titleCaseColorString)
This is a red car with 250 horse power!
Horse power in watts: 186425.0
Red

最後に、クラスとは異なり、値型がどのように渡されるかを見てみましょう。

// Notice we have no problem modifying a constant class with 
// variable properties.
class A {
  var a = "a"
}

func foo(_ a: A) {
  a.a = "foo"
}
let a = A()
print(a.a)
foo(a)
print(a.a)

/* 
Uncomment the following code to see how an error is thrown.
Structs are implicitly passed by value, so we cannot modify it.
> "error: cannot assign to property: 'car' is a 'let' constant"
*/

// func modify(car: FastCar, toColor color: Color) -> Void {
//   car.color = color
// }

// car = FastCar(color: .red, horsePower: 250)
// print(car.description())
// modify(car: &car, toColor: .blue)
// print(car.description())

a
foo

プロトコルを使いましょう

さまざまな車のプロトコルを作成することから始めましょう。

protocol Car {
    var color: Color { get set }
    var price: Int { get }
    func turnOn()
    mutating func drive()
}

protocol Electric {
    mutating func recharge()
    // percentage of the battery level, 0-100%.
    var batteryLevel: Int { get set }
}

protocol Gas {
    mutating func refill()
    // # of liters the car is holding, varies b/w models.
    var gasLevelLiters: Int { get set }
}

オブジェクト指向の世界(多重継承なし)では、 ElectricクラスとGas抽象クラスを作成し、クラス継承を使用して両方をCarから継承し、特定の車モデルを基本クラスにすることができます。ただし、ここでは両方とも、カップリングがゼロの完全に別個のプロトコルです。これにより、システム全体の設計方法がより柔軟になります。

テスラを定義しましょう:

struct TeslaModelS: Car, Electric {
    var color: Color // Needs to be a var since `Car` has a getter and setter.
    let price: Int
    var batteryLevel: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Self driving engaged!")
        batteryLevel -= 8
    }

    mutating func recharge() {
        print("Recharging the battery...")
        batteryLevel = 100
    }
}

var tesla = TeslaModelS(color: .red, price: 110000, batteryLevel: 100)

これは、プロトコルCarElectric両方に準拠する新しい構造体TeslaModelSを指定します。

それでは、ガス動力車を定義しましょう。

struct Mustang: Car, Gas{
    var color: Color
    let price: Int
    var gasLevelLiters: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Time to drive!")
        gasLevelLiters -= 1
    }

    mutating func refill() {
        print("Filling the tank...")
        gasLevelLiters = 25
    }
}

var mustang = Mustang(color: .red, price: 30000, gasLevelLiters: 25)

デフォルトの動作でプロトコルを拡張する

例からわかるのは、冗長性があることです。電気自動車を充電するたびに、バッテリーのパーセンテージレベルを100に設定する必要があります。すべての電気自動車の最大容量は100%ですが、ガス車はガスタンクの容量によって異なるため、電気自動車のレベルをデフォルトで100に設定できます。 。

ここで、Swiftの拡張機能が役立ちます。

extension Electric {
    mutating func recharge() {
        print("Recharging the battery...")
        batteryLevel = 100
    }
}

そのため、新しい電気自動車を作成すると、充電時にバッテリーが100に設定されます。したがって、クラス、構造体、列挙型を一意のデフォルトの動作で装飾することができました。

プロトコルコミック

コミックを作ってくれたレイ・ヴェンダーリッヒに感謝します!

ただし、注意すべき点は次のとおりです。最初の実装では、 foo()Aデフォルトの実装として定義しますが、プロトコルでは必須にしません。したがって、 a.foo()を呼び出すと、「 A default 」が出力されます。

protocol Default {}

extension Default {
    func foo() { print("A default")}
}

struct DefaultStruct: Default {
    func foo() {
        print("Inst")
    }
}

let a: Default = DefaultStruct()
a.foo()
A default

ただし、 Afoo()必須にA 、「 Inst 」が得られます。

protocol Default {
    func foo()
}

extension Default {
    func foo() { 
        print("A default")
    }
}

struct DefaultStruct: Default {
    func foo() {
        print("Inst")
    }
}

let a: Default = DefaultStruct()
a.foo()
Inst

これは、Swiftのプロトコルで、最初の例の静的ディスパッチと2番目の例の静的ディスパッチの違いが原因で発生します。詳細については、この中程度の投稿を参照してください。

デフォルトの動作をオーバーライドする

ただし、必要に応じて、デフォルトの動作をオーバーライドすることもできます。注意すべき重要な点の1つは、これは動的ディスパッチをサポートしていないということです。

古いバージョンの電気自動車があるため、バッテリーの状態が90%に低下したとします。

struct OldElectric: Car, Electric {
    var color: Color // Needs to be a var since `Car` has a getter and setter.
    let price: Int
    var batteryLevel: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Self driving engaged!")
        batteryLevel -= 8
    }

    mutating func reCharge() {
        print("Recharging the battery...")
        batteryLevel = 90
    }
}

プロトコルの標準ライブラリの使用

Swiftのプロトコルがどのように機能するかがわかったので、標準ライブラリプロトコルを使用するいくつかの典型的な例を見てみましょう。

標準ライブラリを拡張する

Swiftにすでに存在するタイプに機能を追加する方法を見てみましょう。 Swiftの型は組み込まれていませんが、構造体として標準ライブラリの一部であるため、これは簡単に実行できます。

配列がソートされていることを確認しながら、要素の配列でバイナリ検索を試してみましょう。

extension Collection where Element: Comparable {
    // Verify that a `Collection` is sorted.
    func isSorted(_ order: (Element, Element) -> Bool) -> Bool {
        var i = index(startIndex, offsetBy: 1)

        while i < endIndex {
            // The longer way of calling a binary function like `<(_:_:)`, 
            // `<=(_:_:)`, `==(_:_:)`, etc.
            guard order(self[index(i, offsetBy: -1)], self[i]) else {
                return false
            }
            i = index(after: i)
        }
        return true
    }

    // Perform binary search on a `Collection`, verifying it is sorted.
    func binarySearch(_ element: Element) -> Index? {
        guard self.isSorted(<=) else {
            return nil
        }

        var low = startIndex
        var high = endIndex

        while low <= high {
            let mid = index(low, offsetBy: distance(from: low, to: high)/2)

            if self[mid] == element {
                return mid
            } else if self[mid] < element {
                low = index(after: mid)
            } else {
                high = index(mid, offsetBy: -1)
            }
        }

        return nil
    }
}

print([2, 2, 5, 7, 11, 13, 17].binarySearch(5)!)
print(["a", "b", "c", "d"].binarySearch("b")!)
print([1.1, 2.2, 3.3, 4.4, 5.5].binarySearch(3.3)!)
2
1
2

これを行うには、 「要素を複数回非破壊的にトラバースし、インデックス付き添え字でアクセスできるシーケンス」を定義するCollectionプロトコルを拡張します。配列は角括弧表記を使用してインデックスを付けることができるため、これは拡張したいプロトコルです。

同様に、このユーティリティ関数は、要素を比較できる配列にのみ追加する必要があります。これが、 where Element: Comparableが存在する理由です。

where句はSwiftの型システムの一部であり、これについてはすぐに説明しますが、要するに、プロトコルを実装するために型を要求する、2つの型を要求するなど、作成する拡張機能に追加の要件を追加できます。同じ、または特定のスーパークラスを持つクラスを要求する。

Elementは、 Collection準拠タイプの要素に関連付けられたタイプです。 ElementSequenceプロトコル内で定義されますが、 CollectionSequenceから継承するため、 Element関連付けられたタイプを継承します。

Comparableは、 「関係演算子<<=>= 、および>を使用して比較できる型」を定義するプロトコルです 。ソートされたCollectionに対してバイナリ検索を実行しているため、これはもちろんtrueである必要があります。そうでない場合、バイナリ検索で左または右のどちらを繰り返すか/繰り返すかがわかりません。

実装に関する補足として、使用されたindex(_:offsetBy:)関数の詳細については、次のドキュメントを参照してください。

ジェネリックス+プロトコル=💥

ジェネリックスとプロトコルは、コードの重複を避けるために正しく使用すれば、強力なツールになります。

まず、別のチュートリアルであるA Swift Tourに目を通します。このチュートリアルでは、Colabの本の最後にあるジェネリックについて簡単に説明しています。

ジェネリックスについての一般的な考え方があると仮定して、いくつかの高度な使用法を簡単に見てみましょう。

単一のタイプに複数のプロトコルに準拠するタイプなどの複数の要件がある場合、自由に使用できるいくつかのオプションがあります。

typealias ComparableReal = Comparable & FloatingPoint

func foo1<T: ComparableReal>(a: T, b: T) -> Bool {
    return a > b
}

func foo2<T: Comparable & FloatingPoint>(a: T, b: T) -> Bool {
    return a > b
}

func foo3<T>(a: T, b: T) -> Bool where T: ComparableReal {
    return a > b
}

func foo4<T>(a: T, b: T) -> Bool where T: Comparable & FloatingPoint {
    return a > b
}

func foo5<T: FloatingPoint>(a: T, b: T) -> Bool where T: Comparable {
    return a > b
}

print(foo1(a: 1, b: 2))
print(foo2(a: 1, b: 2))
print(foo3(a: 1, b: 2))
print(foo4(a: 1, b: 2))
print(foo5(a: 1, b: 2))
false
false
false
false
false

上部でtypealiasが使用されていることにtypealiasしてください。これにより、既存のタイプの名前付きエイリアスがプログラムに追加されます。タイプエイリアスが宣言された後、プログラム内のあらゆる場所で既存のタイプの代わりにエイリアス名を使用できます。型エイリアスは新しい型を作成しません。名前が既存のタイプを参照できるようにするだけです。

それでは、プロトコルとジェネリックを一緒に使用する方法を見てみましょう。

私たちが、店の裏でどのように整理するかを決定するために販売するラップトップに次の要件があるコンピューターストアであると想像してみてください。

enum Box {
    case small
    case medium
    case large
}

enum Mass {
    case light
    case medium
    case heavy
}

// Note: `CustomStringConvertible` protocol lets us pretty-print a `Laptop`.
struct Laptop: CustomStringConvertible {
    var name: String
    var box: Box
    var mass: Mass

    var description: String {
        return "(\(self.name) \(self.box) \(self.mass))"
    }
}

ただし、棚には重量制限があるため、 Laptopを質量でグループ化するという新しい要件があります。

func filtering(_ laptops: [Laptop], by mass: Mass) -> [Laptop] {
    return laptops.filter { $0.mass == mass }
}

let laptops: [Laptop] = [
    Laptop(name: "a", box: .small, mass: .light),
    Laptop(name: "b", box: .large, mass: .medium),
    Laptop(name: "c", box: .medium, mass: .heavy),
    Laptop(name: "d", box: .large, mass: .light)
]

let filteredLaptops = filtering(laptops, by: .light)
print(filteredLaptops)
[(a small light), (d large light)]

ただし、 Mass以外でフィルタリングしたい場合はどうなりますか?

1つのオプションは、次のことを行うことです。

// Define a protocol which will act as our comparator.
protocol DeviceFilterPredicate {
    associatedtype Device
    func shouldKeep(_ item: Device) -> Bool
}

// Define the structs we will use for passing into our filtering function.
struct BoxFilter: DeviceFilterPredicate {
    typealias Device = Laptop
    var box: Box 

    func shouldKeep(_ item: Laptop) -> Bool {
        return item.box == box
    }
}

struct MassFilter: DeviceFilterPredicate {
    typealias Device = Laptop  
    var mass: Mass

    func shouldKeep(_ item: Laptop) -> Bool {
        return item.mass == mass
    }
}

// Make sure our filter conforms to `DeviceFilterPredicate` and that we are 
// filtering `Laptop`s.
func filtering<F: DeviceFilterPredicate>(
    _ laptops: [Laptop], 
    by filter: F
) -> [Laptop] where Laptop == F.Device {
    return laptops.filter { filter.shouldKeep($0) }
}

// Let's test the function out!
print(filtering(laptops, by: BoxFilter(box: .large)))
print(filtering(laptops, by: MassFilter(mass: .heavy)))
[(b large medium), (d large light)]
[(c medium heavy)]

驚くばかり!これで、ラップトップの制約に基づいてフィルタリングできるようになりました。ただし、フィルタリングできるのはLaptopのみです。

箱の中にあり、質量のあるものをすべてフィルタリングできるのはどうですか?たぶん、このラップトップの倉庫は、異なる顧客ベースを持つサーバーにも使用されます。

// Define 2 new protocols so we can filter anything in a box and which has mass.
protocol Weighable {
    var mass: Mass { get }
}

protocol Boxed {
    var box: Box { get }
}

// Define the new Laptop and Server struct which have mass and a box.
struct Laptop: CustomStringConvertible, Boxed, Weighable {
    var name: String
    var box: Box
    var mass: Mass

    var description: String {
        return "(\(self.name) \(self.box) \(self.mass))"
    }
}

struct Server: CustomStringConvertible, Boxed, Weighable {
    var isWorking: Bool
    var name: String
    let box: Box
    let mass: Mass

    var description: String {
        if isWorking {
            return "(working \(self.name) \(self.box) \(self.mass))"
        } else {
            return "(notWorking \(self.name) \(self.box) \(self.mass))"
        }
    }
}

// Define the structs we will use for passing into our filtering function.
struct BoxFilter<T: Boxed>: DeviceFilterPredicate {
    var box: Box 

    func shouldKeep(_ item: T) -> Bool {
        return item.box == box
    }
}

struct MassFilter<T: Weighable>: DeviceFilterPredicate {
    var mass: Mass

    func shouldKeep(_ item: T) -> Bool {
        return item.mass == mass
    }
}

// Define the new filter function.
func filtering<F: DeviceFilterPredicate, T>(
    _ elements: [T], 
    by filter: F
) -> [T] where T == F.Device {
    return elements.filter { filter.shouldKeep($0) }
}


// Let's test the function out!
let servers = [
    Server(isWorking: true, name: "serverA", box: .small, mass: .heavy),
    Server(isWorking: false, name: "serverB", box: .medium, mass: .medium),
    Server(isWorking: true, name: "serverC", box: .large, mass: .light),
    Server(isWorking: false, name: "serverD", box: .medium, mass: .light),
    Server(isWorking: true, name: "serverE", box: .small, mass: .heavy)
]

let products = [
    Laptop(name: "a", box: .small, mass: .light),
    Laptop(name: "b", box: .large, mass: .medium),
    Laptop(name: "c", box: .medium, mass: .heavy),
    Laptop(name: "d", box: .large, mass: .light)
]

print(filtering(servers, by: BoxFilter(box: .small)))
print(filtering(servers, by: MassFilter(mass: .medium)))

print(filtering(products, by: BoxFilter(box: .small)))
print(filtering(products, by: MassFilter(mass: .medium)))
[(working serverA small heavy), (working serverE small heavy)]
[(notWorking serverB medium medium)]
[(a small light)]
[(b large medium)]

特定のstructプロパティだけでなく、そのプロパティを持つstructもフィルタリングできるようになりました。

優れたAPI設計のヒント

このセクションは、 WWDC 2019:Modern Swift APIDesignの講演から抜粋したものです。

プロトコルの動作を理解したので、プロトコルを使用する必要がある場合を確認することをお勧めします。プロトコルは可能な限り強力ですが、プロトコルに飛び込んですぐに開始することが常に最善のアイデアであるとは限りません。

  • 具体的なユースケースから始めます。
    • まず、具体的な型を使用してユースケースを調査し、共有して見つけたいコードが繰り返されていることを理解します。次に、その共有コードをジェネリックスと因数分解します。新しいプロトコルを作成することを意味する場合があります。ジェネリックコードの必要性を発見してください。
  • 標準ライブラリで定義されている既存のプロトコルから新しいプロトコルを作成することを検討してください。この良い例については、次のAppleのドキュメントを参照してください。
  • ジェネリックプロトコルの代わりに、ジェネリック型を定義することを検討してください。

例:カスタムベクタータイプの定義

3つの重要なベクトル演算を定義する作成中のジオメトリアプリで使用する浮動小数点数のGeometricVectorプロトコルを定義するとします。

protocol GeometricVector {
    associatedtype Scalar: FloatingPoint
    static func dot(_ a: Self, _ b: Self) -> Scalar
    var length: Scalar { get }
    func distance(to other: Self) -> Scalar
}

SIMDプロトコルが役立つベクトルの次元を保存したいとします。そのため、新しいタイプでSIMDプロトコルを改良します。 SIMDベクトルは、ベクトル操作を実行するために使用すると非常に高速な固定サイズのベクトルと考えることができます。

protocol GeometricVector: SIMD {
    associatedtype Scalar: FloatingPoint
    static func dot(_ a: Self, _ b: Self) -> Scalar
    var length: Scalar { get }
    func distance(to other: Self) -> Scalar
}

ここで、上記の操作のデフォルトの実装を定義しましょう。

extension GeometricVector {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a * b).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

次に、これらの機能を追加する各タイプに適合性を追加する必要があります。

extension SIMD2: GeometricVector where Scalar: FloatingPoint { }
extension SIMD3: GeometricVector where Scalar: FloatingPoint { }
extension SIMD4: GeometricVector where Scalar: FloatingPoint { }
extension SIMD8: GeometricVector where Scalar: FloatingPoint { }
extension SIMD16: GeometricVector where Scalar: FloatingPoint { }
extension SIMD32: GeometricVector where Scalar: FloatingPoint { }
extension SIMD64: GeometricVector where Scalar: FloatingPoint { }

プロトコルを定義し、それにデフォルトの実装を与え、次に複数のタイプに適合性を追加するこの3ステップのプロセスは、かなり反復的です。

プロトコルは必要でしたか?

SIMDタイプにも固有の実装がないという事実は、警告サインです。したがって、この場合、プロトコルは実際には何も提供していません。

SIMD拡張でそれを定義する

SIMDプロトコルの拡張で3つの演算子を書くと、問題をより簡潔に解決できます。

extension SIMD where Scalar: FloatingPoint {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a * b).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

より少ないコード行を使用して、すべてのタイプのSIMDすべてのデフォルト実装を追加しました。

このタイプの階層を作成したくなる場合がありますが、必ずしも必要なわけではないことを覚えておいてください。これは、コンパイルされたプログラムのバイナリサイズが小さくなり、コードのコンパイルが速くなることも意味します。

ただし、この拡張アプローチは、追加するメソッドがいくつかある場合に最適です。ただし、より大きなAPIを設計している場合は、スケーラビリティの問題が発生します。

ですか?があります?

先ほど、 GeometricVectorSIMDを改良すると述べました。しかし、これは関係ですか?問題は、 SIMDがスカラー1をベクトルに追加できる操作を定義していることですが、ジオメトリのコンテキストでそのような操作を定義することは意味がありません。

したがって、 SIMDを任意の浮動小数点数を処理できる新しいジェネリック型でラップすることで、関係が改善される可能性があります。

// NOTE: `Storage` is the underlying type that is storing the values, 
// just like in a `SIMD` vector.
struct GeometricVector<Storage: SIMD> where Storage.Scalar: FloatingPoint {
    typealias Scalar = Storage.Scalar
    var value: Storage
    init(_ value: Storage) { self.value = value }
}

次に、注意して、ジオメトリのコンテキストでのみ意味のある操作のみを定義できます。

extension GeometricVector {
    static func + (a: Self, b: Self) -> Self {
        Self(a.value + b.value)
    }

    static func - (a: Self, b: Self) -> Self {
        Self(a.value - b.value)
    }
    static func * (a: Self, b: Scalar) -> Self {
        Self(a.value * b)
    }
}

また、汎用拡張機能を使用して、以前とほぼ同じように見える、実装したい3つの以前の演算子を取得できます。

extension GeometricVector {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a.value * b.value).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

全体として、構造体を使用するだけで、3つの操作の動作をタイプに絞り込むことができました。プロトコルでは、すべてのSIMDベクトルに繰り返し準拠するという問題に直面し、 Scalar + Vectorなどの特定の演算子が使用可能になるのを防ぐこともできませんでした(この場合は必要ありませんでした)。そのため、プロトコルは万能のソリューションではないことを忘れないでください。しかし、より伝統的なソリューションがより強力であることが証明される場合があります。

より多くのプロトコル指向プログラミングリソース

議論されたトピックに関する追加のリソースは次のとおりです。

  • WWDC 2015:Swiftでのプロトコル指向プログラミング:これはSwift 2を使用して提示されたため、それ以降多くの変更がありました(たとえば、プレゼンテーションで使用したプロトコルの名前)が、これは理論の優れたリソースであり、その背後で使用されています。
  • Swift 3でのプロトコル指向プログラミングの紹介:これはSwift 3で書かれたため、正常にコンパイルするにはコードの一部を変更する必要があるかもしれませんが、これはもう1つの優れたリソースです。
  • WWDC 2019:最新のSwift API設計:値型と参照型の違いについて説明します。API設計でプロトコルがより悪い選択であることが判明する可能性がある場合のユースケース(上記の「優れたAPI設計のヒント」セクションと同じ)、キーパスメンバールックアップ、およびプロパティラッパー。
  • ジェネリック:ジェネリックに関するすべてのSwift5に関するSwift独自のドキュメント。