Trong Swift, làm cách nào để khai báo một biến của một kiểu cụ thể phù hợp với một hoặc nhiều giao thức?


96

Trong Swift, tôi có thể đặt kiểu của một biến một cách rõ ràng bằng cách khai báo nó như sau:

var object: TYPE_NAME

Nếu chúng ta muốn tiến thêm một bước và khai báo một biến phù hợp với nhiều giao thức, chúng ta có thể sử dụng protocolkhai báo:

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

Điều gì sẽ xảy ra nếu tôi muốn khai báo một đối tượng tuân theo một hoặc nhiều giao thức và cũng thuộc loại lớp cơ sở cụ thể? Tương đương Objective-C sẽ như thế này:

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

Trong Swift, tôi mong đợi nó trông như thế này:

var object: TYPE_NAME,ProtocolOne//etc

Điều này mang lại cho chúng ta sự linh hoạt khi có thể đối phó với việc triển khai kiểu cơ sở cũng như giao diện bổ sung được xác định trong giao thức.

Có cách nào khác rõ ràng hơn mà tôi có thể thiếu không?

Thí dụ

Ví dụ: giả sử tôi có một UITableViewCellnhà máy chịu trách nhiệm trả về các ô tuân theo một giao thức. Chúng ta có thể dễ dàng thiết lập một hàm chung trả về các ô tuân theo một giao thức:

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

sau đó, tôi muốn hủy hàng hóa các ô này trong khi tận dụng cả loại và giao thức

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

Điều này trả về lỗi vì ô xem bảng không tuân theo giao thức ...

Tôi muốn có thể chỉ định ô đó là a UITableViewCellvà phù hợp với MyProtocolkhai báo biến?

Biện minh

Nếu bạn đã quen với Factory Pattern, điều này sẽ có ý nghĩa trong bối cảnh có thể trả về các đối tượng của một lớp cụ thể triển khai một giao diện nhất định.

Cũng giống như trong ví dụ của tôi, đôi khi chúng ta muốn xác định các giao diện có ý nghĩa khi áp dụng cho một đối tượng cụ thể. Ví dụ của tôi về ô xem bảng là một trong những lý do như vậy.

Trong khi kiểu được cung cấp không hoàn toàn phù hợp với giao diện đã đề cập, đối tượng mà nhà máy trả về sẽ có và vì vậy tôi muốn sự linh hoạt trong việc tương tác với cả kiểu lớp cơ sở và giao diện giao thức đã khai báo


Xin lỗi, nhưng điều này nhanh chóng có ích lợi gì. Các loại đã biết chúng tuân theo giao thức nào. Những gì không chỉ sử dụng loại?
Kirsteins

1
@Kirsteins Không, trừ khi loại được trả về từ một nhà máy và do đó là một kiểu generic với một lớp cơ sở chung
Daniel Galasko

Vui lòng cho một ví dụ nếu có thể.
Kirsteins

NSSomething<ABCProtocolOne,ABCProtocolTwo> * object = ...;. Đối tượng này có vẻ khá vô dụng vì NSSomethingđã biết nó tuân theo những gì. Nếu nó không phù hợp với một trong các giao thức trong <>bạn sẽ gặp unrecognised selector ...sự cố. Điều này không cung cấp an toàn cho loại nào cả.
Kirsteins

@Kirsteins Hãy xem ví dụ của tôi một lần nữa, nó được sử dụng khi bạn biết rằng đối tượng nhà máy của bạn vends ra là của một lớp cơ sở đặc biệt phù hợp với một giao thức định
Daniel Galasko

Câu trả lời:


72

Trong Swift 4, giờ đây có thể khai báo một biến là một lớp con của một kiểu và thực hiện một hoặc nhiều giao thức cùng một lúc.

var myVariable: MyClass & MyProtocol & MySecondProtocol

Để thực hiện một biến tùy chọn:

var myVariable: (MyClass & MyProtocol & MySecondProtocol)?

hoặc là tham số của một phương thức:

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

Apple đã công bố điều này tại WWDC 2017 trong Phiên 402: Có gì mới trong Swift

Thứ hai, tôi muốn nói về việc soạn các lớp và giao thức. Vì vậy, ở đây tôi đã giới thiệu giao thức có thể lắc này cho một phần tử giao diện người dùng có thể tạo ra một chút hiệu ứng rung lắc để thu hút sự chú ý vào chính nó. Và tôi đã tiếp tục và mở rộng một số lớp UIKit để thực sự cung cấp chức năng lắc này. Và bây giờ tôi muốn viết một cái gì đó có vẻ đơn giản. Tôi chỉ muốn viết một hàm sử dụng một loạt các điều khiển có thể lắc và lắc những điều khiển được bật để thu hút sự chú ý đến chúng. Tôi có thể viết kiểu gì ở đây trong mảng này? Nó thực sự bực bội và khó khăn. Vì vậy, tôi có thể thử sử dụng điều khiển giao diện người dùng. Nhưng không phải tất cả các điều khiển giao diện người dùng đều có thể lắc được trong trò chơi này. Tôi có thể thử shakable, nhưng không phải tất cả shakables đều là điều khiển giao diện người dùng. Và thực sự không có cách nào tốt để thể hiện điều này trong Swift 3.Swift 4 giới thiệu khái niệm về việc soạn một lớp với bất kỳ số lượng giao thức nào.


3
Chỉ cần thêm một liên kết đến đề xuất phát triển nhanh chóng github.com/apple/swift-evolution/blob/master/proposal/…
Daniel Galasko

Cảm ơn Philipp!
Omar Albeik

Nếu cần một biến tùy chọn thuộc loại này thì sao?
Vyachaslav Gerchicov

2
@VyachaslavGerchicov: Bạn có thể đặt dấu ngoặc đơn xung quanh nó và sau đó là dấu hỏi như sau: var myVariable: (MyClass & MyProtocol & MySecondProtocol)?
Philipp Otto

30

Bạn không thể khai báo biến như

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

cũng không khai báo kiểu trả về của hàm như

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

Bạn có thể khai báo dưới dạng một tham số hàm như thế này, nhưng về cơ bản thì nó được truyền lên.

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)

Hiện tại, tất cả những gì bạn có thể làm là:

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

Với điều này, về mặt kỹ thuật celllà giống hệt với asProtocol.

Tuy nhiên, đối với trình biên dịch, chỉ cellcó giao diện UITableViewCell, trong khi asProtocolchỉ có giao diện giao thức. Vì vậy, khi bạn muốn gọi UITableViewCellcác phương thức của, bạn phải sử dụng cellbiến. Khi bạn muốn gọi phương thức giao thức, hãy sử dụng asProtocolbiến.

Nếu bạn chắc chắn rằng ô đó tuân theo các giao thức mà bạn không phải sử dụng if let ... as? ... {}. giống:

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

Vì nhà máy chỉ định các kiểu trả về nên về mặt kỹ thuật, tôi không cần thực hiện ép kiểu tùy chọn? Tôi chỉ có thể dựa vào kiểu gõ ngầm swifts để thực hiện việc nhập mà tôi khai báo rõ ràng các giao thức?
Daniel Galasko

Tôi không hiểu ý bạn, xin lỗi vì khả năng tiếng Anh kém của tôi. Nếu bạn đang nói về -> UITableViewCell<MyProtocol>, điều này không hợp lệ, vì UITableViewCellkhông phải là một kiểu chung. Tôi nghĩ rằng điều này thậm chí không biên dịch.
rintaro

Tôi không đề cập đến việc triển khai chung của bạn mà là minh họa ví dụ của bạn về việc triển khai. nơi bạn nói chúng ta hãy asProtocol = ...
Daniel Galasko

hoặc, tôi chỉ có thể làm: tế bào var: giao thức <ProtocolOne, ProtocolTwo> = someObject như UITableViewCell và nhận được lợi ích của cả hai trong một biến
Daniel Galasko

2
Tôi không nghĩ vậy. Ngay cả khi bạn có thể làm như vậy, cellchỉ có các phương thức giao thức (cho trình biên dịch).
rintaro

2

Thật không may, Swift không hỗ trợ tuân thủ giao thức mức đối tượng. Tuy nhiên, có một công việc hơi khó xử có thể phục vụ mục đích của bạn.

struct VCWithSomeProtocol {
    let protocol: SomeProtocol
    let viewController: UIViewController

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

Sau đó, bất cứ nơi nào bạn cần làm bất cứ điều gì mà UIViewController có, bạn sẽ truy cập vào khía cạnh .viewController của struct và bất cứ thứ gì bạn cần khía cạnh giao thức, bạn sẽ tham chiếu đến .protocol.

Ví dụ:

class SomeClass {
   let mySpecialViewController: VCWithSomeProtocol

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

Bây giờ bất cứ lúc nào bạn cần mySpecialViewController để làm bất cứ điều gì liên quan đến UIViewController, bạn chỉ cần tham khảo mySpecialViewController.viewController và bất cứ khi nào bạn cần nó để thực hiện một số chức năng giao thức, bạn tham khảo mySpecialViewController.protocol.

Hy vọng rằng Swift 4 sẽ cho phép chúng ta khai báo một đối tượng với các giao thức gắn liền với nó trong tương lai. Nhưng hiện tại, điều này hoạt động.

Hi vọng điêu nay co ich!


1

CHỈNH SỬA: Tôi đã nhầm lẫn , nhưng nếu ai đó khác đọc được sự hiểu lầm này giống như tôi, tôi để câu trả lời này ra khỏi đó. OP đã hỏi về việc kiểm tra sự tuân thủ giao thức của đối tượng của một lớp con nhất định, và đó là một câu chuyện khác như câu trả lời được chấp nhận cho thấy. Câu trả lời này nói về sự tuân thủ giao thức cho lớp cơ sở.

Có thể tôi nhầm, nhưng bạn không nói về việc thêm sự tuân thủ giao thức vào UITableCellViewlớp sao? Trong trường hợp đó, giao thức được mở rộng cho lớp cơ sở chứ không phải đối tượng. Xem tài liệu của Apple về Tuyên bố Áp dụng Giao thức với Tiện ích mở rộng , trong trường hợp của bạn sẽ giống như sau:

extension UITableCellView : ProtocolOne {}

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

Ngoài tài liệu Swift đã được tham chiếu, hãy xem bài viết Nate Cooks Hàm chung cho các kiểu không tương thích với các ví dụ khác.

Điều này mang lại cho chúng ta sự linh hoạt khi có thể đối phó với việc triển khai kiểu cơ sở cũng như giao diện bổ sung được xác định trong giao thức.

Có cách nào khác rõ ràng hơn mà tôi có thể thiếu không?

Giao thức áp dụng sẽ chỉ làm điều này, làm cho một đối tượng tuân theo giao thức nhất định. Tuy nhiên, hãy lưu ý về mặt bất lợi, rằng một biến của một loại giao thức nhất định không biết bất cứ điều gì bên ngoài giao thức. Nhưng điều này có thể được phá vỡ bằng cách xác định một giao thức có tất cả các phương thức / biến / ...

Trong khi kiểu được cung cấp không hoàn toàn phù hợp với giao diện đã đề cập, đối tượng mà nhà máy trả về sẽ có và vì vậy tôi muốn sự linh hoạt trong việc tương tác với cả kiểu lớp cơ sở và giao diện giao thức đã khai báo

Nếu bạn muốn một phương thức chung, biến để phù hợp với cả giao thức và các loại lớp cơ sở, bạn có thể gặp may. Nhưng có vẻ như bạn cần xác định giao thức đủ rộng để có các phương thức tuân thủ cần thiết, đồng thời đủ hẹp để có tùy chọn áp dụng nó vào các lớp cơ sở mà không cần quá nhiều thao tác (tức là chỉ cần khai báo rằng một lớp tuân theo giao thức).


1
Đó không phải là những gì tôi đang nói đến nhưng cảm ơn :) Tôi muốn có thể giao tiếp với một đối tượng thông qua cả lớp của nó và một giao thức cụ thể. Cũng giống như cách trong obj-c tôi có thể làm NSObject <MyProtocol> obj = ... Không cần phải nói điều này không thể được thực hiện trong nhanh chóng, bạn phải cast đối tượng để giao thức của nó
Daniel Galasko

0

Tôi đã từng gặp trường hợp tương tự khi cố gắng liên kết các kết nối tương tác chung của mình trong Storyboards (IB sẽ không cho phép bạn kết nối các cửa hàng với giao thức, chỉ các phiên bản đối tượng), tôi đã gặp phải tình huống này bằng cách che giấu ivar công cộng lớp cơ sở với một máy tính riêng bất động sản. Mặc dù điều này không ngăn ai đó thực hiện các nhiệm vụ bất hợp pháp, nhưng nó cung cấp một cách thuận tiện để ngăn chặn một cách an toàn mọi tương tác không mong muốn với một cá thể không tuân thủ trong thời gian chạy. (tức là ngăn không cho gọi các phương thức đại biểu cho các đối tượng không phù hợp với giao thức.)

Thí dụ:

@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" được khai báo là tùy chọn, cũng như "protocolOutputReceiver" riêng. Bằng cách luôn truy cập outputReceiver (hay còn gọi là ủy quyền) thông qua cái sau (thuộc tính được tính toán), tôi lọc ra một cách hiệu quả mọi đối tượng không tuân theo giao thức. Bây giờ tôi có thể đơn giản sử dụng chuỗi tùy chọn để gọi ra đối tượng ủy nhiệm một cách an toàn cho dù nó có triển khai giao thức hay thậm chí tồn tại hay không.

Để áp dụng điều này cho tình huống của bạn, bạn có thể đặt ivar công khai thuộc loại "YourBaseClass?" (trái ngược với AnyObject) và sử dụng thuộc tính được tính toán riêng để thực thi tuân thủ giao thức. FWIW.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.