प्रोटोकॉल-उन्मुख प्रोग्रामिंग और amp; जेनरिक

TensorFlow.org पर देखें GitHub पर स्रोत देखें

यह ट्यूटोरियल प्रोटोकॉल-उन्मुख प्रोग्रामिंग और रोजमर्रा के उदाहरणों में जेनेरिक के साथ उनका उपयोग कैसे किया जा सकता है, इसके विभिन्न उदाहरणों पर चर्चा करेगा।

प्रोटोकॉल

इनहेरिटेंस प्रोग्रामिंग भाषाओं में कोड को व्यवस्थित करने का एक शक्तिशाली तरीका है जो आपको प्रोग्राम के कई घटकों के बीच कोड साझा करने की अनुमति देता है।

स्विफ्ट में, विरासत को व्यक्त करने के विभिन्न तरीके हैं। आप अन्य भाषाओं में से एक तरीके से पहले से ही परिचित हो सकते हैं: वर्ग वंशानुक्रम। हालाँकि, स्विफ्ट का एक और तरीका है: प्रोटोकॉल।

इस ट्यूटोरियल में, हम प्रोटोकॉल का पता लगाएंगे - उपवर्गीकरण का एक विकल्प जो आपको विभिन्न ट्रेडऑफ़ के माध्यम से समान लक्ष्य प्राप्त करने की अनुमति देता है। स्विफ्ट में, प्रोटोकॉल में कई अमूर्त सदस्य होते हैं। कक्षाएं, संरचनाएं और एनम कई प्रोटोकॉल के अनुरूप हो सकते हैं और अनुरूपता संबंध पूर्वव्यापी रूप से स्थापित किया जा सकता है। यह सब कुछ ऐसे डिज़ाइनों को सक्षम बनाता है जिन्हें उपवर्गीकरण का उपयोग करके स्विफ्ट में आसानी से व्यक्त नहीं किया जा सकता है। हम उन मुहावरों के बारे में जानेंगे जो प्रोटोकॉल (एक्सटेंशन और प्रोटोकॉल बाधाएं) के उपयोग का समर्थन करते हैं, साथ ही प्रोटोकॉल की सीमाएं भी।

स्विफ्ट 💖 के मूल्य प्रकार!

संदर्भ शब्दार्थ वाली कक्षाओं के अलावा, स्विफ्ट उन एनम और संरचनाओं का समर्थन करता है जो मूल्य द्वारा पारित किए जाते हैं। Enums और structs कक्षाओं द्वारा प्रदान की गई कई सुविधाओं का समर्थन करते हैं। चलो एक नज़र मारें!

सबसे पहले, आइए देखें कि एनम कक्षाओं के समान कैसे हैं:

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

अब, आइए संरचनाओं को देखें। ध्यान दें कि हम structs को इनहेरिट नहीं कर सकते, बल्कि इसके बजाय प्रोटोकॉल का उपयोग कर सकते हैं:

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)

यह एक नई संरचना TeslaModelS निर्दिष्ट करता है जो Car और Electric दोनों प्रोटोकॉल के अनुरूप है।

आइए अब गैस से चलने वाली कार को परिभाषित करें:

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 पर डिफ़ॉल्ट कर सकते हैं। .

यहीं पर स्विफ्ट में एक्सटेंशन काम आ सकते हैं:

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

हालाँकि, यदि हम A पर foo() आवश्यक बनाते हैं, तो हमें " 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

यह स्विफ्ट में प्रोटोकॉल पर पहले उदाहरण में स्थैतिक प्रेषण और दूसरे में स्थैतिक प्रेषण के बीच अंतर के कारण होता है। अधिक जानकारी के लिए, इस मीडियम पोस्ट को देखें।

डिफ़ॉल्ट व्यवहार को ओवरराइड करना

हालाँकि, यदि हम चाहें, तो हम अभी भी डिफ़ॉल्ट व्यवहार को ओवरराइड कर सकते हैं। ध्यान देने योग्य एक महत्वपूर्ण बात यह है कि यह गतिशील प्रेषण का समर्थन नहीं करता है

मान लीजिए कि हमारे पास इलेक्ट्रिक कार का पुराना संस्करण है, इसलिए बैटरी की क्षमता 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
    }
}

प्रोटोकॉल का मानक पुस्तकालय उपयोग

अब जब हमें पता चल गया है कि स्विफ्ट में प्रोटोकॉल कैसे काम करते हैं, तो आइए मानक लाइब्रेरी प्रोटोकॉल का उपयोग करने के कुछ विशिष्ट उदाहरण देखें।

मानक पुस्तकालय का विस्तार करें

आइए देखें कि हम स्विफ्ट में पहले से मौजूद प्रकारों में अतिरिक्त कार्यक्षमता कैसे जोड़ सकते हैं। चूंकि स्विफ्ट में प्रकार अंतर्निहित नहीं हैं, लेकिन संरचना के रूप में मानक लाइब्रेरी का हिस्सा हैं, इसलिए ऐसा करना आसान है।

आइए तत्वों की एक सरणी पर बाइनरी खोज करने का प्रयास करें, साथ ही यह भी सुनिश्चित करें कि सरणी क्रमबद्ध है:

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 क्लॉज स्विफ्ट के टाइप सिस्टम का एक हिस्सा है, जिसे हम जल्द ही कवर करेंगे, लेकिन संक्षेप में हम जो एक्सटेंशन लिख रहे हैं उसमें अतिरिक्त आवश्यकताएं जोड़ते हैं, जैसे प्रोटोकॉल को लागू करने के लिए टाइप की आवश्यकता होती है, दो प्रकारों की आवश्यकता होती है समान, या एक वर्ग के लिए एक विशेष सुपरक्लास की आवश्यकता होती है।

Element Collection -अनुरूप प्रकार में तत्वों का संबद्ध प्रकार है। Element Sequence प्रोटोकॉल के भीतर परिभाषित किया गया है, लेकिन चूंकि Collection Sequence से विरासत में मिला है, इसलिए यह Element से संबंधित प्रकार को विरासत में मिला है।

Comparable एक प्रोटोकॉल है जो "एक प्रकार को परिभाषित करता है जिसकी तुलना रिलेशनल ऑपरेटर्स < , <= , >= , और > का उपयोग करके की जा सकती है।" . चूँकि हम क्रमबद्ध Collection पर बाइनरी खोज कर रहे हैं, यह निश्चित रूप से सत्य होना चाहिए अन्यथा हम नहीं जानते कि बाइनरी खोज में बाएँ या दाएँ पुनरावृत्ति/पुनरावृत्ति करनी है या नहीं।

कार्यान्वयन के बारे में एक साइड नोट के रूप में, उपयोग किए गए index(_:offsetBy:) फ़ंक्शन के बारे में अधिक जानकारी के लिए, निम्नलिखित दस्तावेज़ देखें।

जेनेरिक + प्रोटोकॉल = 💥

यदि डुप्लिकेट कोड से बचने के लिए जेनरिक और प्रोटोकॉल का सही ढंग से उपयोग किया जाए तो यह एक शक्तिशाली उपकरण हो सकता है।

सबसे पहले, एक अन्य ट्यूटोरियल, ए स्विफ्ट टूर देखें, जिसमें कोलाब पुस्तक के अंत में जेनेरिक को संक्षेप में शामिल किया गया है।

यह मानते हुए कि आपके पास जेनरिक के बारे में एक सामान्य विचार है, आइए जल्दी से कुछ उन्नत उपयोगों पर एक नज़र डालें।

जब एक ही प्रकार की कई आवश्यकताएं होती हैं जैसे कि एक प्रकार कई प्रोटोकॉल के अनुरूप होता है, तो आपके पास अपने निपटान में कई विकल्प होते हैं:

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 के उपयोग पर ध्यान दें। यह आपके प्रोग्राम में मौजूदा प्रकार का नामित उपनाम जोड़ता है। एक प्रकार का उपनाम घोषित होने के बाद, आपके प्रोग्राम में हर जगह मौजूदा प्रकार के बजाय उपनाम नाम का उपयोग किया जा सकता है। प्रकार उपनाम नए प्रकार नहीं बनाते हैं; वे बस एक नाम को मौजूदा प्रकार को संदर्भित करने की अनुमति देते हैं।

अब, आइए देखें कि हम प्रोटोकॉल और जेनरिक का एक साथ उपयोग कैसे कर सकते हैं।

आइए कल्पना करें कि हम एक कंप्यूटर स्टोर हैं और हम जो भी लैपटॉप बेचते हैं उसके लिए निम्नलिखित आवश्यकताएं हैं, यह निर्धारित करने के लिए कि हम उन्हें स्टोर के पीछे कैसे व्यवस्थित करते हैं:

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 के अलावा किसी अन्य चीज़ द्वारा फ़िल्टर करना चाहें तो क्या होगा?

एक विकल्प निम्नलिखित करना है:

// 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 की किसी भी संपत्ति द्वारा एक सरणी को फ़िल्टर करने में सक्षम हैं, बल्कि उस संपत्ति वाली किसी भी संरचना को फ़िल्टर करने में भी सक्षम हैं!

अच्छे एपीआई डिज़ाइन के लिए युक्तियाँ

यह अनुभाग WWDC 2019: मॉडर्न स्विफ्ट एपीआई डिज़ाइन टॉक से लिया गया था।

अब जब आप समझ गए हैं कि प्रोटोकॉल कैसे व्यवहार करते हैं, तो यह जानना सबसे अच्छा है कि आपको प्रोटोकॉल का उपयोग कब करना चाहिए। प्रोटोकॉल जितने शक्तिशाली हो सकते हैं, उनमें गहराई से उतरना और तुरंत प्रोटोकॉल शुरू करना हमेशा सबसे अच्छा विचार नहीं होता है।

  • ठोस उपयोग के मामलों से प्रारंभ करें:
    • पहले ठोस प्रकारों के साथ उपयोग के मामले का पता लगाएं और समझें कि वह कौन सा कोड है जिसे आप साझा करना चाहते हैं और जिसे दोहराया जा रहा है। फिर, वह कारक जो जेनेरिक के साथ कोड साझा करता है। इसका मतलब नए प्रोटोकॉल बनाना हो सकता है। जेनेरिक कोड की आवश्यकता का पता लगाएं।
  • मानक लाइब्रेरी में परिभाषित मौजूदा प्रोटोकॉल से नए प्रोटोकॉल बनाने पर विचार करें। इसके अच्छे उदाहरण के लिए निम्नलिखित Apple दस्तावेज़ देखें।
  • सामान्य प्रोटोकॉल के बजाय, सामान्य प्रकार को परिभाषित करने पर विचार करें।

उदाहरण: एक कस्टम वेक्टर प्रकार को परिभाषित करना

मान लीजिए कि हम अपने द्वारा बनाए जा रहे कुछ ज्योमेट्री ऐप में उपयोग करने के लिए फ़्लोटिंग-पॉइंट नंबरों पर एक GeometricVector प्रोटोकॉल को परिभाषित करना चाहते हैं जो 3 महत्वपूर्ण वेक्टर ऑपरेशंस को परिभाषित करता है:

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

प्रोटोकॉल को परिभाषित करने, इसे एक डिफ़ॉल्ट कार्यान्वयन देने और फिर कई प्रकारों में अनुरूपता जोड़ने की यह तीन-चरणीय प्रक्रिया काफी दोहराव वाली है।

क्या प्रोटोकॉल आवश्यक था?

यह तथ्य कि किसी भी 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 में सभी डिफ़ॉल्ट कार्यान्वयन जोड़े।

कभी-कभी आप इस प्रकार के पदानुक्रम को बनाने के लिए प्रलोभित हो सकते हैं, लेकिन याद रखें कि यह हमेशा आवश्यक नहीं होता है। इसका मतलब यह भी है कि आपके संकलित प्रोग्राम का बाइनरी आकार छोटा होगा, और आपका कोड संकलित करने में तेज़ होगा।

हालाँकि, यह विस्तार दृष्टिकोण तब बहुत अच्छा होता है जब आपके पास कुछ संख्या में विधियाँ हों जिन्हें आप जोड़ना चाहते हैं। हालाँकि, जब आप एक बड़ा एपीआई डिज़ाइन कर रहे होते हैं तो यह स्केलेबिलिटी समस्या का सामना करता है।

एक है? एक?

पहले हमने कहा था कि GeometricVector SIMD परिष्कृत करेगा। लेकिन क्या यह एक रिश्ता है? समस्या यह है कि 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
    }
}

कुल मिलाकर, हम केवल एक संरचना का उपयोग करके अपने तीन ऑपरेशनों के व्यवहार को एक प्रकार में परिष्कृत करने में सक्षम हैं। प्रोटोकॉल के साथ, हमें सभी SIMD वैक्टरों के लिए दोहरावदार अनुरूपताएं लिखने की समस्या का सामना करना पड़ा, और हम Scalar + Vector जैसे कुछ ऑपरेटरों को उपलब्ध होने से रोकने में भी सक्षम नहीं थे (जो इस मामले में हम नहीं चाहते थे)। इस प्रकार, याद रखें कि प्रोटोकॉल ही सब कुछ का समाधान नहीं है। लेकिन कभी-कभी अधिक पारंपरिक समाधान अधिक शक्तिशाली साबित हो सकते हैं।

अधिक प्रोटोकॉल-उन्मुख प्रोग्रामिंग संसाधन

यहां चर्चा किए गए विषयों पर अतिरिक्त संसाधन दिए गए हैं:

  • WWDC 2015: स्विफ्ट में प्रोटोकॉल-ओरिएंटेड प्रोग्रामिंग : इसे स्विफ्ट 2 का उपयोग करके प्रस्तुत किया गया था, इसलिए तब से बहुत कुछ बदल गया है (उदाहरण के लिए प्रस्तुति में उपयोग किए गए प्रोटोकॉल का नाम) लेकिन यह अभी भी सिद्धांत और इसके पीछे उपयोग के लिए एक अच्छा संसाधन है .
  • स्विफ्ट 3 में प्रोटोकॉल-ओरिएंटेड प्रोग्रामिंग का परिचय : यह स्विफ्ट 3 में लिखा गया था, इसलिए इसे सफलतापूर्वक संकलित करने के लिए कुछ कोड को संशोधित करने की आवश्यकता हो सकती है, लेकिन यह एक और महान संसाधन है।
  • WWDC 2019: आधुनिक स्विफ्ट एपीआई डिज़ाइन : मूल्य और संदर्भ प्रकारों के बीच अंतर पर चर्चा करता है, एक उपयोग का मामला जब प्रोटोकॉल एपीआई डिज़ाइन में सबसे खराब विकल्प साबित हो सकता है (उपरोक्त "अच्छे एपीआई डिज़ाइन के लिए टिप्स" अनुभाग के समान), कुंजी पथ सदस्य लुकअप, और संपत्ति रैपर।
  • जेनेरिक : जेनेरिक के बारे में स्विफ्ट 5 के लिए स्विफ्ट का अपना दस्तावेज।