Lập trình hướng giao thức & thuốc generic

Xem trên TensorFlow.org Chạy trong Google Colab Xem nguồn trên GitHub

Hướng dẫn này sẽ giới thiệu về lập trình hướng giao thức và các ví dụ khác nhau về cách chúng có thể được sử dụng với các generic trong các ví dụ hàng ngày.

Giao thức

Kế thừa là một cách mạnh mẽ để tổ chức mã trong ngôn ngữ lập trình, cho phép bạn chia sẻ mã giữa nhiều thành phần của chương trình.

Trong Swift, có nhiều cách khác nhau để thể hiện sự kế thừa. Bạn có thể đã quen với một trong những cách đó, từ các ngôn ngữ khác: kế thừa lớp. Tuy nhiên, Swift có một cách khác: giao thức.

Trong hướng dẫn này, chúng ta sẽ khám phá các giao thức - một giải pháp thay thế cho việc phân lớp con cho phép bạn đạt được các mục tiêu tương tự thông qua những đánh đổi khác nhau. Trong Swift, các giao thức chứa nhiều thành viên trừu tượng. Các lớp, cấu trúc và enum có thể tuân theo nhiều giao thức và mối quan hệ tuân thủ có thể được thiết lập từ trước. Tất cả điều đó cho phép một số thiết kế không dễ diễn đạt trong Swift bằng cách sử dụng phân lớp. Chúng ta sẽ tìm hiểu các thành ngữ hỗ trợ việc sử dụng các giao thức (phần mở rộng và các ràng buộc của giao thức), cũng như các hạn chế của giao thức.

Các loại giá trị của Swift 💖!

Ngoài các lớp có ngữ nghĩa tham chiếu, Swift còn hỗ trợ các enum và cấu trúc được truyền theo giá trị. Enums và struct hỗ trợ nhiều tính năng do các lớp cung cấp. Chúng ta hãy xem xét!

Đầu tiên, chúng ta hãy xem enum tương tự như thế nào với các lớp:

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

Bây giờ, hãy nhìn vào cấu trúc. Lưu ý rằng chúng ta không thể kế thừa các cấu trúc mà thay vào đó có thể sử dụng các giao thức:

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

Cuối cùng, hãy xem cách chúng được truyền theo các loại giá trị không giống như các lớp:

// 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

Hãy sử dụng các giao thức

Hãy bắt đầu bằng cách tạo giao thức cho các ô tô khác nhau:

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 }
}

Trong thế giới hướng đối tượng (không có nhiều kế thừa), bạn có thể đã tạo các lớp trừu tượng ElectricGas sau đó sử dụng tính kế thừa lớp để kế thừa cả hai từ Car và sau đó lấy một mẫu ô tô cụ thể làm lớp cơ sở. Tuy nhiên, ở đây cả hai đều là các giao thức hoàn toàn riêng biệt và không có sự ghép nối! Điều này làm cho toàn bộ hệ thống linh hoạt hơn trong cách bạn thiết kế nó.

Hãy xác định một Tesla:

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)

Điều này chỉ định một cấu trúc TeslaModelS mới phù hợp với cả hai giao thức CarElectric .

Bây giờ hãy xác định một chiếc ô tô chạy bằng xăng:

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)

Mở rộng giao thức với hành vi mặc định

Điều bạn có thể nhận thấy từ các ví dụ là chúng ta có một số dư thừa. Mỗi lần sạc ô tô điện, chúng ta cần đặt mức phần trăm pin là 100. Vì tất cả ô tô điện đều có công suất tối đa là 100%, nhưng ô tô xăng khác nhau tùy theo dung tích bình xăng nên chúng ta có thể mặc định mức 100 cho ô tô điện .

Đây là lúc các tiện ích mở rộng trong Swift có thể hữu ích:

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

Vì vậy, bây giờ, bất kỳ chiếc ô tô điện mới nào mà chúng tôi tạo ra sẽ đặt pin ở mức 100 khi chúng tôi sạc lại. Vì vậy, chúng ta vừa có thể trang trí các lớp, cấu trúc và enum với hành vi mặc định và duy nhất.

truyện tranh giao thức

Cảm ơn Ray Wenderlich về truyện tranh!

Tuy nhiên, có một điều cần chú ý là sau đây. Trong lần triển khai đầu tiên, chúng tôi xác định foo() làm cách triển khai mặc định trên A , nhưng không bắt buộc phải có nó trong giao thức. Vì vậy, khi chúng tôi gọi a.foo() , chúng tôi nhận được " A default " được in.

protocol Default {}

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

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

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

Tuy nhiên, nếu chúng ta thực hiện yêu cầu foo() trên A , chúng ta sẽ nhận được " 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

Điều này xảy ra do sự khác biệt giữa công văn tĩnh trong ví dụ đầu tiên và công văn tĩnh trong ví dụ thứ hai trên các giao thức trong Swift. Để biết thêm thông tin, hãy tham khảo bài đăng trên Medium này.

Ghi đè hành vi mặc định

Tuy nhiên, nếu muốn, chúng ta vẫn có thể ghi đè hành vi mặc định. Một điều quan trọng cần lưu ý là tính năng này không hỗ trợ công văn động .

Giả sử chúng ta có một phiên bản cũ hơn của một chiếc ô tô điện, do đó tình trạng pin đã giảm xuống 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
    }
}

Thư viện tiêu chuẩn sử dụng các giao thức

Bây giờ chúng ta đã biết cách hoạt động của các giao thức trong Swift, hãy xem qua một số ví dụ điển hình về việc sử dụng các giao thức thư viện chuẩn.

Mở rộng thư viện chuẩn

Hãy xem cách chúng ta có thể thêm chức năng bổ sung cho các loại đã tồn tại trong Swift. Vì các kiểu trong Swift không được tích hợp sẵn nhưng là một phần của thư viện chuẩn dưới dạng cấu trúc nên việc này rất dễ thực hiện.

Hãy thử thực hiện tìm kiếm nhị phân trên một mảng các phần tử, đồng thời đảm bảo kiểm tra xem mảng đó đã được sắp xếp chưa:

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

Chúng tôi thực hiện điều này bằng cách mở rộng giao thức Bộ Collection xác định "một chuỗi có các phần tử có thể được duyệt qua nhiều lần, không bị phá hủy và được truy cập bằng chỉ số dưới được lập chỉ mục". Vì các mảng có thể được lập chỉ mục bằng ký hiệu dấu ngoặc vuông nên đây là giao thức mà chúng tôi muốn mở rộng.

Tương tự, chúng ta chỉ muốn thêm hàm tiện ích này vào các mảng có các phần tử có thể so sánh được. Đây là lý do tại sao chúng ta có where Element: Comparable .

Mệnh đề where là một phần của hệ thống kiểu của Swift mà chúng tôi sẽ sớm đề cập đến, nhưng tóm lại, hãy để chúng tôi thêm các yêu cầu bổ sung vào tiện ích mở rộng mà chúng tôi đang viết, chẳng hạn như yêu cầu loại để triển khai một giao thức, yêu cầu hai loại là giống nhau hoặc yêu cầu một lớp phải có một siêu lớp cụ thể.

Element là loại được liên kết của các phần tử trong loại tuân thủ Collection . Element được xác định trong giao thức Sequence , nhưng vì Collection kế thừa từ Sequence nên nó kế thừa loại được liên kết với Element .

Comparable là một giao thức xác định "một loại có thể được so sánh bằng cách sử dụng các toán tử quan hệ < , <= , >=> ." . Vì chúng tôi đang thực hiện tìm kiếm nhị phân trên một Collection đã được sắp xếp, nên điều này tất nhiên phải đúng, nếu không chúng tôi không biết nên lặp lại/lặp lại sang trái hay phải trong tìm kiếm nhị phân.

Là một lưu ý phụ về việc triển khai, để biết thêm thông tin về hàm index(_:offsetBy:) đã được sử dụng, hãy tham khảo tài liệu sau.

Thuốc gốc + giao thức = 🔥

Generics và giao thức có thể là một công cụ mạnh mẽ nếu được sử dụng đúng cách để tránh mã trùng lặp.

Đầu tiên, hãy xem qua một hướng dẫn khác, A Swift Tour , hướng dẫn ngắn gọn về khái niệm chung ở cuối sách Colab.

Giả sử bạn có ý tưởng chung về thuốc generic, hãy nhanh chóng xem xét một số cách sử dụng nâng cao.

Khi một loại có nhiều yêu cầu, chẳng hạn như một loại tuân thủ một số giao thức, bạn có một số tùy chọn theo ý mình:

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

Lưu ý việc sử dụng typealias ở trên cùng. Thao tác này sẽ thêm bí danh được đặt tên thuộc loại hiện có vào chương trình của bạn. Sau khi bí danh loại được khai báo, tên bí danh có thể được sử dụng thay cho loại hiện có ở mọi nơi trong chương trình của bạn. Bí danh loại không tạo ra loại mới; họ chỉ đơn giản cho phép một tên đề cập đến một loại hiện có.

Bây giờ, hãy xem cách chúng ta có thể sử dụng các giao thức và generic cùng nhau.

Hãy tưởng tượng chúng ta là một cửa hàng máy tính với các yêu cầu sau đối với bất kỳ máy tính xách tay nào chúng ta bán để xác định cách chúng ta sắp xếp chúng ở phía sau cửa hàng:

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))"
    }
}

Tuy nhiên, chúng tôi có một yêu cầu mới là nhóm Laptop của mình theo khối lượng vì các kệ có giới hạn về trọng lượng.

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)]

Tuy nhiên, nếu chúng ta muốn lọc theo thứ gì đó không phải là Mass thì sao?

Một lựa chọn là làm như sau:

// 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)]

Tuyệt vời! Bây giờ chúng tôi có thể lọc dựa trên bất kỳ hạn chế nào về máy tính xách tay. Tuy nhiên, chúng tôi chỉ có thể lọc Laptop s.

Còn việc có thể lọc bất cứ thứ gì có trong hộp và có khối lượng thì sao? Có thể kho máy tính xách tay này cũng sẽ được sử dụng cho các máy chủ có đối tượng khách hàng khác:

// 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)]

Giờ đây, chúng tôi đã có thể lọc một mảng không chỉ theo bất kỳ thuộc tính nào của một struct cụ thể mà còn có thể lọc bất kỳ struct nào có thuộc tính đó!

Mẹo để thiết kế API tốt

Phần này được lấy từ WWDC 2019: Buổi nói chuyện về Thiết kế API Swift hiện đại .

Bây giờ bạn đã hiểu cách hoạt động của các giao thức, tốt nhất bạn nên xem xét khi nào bạn nên sử dụng các giao thức. Mặc dù các giao thức có thể mạnh mẽ như vậy nhưng không phải lúc nào bạn cũng nên tìm hiểu kỹ và bắt đầu ngay với các giao thức.

  • Bắt đầu với các trường hợp sử dụng cụ thể:
    • Trước tiên, hãy khám phá trường hợp sử dụng với các loại cụ thể và hiểu mã bạn muốn chia sẻ và tìm thấy đang được lặp lại. Sau đó, tính đến yếu tố chia sẻ mã với thuốc generic. Nó có thể có nghĩa là tạo ra các giao thức mới. Khám phá nhu cầu về mã chung.
  • Xem xét việc soạn các giao thức mới từ các giao thức hiện có được xác định trong thư viện chuẩn. Hãy tham khảo tài liệu sau đây của Apple để biết ví dụ điển hình về điều này.
  • Thay vì một giao thức chung, hãy xem xét việc xác định một loại chung.

Ví dụ: xác định loại vectơ tùy chỉnh

Giả sử chúng ta muốn xác định giao thức GeometricVector trên các số dấu phẩy động để sử dụng trong một số ứng dụng hình học mà chúng ta đang tạo nhằm xác định 3 phép toán vectơ quan trọng:

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

Giả sử chúng ta muốn lưu trữ các kích thước của vectơ mà giao thức SIMD có thể giúp chúng ta thực hiện, vì vậy chúng ta sẽ làm cho kiểu mới của chúng ta tinh chỉnh giao thức SIMD . Các vectơ SIMD có thể được coi là các vectơ có kích thước cố định và rất nhanh khi bạn sử dụng chúng để thực hiện các phép toán với vectơ:

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

Bây giờ, chúng ta hãy xác định cách triển khai mặc định của các hoạt động trên:

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
    }
}

Và sau đó chúng ta cần thêm sự phù hợp cho từng loại mà chúng ta muốn thêm các khả năng sau:

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 { }

Quá trình ba bước này nhằm xác định giao thức, cài đặt giao thức mặc định và sau đó thêm sự tuân thủ cho nhiều loại khá lặp đi lặp lại.

Giao thức có cần thiết không?

Việc không có loại SIMD nào có cách triển khai duy nhất là một dấu hiệu cảnh báo. Vì vậy, trong trường hợp này, giao thức không thực sự mang lại cho chúng ta bất cứ điều gì.

Xác định nó trong phần mở rộng của SIMD

Nếu chúng ta viết 3 toán tử trong phần mở rộng của giao thức SIMD , điều này có thể giải quyết vấn đề ngắn gọn hơn:

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
    }
}

Sử dụng ít dòng mã hơn, chúng tôi đã thêm tất cả cách triển khai mặc định cho tất cả các loại SIMD .

Đôi khi bạn có thể muốn tạo ra hệ thống phân cấp loại này, nhưng hãy nhớ rằng điều đó không phải lúc nào cũng cần thiết. Điều này cũng có nghĩa là kích thước nhị phân của chương trình được biên dịch của bạn sẽ nhỏ hơn và mã của bạn sẽ được biên dịch nhanh hơn.

Tuy nhiên, cách tiếp cận mở rộng này rất phù hợp khi bạn có một số phương pháp muốn thêm vào. Tuy nhiên, nó gặp phải vấn đề về khả năng mở rộng khi bạn thiết kế API lớn hơn.

Là một? Có một?

Trước đó chúng tôi đã nói GeometricVector sẽ tinh chỉnh SIMD . Nhưng đây có phải là một mối quan hệ? Vấn đề là SIMD xác định các phép toán cho phép chúng ta thêm số vô hướng 1 vào một vectơ, nhưng việc xác định một phép toán như vậy trong bối cảnh hình học là vô nghĩa.

Vì vậy, có lẽ mối quan hệ has-a sẽ tốt hơn bằng cách gói SIMD trong một loại chung mới có thể xử lý bất kỳ số dấu phẩy động nào:

// 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 }
}

Khi đó chúng ta có thể cẩn thận và chỉ xác định các phép toán chỉ có ý nghĩa trong bối cảnh hình học:

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)
    }
}

Và chúng tôi vẫn có thể sử dụng các tiện ích mở rộng chung để có được 3 toán tử trước đó mà chúng tôi muốn triển khai trông gần giống như trước:

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
    }
}

Nhìn chung, chúng tôi đã có thể tinh chỉnh hành vi của ba thao tác thành một loại bằng cách sử dụng cấu trúc. Với các giao thức, chúng tôi phải đối mặt với vấn đề ghi các tuân thủ lặp đi lặp lại cho tất cả các vectơ SIMD và cũng không thể ngăn một số toán tử nhất định như Scalar + Vector khả dụng (trong trường hợp này chúng tôi không muốn). Vì vậy, hãy nhớ rằng các giao thức không phải là giải pháp cuối cùng. Nhưng đôi khi các giải pháp truyền thống hơn có thể tỏ ra mạnh mẽ hơn.

Nhiều tài nguyên lập trình hướng giao thức hơn

Dưới đây là các tài nguyên bổ sung về các chủ đề được thảo luận:

  • WWDC 2015: Lập trình hướng giao thức trong Swift : điều này đã được trình bày bằng Swift 2, vì vậy rất nhiều thứ đã thay đổi kể từ đó (ví dụ: tên của các giao thức họ đã sử dụng trong bài thuyết trình) nhưng đây vẫn là một nguồn tài nguyên tốt cho lý thuyết và cách sử dụng đằng sau nó .
  • Giới thiệu Lập trình hướng giao thức trong Swift 3 : điều này được viết bằng Swift 3, vì vậy một số mã có thể cần được sửa đổi để biên dịch thành công, nhưng đó là một nguồn tài nguyên tuyệt vời khác.
  • WWDC 2019: Thiết kế API Swift hiện đại : xem xét sự khác biệt giữa loại giá trị và loại tham chiếu, trường hợp sử dụng khi các giao thức có thể được chứng minh là lựa chọn tồi hơn trong thiết kế API (giống như phần "Mẹo để thiết kế API tốt" ở trên), khóa tra cứu thành viên đường dẫn và trình bao bọc thuộc tính.
  • Generics : Tài liệu riêng của Swift dành cho Swift 5 đều nói về generics.