Trang này được dịch bởi Cloud Translation API.
Switch to English

Lập trình theo hướng giao thức & số liệu chung

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

Hướng dẫn này sẽ trình bày 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 chỉ số chung 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 các 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 tính 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 phân lớp cho phép bạn đạt được các mục tiêu tương tự thông qua các đá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 trở về 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ẽ đi qua các thành ngữ hỗ trợ việc sử dụng giao thức (phần mở rộng và ràng buộc giao thức), cũng như những 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 hỗ trợ các enum và cấu trúc được truyền theo giá trị. Enums và cấu trúc hỗ trợ nhiều tính năng do các lớp cung cấp. Hãy cùng xem!

Đầu tiên, hãy xem enums 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ờ, chúng ta hãy xem xét cấu trúc. Lưu ý rằng chúng ta không thể kế thừa các cấu trúc, nhưng 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 bởi các kiểu 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 các 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ó đa kế thừa), bạn có thể đã tạo các lớp trừu tượng ElectricGas sau đó sử dụng kế thừa lớp để làm cho cả hai kế thừa từ Car và sau đó có một mô hình ô tô cụ thể là một 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ới không khớ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ế.

Hãy định nghĩa 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ờ chúng ta hãy xác định một chiếc ô tô chạy bằng khí đốt:

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 các giao thức với các hành vi mặc định

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

Đây là nơi 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 xe điện mới nào chúng tôi tạo ra sẽ đặt pin thành 100 khi chúng tôi sạc lại. Như 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ề bộ truyện tranh!

Tuy nhiên, một điều cần chú ý là những điều sau đây. Trong lần triển khai đầu tiên, chúng tôi định nghĩa foo() là một triển khai mặc định trên A , nhưng không bắt buộc nó phải có 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 bắt buộc foo() trên A , chúng ta nhận được " Inst ":

bố46cc745
Inst

Điều này xảy ra do sự khác biệt giữa điều phối tĩnh trong ví dụ đầu tiên và điều phối 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 này trên Phương tiện .

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à điều này không hỗ trợ điều phối độ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, vì vậy lượng pin đã giảm xuống còn 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 đã có ý tưởng về cách các giao thức trong Swift hoạt động, hãy cùng 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 tiêu chuẩn

Hãy xem cách chúng ta có thể thêm chức năng bổ sung cho các kiểu đã 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, điều này rất dễ thực hiện.

Hãy thử và 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ó được sắp xếp hay không:

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 Collection , định nghĩa "một chuỗi có các phần tử có thể được duyệt nhiều lần, không phá hủy và được truy cập bởi một chỉ số con được lập chỉ mục." Vì các mảng có thể được lập chỉ mục bằng cách sử dụng ký hiệu dấu ngoặc vuông, đây là giao thức chúng tôi muốn mở rộng.

Tương tự, chúng tôi chỉ muốn thêm chức năng tiện ích này vào các mảng có các phần tử có thể được so sánh. Đây là lý do tại sao chúng tôi 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ẽ đề cập ngay sau đây, nhưng tóm lại cho phép chúng tôi thêm các yêu cầu bổ sung vào phần 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 lớp cha cụ thể.

Element là kiểu liên kết của các phần tử trong kiểu cấu trúc Collection hợp. Element được định nghĩa trong giao thức Sequence , nhưng vì Collection kế thừa từ Sequence nên nó sẽ kế thừa kiểu Element liên kết.

Comparable là một giao thức xác định "một kiểu có thể được so sánh bằng cách sử dụng các toán tử quan hệ < , <= , >= , và > ." . Vì chúng ta đang thực hiện tìm kiếm nhị phân trên Collection sắp xếp, điều này tất nhiên phải đúng nếu không chúng ta 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ưu ý thêm 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.

Generics + 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 trùng lặp mã.

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

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

Khi một loại duy nhất có nhiều yêu cầu, chẳng hạn như một loại phù hợp với 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 các typealias ở trên cùng. Điều này thêm bí danh được đặt tên của loại hiện có vào chương trình của bạn. Sau khi một bí danh kiểu được khai báo, tên bí danh đó có thể được sử dụng thay cho kiểu hiện có ở mọi nơi trong chương trình của bạn. Loại bí danh không tạo ra loại mới; họ chỉ cho phép một tên tham chiếu đến một kiểu hiện có.

Bây giờ, chúng ta 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 tôi 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 tôi bán để xác định cách chúng tôi tổ chức 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à phân nhóm Laptop của chúng tôi 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, điều gì sẽ xảy ra nếu chúng ta muốn lọc theo thứ khác ngoài Mass ?

Một tùy 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ỳ ràng buộc nào của máy tính xách tay. Tuy nhiên, chúng tôi chỉ có thể lọc Laptop .

Điều gì về việc có thể lọc bất cứ thứ gì có trong hộp và có khối lượng? 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ó cơ sở 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ỳ cấu trúc nào có thuộc tính đó!

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

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

Bây giờ bạn đã hiểu cách các giao thức hoạt động, tốt nhất là bạn nên xem qua khi nào bạn nên sử dụng các giao thức. Mạnh mẽ như các giao thức có thể, không phải lúc nào cũng là ý tưởng tốt nhất để đi sâu vào và bắt đầu ngay lập tức 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 đó, yếu tố chia sẻ mã ra với 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. Tham khảo tài liệu sau của Apple để biết ví dụ điển hình về điều này.
  • Thay vì một giao thức chung 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 tôi muốn xác định giao thức GeometricVector trên số dấu phẩy động để sử dụng trong một số ứng dụng hình học mà chúng tôi đang thực hiện, 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 tôi 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 tôi, vì vậy chúng tôi sẽ làm cho kiểu mới của mình tinh chỉnh giao thức SIMD . Vectơ SIMD có thể được coi là vectơ có kích thước cố định rất nhanh khi bạn sử dụng chúng để thực hiện các phép toán 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ác 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 với từng loại mà chúng ta muốn thêm các khả năng này:

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 để xác định giao thức, cung cấp cho nó một triển khai mặc định, và sau đó thêm một sự phù hợp cho nhiều loại là khá lặp đi lặp lại.

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

Thực tế là không có loại SIMD có 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ì.

Định nghĩa nó trong phần mở rộng của SIMD

Nếu chúng tôi viết 3 toán tử trong một 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ác triển khai mặc định cho tất cả các loại SIMD .

Đôi khi bạn có thể bị cám dỗ để tạo ra các kiểu phân cấp này, nhưng hãy nhớ rằng nó 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 đã 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 thức muốn thêm. Tuy nhiên, nó gặp phải vấn đề về khả năng mở rộng khi bạn đang thiết kế một 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ệ không? Vấn đề là SIMD định nghĩa các phép toán cho phép chúng ta thêm một vô hướng 1 vào một vectơ, nhưng sẽ không có ý nghĩa gì khi xác định một phép toán như vậy trong ngữ cảnh hình học.

Vì vậy, có thể mối quan hệ has-a sẽ tốt hơn bằng cách gói SIMD trong một kiểu 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 }
}

Sau đó, 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 ngữ 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 phần 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 hệt như trước đây:

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 hoạt động của chúng tôi thành một loại chỉ bằng cách sử dụng một cấu trúc. Với các giao thức, chúng tôi phải đối mặt với vấn đề viết các tuân thủ lặp lại cho tất cả SIMD 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). Như vậy, hãy nhớ rằng các giao thức không phải là một giải pháp tất cả và 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 đã có nhiều 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 liệu tốt cho lý thuyết và sử dụng đằng sau nó .
  • Giới thiệu Lập trình hướng giao thức trong Swift 3 : phần 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 các loại giá trị và tham chiếu, một trường hợp sử dụng về thời điểm các giao thức có thể trở thành lựa chọn tồi tệ 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 cho Swift 5 tất cả về generic.