ใน Swift ฉันจะประกาศตัวแปรประเภทเฉพาะที่สอดคล้องกับโปรโตคอลอย่างน้อยหนึ่งรายการได้อย่างไร


97

ใน Swift ฉันสามารถตั้งค่าประเภทของตัวแปรอย่างชัดเจนโดยการประกาศดังต่อไปนี้:

var object: TYPE_NAME

หากเราต้องการก้าวไปอีกขั้นและประกาศตัวแปรที่สอดคล้องกับโปรโตคอลหลายตัวเราสามารถใช้การprotocolประกาศ:

var object: protocol<ProtocolOne,ProtocolTwo>//etc

จะเป็นอย่างไรหากฉันต้องการประกาศอ็อบเจ็กต์ที่เป็นไปตามโปรโตคอลตั้งแต่หนึ่งโปรโตคอลขึ้นไปและเป็นประเภทคลาสพื้นฐานที่เฉพาะเจาะจง Objective-C ที่เทียบเท่าจะมีลักษณะดังนี้:

NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;

ใน Swift ฉันคาดหวังว่ามันจะมีลักษณะเช่นนี้:

var object: TYPE_NAME,ProtocolOne//etc

สิ่งนี้ทำให้เรามีความยืดหยุ่นในการจัดการกับการใช้งานประเภทพื้นฐานตลอดจนอินเทอร์เฟซเพิ่มเติมที่กำหนดไว้ในโปรโตคอล

มีวิธีอื่นที่ชัดเจนกว่าที่ฉันอาจพลาดไปหรือไม่?

ตัวอย่าง

ตัวอย่างเช่นสมมติว่าฉันมีUITableViewCellโรงงานที่รับผิดชอบในการส่งคืนเซลล์ที่เป็นไปตามโปรโตคอล เราสามารถตั้งค่าฟังก์ชันทั่วไปที่ส่งคืนเซลล์ที่เป็นไปตามโปรโตคอลได้อย่างง่ายดาย:

class CellFactory {
    class func createCellForItem<T: UITableViewCell where T:MyProtocol >(item: SpecialItem,tableView: UITableView) -> T {
        //etc
    }
}

ในภายหลังฉันต้องการยกเลิกการจัดคิวเซลล์เหล่านี้ในขณะที่ใช้ประโยชน์จากทั้งชนิดและโปรโตคอล

var cell: MyProtocol = CellFactory.createCellForItem(somethingAtIndexPath) as UITableViewCell

สิ่งนี้ส่งคืนข้อผิดพลาดเนื่องจากเซลล์มุมมองตารางไม่เป็นไปตามโปรโตคอล ...

ฉันต้องการที่จะระบุว่าเซลล์นั้นเป็น a UITableViewCellและสอดคล้องกับMyProtocolในการประกาศตัวแปรหรือไม่

เหตุผล

หากคุณคุ้นเคยกับFactory Patternสิ่งนี้จะสมเหตุสมผลในบริบทของความสามารถในการส่งคืนอ็อบเจ็กต์ของคลาสเฉพาะที่ใช้อินเทอร์เฟซบางอย่าง

เช่นเดียวกับในตัวอย่างของฉันบางครั้งเราต้องการกำหนดอินเทอร์เฟซที่เหมาะสมเมื่อนำไปใช้กับวัตถุหนึ่ง ๆ ตัวอย่างเซลล์มุมมองตารางของฉันเป็นเหตุผลอย่างหนึ่ง

ในขณะที่ประเภทที่ให้มาไม่สอดคล้องกับอินเทอร์เฟซที่กล่าวถึงอย่างแน่นอน แต่อ็อบเจ็กต์ที่โรงงานกลับมาทำดังนั้นฉันต้องการความยืดหยุ่นในการโต้ตอบกับทั้งประเภทคลาสฐานและอินเทอร์เฟซโปรโตคอลที่ประกาศ


ขออภัยประเด็นนี้คืออะไร ประเภทรู้อยู่แล้วว่าเป็นไปตามโปรโตคอลใด ไม่ใช้แค่ประเภทอะไร
Kirsteins

1
@Kirsteins ไม่เว้นแต่ประเภทจะถูกส่งคืนจากโรงงานดังนั้นจึงเป็นประเภททั่วไปที่มีคลาสพื้นฐานทั่วไป
Daniel Galasko

โปรดยกตัวอย่างถ้าเป็นไปได้
Kirsteins

NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;. วัตถุนี้ดูเหมือนจะไร้ประโยชน์เพราะNSSomethingรู้อยู่แล้วว่ามันสอดคล้องกับอะไร หากไม่เป็นไปตามโปรโตคอลใดโปรโตคอลหนึ่งในตัว<>คุณจะunrecognised selector ...เกิดปัญหา สิ่งนี้ไม่ให้ความปลอดภัยใด ๆ เลย
Kirsteins

@Kirsteins โปรดดูตัวอย่างของฉันอีกครั้งซึ่งใช้เมื่อคุณรู้ว่าวัตถุที่โรงงานของคุณจำหน่ายออกไปนั้นมาจากคลาสพื้นฐานที่เป็นไปตามโปรโตคอลที่ระบุ
Daniel Galasko

คำตอบ:


73

ใน Swift 4 ตอนนี้คุณสามารถประกาศตัวแปรที่เป็นคลาสย่อยของประเภทและใช้โปรโตคอลอย่างน้อยหนึ่งรายการในเวลาเดียวกัน

var myVariable: MyClass & MyProtocol & MySecondProtocol

ในการทำตัวแปรเสริม:

var myVariable: (MyClass & MyProtocol & MySecondProtocol)?

หรือเป็นพารามิเตอร์ของวิธีการ:

func shakeEm(controls: [UIControl & Shakeable]) {}

Apple ประกาศสิ่งนี้ที่ WWDC 2017 ในเซสชัน 402: มีอะไรใหม่ใน Swift

ประการที่สองฉันต้องการพูดคุยเกี่ยวกับการแต่งคลาสและโปรโตคอล ดังนั้นที่นี่ฉันได้แนะนำโปรโตคอลที่สั่นได้นี้สำหรับองค์ประกอบ UI ที่สามารถให้เอฟเฟกต์การสั่นไหวเล็กน้อยเพื่อดึงดูดความสนใจให้กับตัวเอง และฉันได้ดำเนินการต่อและขยายคลาส UIKit บางส่วนเพื่อให้ฟังก์ชันการสั่นนี้เป็นจริง และตอนนี้ฉันต้องการเขียนสิ่งที่ดูเหมือนง่าย ฉันแค่ต้องการเขียนฟังก์ชันที่ใช้การควบคุมจำนวนมากที่สั่นได้และเขย่าฟังก์ชันที่เปิดใช้งานเพื่อดึงดูดความสนใจมาที่พวกเขา ฉันสามารถเขียนประเภทใดในอาร์เรย์นี้ได้ มันน่าหงุดหงิดและยุ่งยากจริงๆ ดังนั้นฉันสามารถลองใช้การควบคุม UI แต่ไม่ใช่การควบคุม UI ทั้งหมดที่สามารถเขย่าได้ในเกมนี้ ฉันสามารถลอง shakable ได้ แต่ shakables ไม่ใช่ทั้งหมดที่เป็นตัวควบคุม UI และไม่มีวิธีใดที่ดีในการแสดงสิ่งนี้ใน Swift 3Swift 4 นำเสนอแนวคิดของการแต่งคลาสด้วยโปรโตคอลจำนวนเท่าใดก็ได้


3
เพียงแค่เพิ่มลิงค์ไปยังข้อเสนอวิวัฒนาการที่รวดเร็วgithub.com/apple/swift-evolution/blob/master/proposals/…
Daniel Galasko

ขอบคุณ Philipp!
Omar Albeik

จะเป็นอย่างไรหากต้องการตัวแปรเสริมประเภทนี้
Vyachaslav Gerchicov

2
@VyachaslavGerchicov: คุณสามารถใส่วงเล็บไว้รอบ ๆ แล้วใส่เครื่องหมายคำถามดังนี้: myVariable: (MyClass & MyProtocol & MySecondProtocol)?
Philipp Otto

30

คุณไม่สามารถประกาศตัวแปรเช่น

var object:Base,protocol<ProtocolOne,ProtocolTwo> = ...

หรือประกาศประเภทการส่งคืนฟังก์ชันเช่น

func someFunc() -> Base,protocol<MyProtocol,Protocol2> { ... }

คุณสามารถประกาศเป็นพารามิเตอร์ฟังก์ชันได้เช่นนี้ แต่โดยพื้นฐานแล้วมันเป็นการหล่อขึ้น

func someFunc<T:Base where T:protocol<MyProtocol1,MyProtocol2>>(val:T) {
    // here, `val` is guaranteed to be `Base` and conforms `MyProtocol` and `MyProtocol2`
}

class SubClass:BaseClass, MyProtocol1, MyProtocol2 {
   //...
}

let val = SubClass()
someFunc(val)

ณ ตอนนี้สิ่งที่คุณทำได้มีดังนี้

class CellFactory {
    class func createCellForItem(item: SpecialItem) -> UITableViewCell {
        return ... // any UITableViewCell subclass
    }
}

let cell = CellFactory.createCellForItem(special)
if let asProtocol = cell as? protocol<MyProtocol1,MyProtocol2> {
    asProtocol.protocolMethod()
    cell.cellMethod()
}

ด้วยวิธีนี้ในทางเทคนิคเป็นเหมือนcellasProtocol

แต่สำหรับคอมไพเลอร์cellมีอินเทอร์เฟซUITableViewCellเพียงอย่างเดียวในขณะที่asProtocolมีอินเตอร์เฟสโปรโตคอลเท่านั้น ดังนั้นเมื่อคุณต้องการเรียกใช้UITableViewCellเมธอดคุณต้องใช้cellตัวแปร เมื่อคุณต้องการเรียกวิธีโปรโตคอลให้ใช้asProtocolตัวแปร

if let ... as? ... {}ถ้าคุณแน่ใจว่าที่สอดคล้องเซลล์โปรโตคอลที่คุณไม่จำเป็นต้องใช้ ชอบ:

let cell = CellFactory.createCellForItem(special)
let asProtocol = cell as protocol<MyProtocol1,MyProtocol2>

เนื่องจากโรงงานระบุประเภทการส่งคืนฉันไม่จำเป็นต้องทำการหล่อเสริมในทางเทคนิค? ฉันสามารถพึ่งพาการพิมพ์โดยนัยของ swifts เพื่อทำการพิมพ์โดยที่ฉันประกาศโปรโตคอลอย่างชัดเจน?
Daniel Galasko

ฉันไม่เข้าใจความหมายของคุณขอโทษที่ทักษะภาษาอังกฤษของฉันไม่ดี หากคุณกำลังพูดถึง-> UITableViewCell<MyProtocol>สิ่งนี้ไม่ถูกต้องเนื่องจากUITableViewCellไม่ใช่ประเภททั่วไป ฉันคิดว่านี่ไม่ได้รวบรวม
rintaro

ฉันไม่ได้อ้างถึงการใช้งานทั่วไปของคุณ แต่เป็นภาพประกอบตัวอย่างของการใช้งาน ที่คุณพูดว่า let asProtocol = ...
Daniel Galasko

หรือฉันทำได้แค่: var cell: protocol <ProtocolOne, ProtocolTwo> = someObject เป็น UITableViewCell และได้รับประโยชน์จากทั้งสองตัวแปร
Daniel Galasko

2
ฉันไม่คิดอย่างนั้น แม้ว่าคุณจะทำเช่นนั้นcellได้ แต่ก็มีวิธีโปรโตคอลเท่านั้น (สำหรับคอมไพเลอร์)
rintaro

2

ขออภัย Swift ไม่รองรับความสอดคล้องของโปรโตคอลระดับวัตถุ อย่างไรก็ตามมีวิธีแก้ไขที่ค่อนข้างน่าอึดอัดใจที่อาจตอบสนองวัตถุประสงค์ของคุณ

struct VCWithSomeProtocol {
    let protocol: SomeProtocol
    let viewController: UIViewController

    init<T: UIViewController>(vc: T) where T: SomeProtocol {
        self.protocol = vc
        self.viewController = vc
    }
}

จากนั้นทุกที่ที่คุณต้องการทำอะไรก็ตามที่ UIViewController มีคุณจะเข้าถึงด้าน. viewController ของโครงสร้างและทุกสิ่งที่คุณต้องการด้านโปรโตคอลคุณจะอ้างอิงถึง. โปรโตคอล

สำหรับอินสแตนซ์:

class SomeClass {
   let mySpecialViewController: VCWithSomeProtocol

   init<T: UIViewController>(injectedViewController: T) where T: SomeProtocol {
       self.mySpecialViewController = VCWithSomeProtocol(vc: injectedViewController)
   }
}

ทุกเวลาที่คุณต้องการ mySpecialViewController เพื่อทำทุกอย่างที่เกี่ยวข้องกับ UIViewController คุณเพียงแค่อ้างอิง mySpecialViewController.viewController และเมื่อใดก็ตามที่คุณต้องการให้ทำฟังก์ชันโปรโตคอลคุณอ้างอิง mySpecialViewController.protocol

หวังว่า Swift 4 จะช่วยให้เราสามารถประกาศวัตถุที่มีโปรโตคอลที่แนบมาได้ในอนาคต แต่สำหรับตอนนี้มันได้ผล

หวังว่านี่จะช่วยได้!


1

แก้ไข: ฉันเข้าใจผิดแต่ถ้ามีคนอื่นอ่านความเข้าใจผิดนี้เหมือนฉันฉันก็ทิ้งคำตอบนี้ไว้ที่นั่น OP ถามเกี่ยวกับการตรวจสอบความสอดคล้องของโปรโตคอลของวัตถุของคลาสย่อยที่กำหนดและนั่นก็เป็นอีกเรื่องหนึ่งที่คำตอบที่ยอมรับแสดงให้เห็น คำตอบนี้พูดถึงความสอดคล้องของโปรโตคอลสำหรับคลาสพื้นฐาน

บางทีฉันอาจจะเข้าใจผิด แต่คุณไม่ได้พูดถึงการเพิ่มความสอดคล้องของโปรโตคอลในUITableCellViewชั้นเรียนหรือ โปรโตคอลอยู่ในกรณีที่ขยายไปยังคลาสฐานไม่ใช่วัตถุ ดูเอกสารของ Apple เกี่ยวกับการประกาศการยอมรับโปรโตคอลพร้อมส่วนขยายซึ่งในกรณีของคุณจะเป็นดังนี้:

extension UITableCellView : ProtocolOne {}

// Or alternatively if you need to add a method, protocolMethod()
extension UITableCellView : ProcotolTwo {
   func protocolTwoMethod() -> String {
     return "Compliant method"
   }
}

นอกเหนือจากเอกสาร Swift ที่อ้างถึงแล้วโปรดดูบทความ Nate Cooks ฟังก์ชันทั่วไปสำหรับประเภทที่เข้ากันไม่ได้พร้อมตัวอย่างเพิ่มเติม

สิ่งนี้ทำให้เรามีความยืดหยุ่นในการจัดการกับการใช้งานประเภทพื้นฐานตลอดจนอินเทอร์เฟซเพิ่มเติมที่กำหนดไว้ในโปรโตคอล

มีวิธีอื่นที่ชัดเจนกว่าที่ฉันอาจพลาดไปหรือไม่?

การยอมรับโปรโตคอลจะทำเพียงแค่นี้ทำให้วัตถุเป็นไปตามโปรโตคอลที่กำหนด จะมี แต่ความตระหนักในด้านที่ไม่พึงประสงค์ที่ตัวแปรประเภทโปรโตคอลที่กำหนดไม่ได้รู้อะไรนอกของโปรโตคอล แต่สิ่งนี้สามารถหลีกเลี่ยงได้โดยการกำหนดโปรโตคอลซึ่งมีวิธีการ / ตัวแปรที่จำเป็นทั้งหมด / ...

ในขณะที่ประเภทที่ให้มาไม่สอดคล้องกับอินเทอร์เฟซที่กล่าวถึงอย่างแน่นอน แต่อ็อบเจ็กต์ที่โรงงานกลับมาทำดังนั้นฉันต้องการความยืดหยุ่นในการโต้ตอบกับทั้งประเภทคลาสฐานและอินเทอร์เฟซโปรโตคอลที่ประกาศ

หากคุณต้องการวิธีการทั่วไปตัวแปรที่สอดคล้องกับทั้งโปรโตคอลและประเภทคลาสพื้นฐานคุณอาจโชคไม่ดี แต่ดูเหมือนว่าคุณต้องกำหนดโปรโตคอลให้กว้างพอที่จะมีวิธีการปฏิบัติตามที่จำเป็นและในขณะเดียวกันก็แคบพอที่จะมีตัวเลือกในการนำไปใช้กับคลาสพื้นฐานโดยไม่ต้องทำงานมากเกินไป (เช่นเพียงแค่ประกาศว่าคลาสเป็นไปตาม มาตรการ).


1
นั่นไม่ใช่สิ่งที่ฉันกำลังพูดถึง แต่ขอบคุณ :) ฉันต้องการที่จะเชื่อมต่อกับวัตถุผ่านทั้งคลาสและโปรโตคอลเฉพาะ เช่นเดียวกับวิธีใน obj-c ฉันสามารถทำ NSObject <MyProtocol> obj = ... ไม่จำเป็นต้องบอกว่าสิ่งนี้ไม่สามารถทำได้อย่างรวดเร็วคุณต้องส่งวัตถุไปยังโปรโตคอล
Daniel Galasko

0

ครั้งหนึ่งฉันเคยมีสถานการณ์คล้าย ๆ กันเมื่อพยายามเชื่อมโยงการเชื่อมต่อผู้โต้ตอบทั่วไปของฉันใน Storyboards (IB ไม่อนุญาตให้คุณเชื่อมต่อร้านค้ากับโปรโตคอลเฉพาะอินสแตนซ์ออบเจ็กต์) ซึ่งฉันได้รับเพียงแค่ปกปิด ivar สาธารณะระดับพื้นฐานด้วยคอมพิวเตอร์ส่วนตัว ทรัพย์สิน. แม้ว่าวิธีนี้จะไม่ได้ป้องกันไม่ให้ใครบางคนทำการมอบหมายงานที่ผิดกฎหมาย แต่ก็เป็นวิธีที่สะดวกในการป้องกันการโต้ตอบที่ไม่ต้องการใด ๆ อย่างปลอดภัยกับอินสแตนซ์ที่ไม่เป็นไปตามที่รันไทม์ (เช่นป้องกันการเรียกใช้เมธอดผู้แทนไปยังอ็อบเจ็กต์ที่ไม่เป็นไปตามโปรโตคอล)

ตัวอย่าง:

@objc protocol SomeInteractorInputProtocol {
    func getSomeString()
}

@objc protocol SomeInteractorOutputProtocol {
    optional func receiveSomeString(value:String)
}

@objc class SomeInteractor: NSObject, SomeInteractorInputProtocol {

    @IBOutlet var outputReceiver : AnyObject? = nil

    private var protocolOutputReceiver : SomeInteractorOutputProtocol? {
        get { return self.outputReceiver as? SomeInteractorOutputProtocol }
    }

    func getSomeString() {
        let aString = "This is some string."
        self.protocolOutputReceiver?.receiveSomeString?(aString)
    }
}

"outputReceiver" ประกาศเป็นทางเลือกเช่นเดียวกับ "protocolOutputReceiver" ส่วนตัว ด้วยการเข้าถึง outputReceiver (aka delegate) ผ่านทางหลัง (คุณสมบัติที่คำนวณ) เสมอฉันจะกรองวัตถุใด ๆ ที่ไม่เป็นไปตามโปรโตคอลได้อย่างมีประสิทธิภาพ ตอนนี้ฉันสามารถใช้การผูกมัดทางเลือกเพื่อโทรออกไปยังวัตถุที่ได้รับมอบหมายอย่างปลอดภัยไม่ว่าจะใช้โปรโตคอลหรือแม้กระทั่งมีอยู่ก็ตาม

เพื่อนำไปใช้กับสถานการณ์ของคุณคุณสามารถกำหนดให้ ivar สาธารณะเป็นประเภท "YourBaseClass?" (ซึ่งตรงข้ามกับ AnyObject) และใช้คุณสมบัติการคำนวณส่วนตัวเพื่อบังคับใช้ความสอดคล้องของโปรโตคอล FWIW.

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.