Sử dụng các giao thức như các kiểu mảng và tham số hàm trong swift


136

Tôi muốn tạo một lớp có thể lưu trữ các đối tượng tuân theo một giao thức nhất định. Các đối tượng nên được lưu trữ trong một mảng đánh máy. Theo các giao thức tài liệu Swift có thể được sử dụng như các loại: 

Vì là loại nên bạn có thể sử dụng giao thức ở nhiều nơi cho phép các loại khác, bao gồm:

  • Là kiểu tham số hoặc kiểu trả về trong hàm, phương thức hoặc trình khởi tạo
  • Là loại hằng số, biến hoặc thuộc tính
  • Là loại mục trong một mảng, từ điển hoặc vùng chứa khác

Tuy nhiên, sau đây tạo ra các lỗi biên dịch:

Giao thức 'someProtocol' chỉ có thể được sử dụng như một ràng buộc chung vì nó có các yêu cầu loại Tự hoặc liên quan

Làm thế nào bạn có nghĩa vụ để giải quyết điều này:

protocol SomeProtocol: Equatable {
    func bla()
}

class SomeClass {
    
    var protocols = [SomeProtocol]()
    
    func addElement(element: SomeProtocol) {
        self.protocols.append(element)
    }
    
    func removeElement(element: SomeProtocol) {
        if let index = find(self.protocols, element) {
            self.protocols.removeAtIndex(index)
        }
    }
}

2
Trong Swift có một lớp giao thức đặc biệt không cung cấp tính đa hình đối với các kiểu thực hiện nó. Các giao thức như vậy sử dụng Self hoặc Associtype trong định nghĩa của nó (và Equitable là một trong số chúng). Trong một số trường hợp, có thể sử dụng trình bao bọc loại để làm cho bộ sưu tập của bạn đồng nhất. Nhìn vào đây chẳng hạn.
werediver

Câu trả lời:


48

Bạn đã gặp phải một biến thể của một vấn đề với các giao thức trong Swift mà chưa có giải pháp tốt nào tồn tại.

Xem thêm Mở rộng mảng để kiểm tra xem nó có được sắp xếp trong Swift không? , nó chứa các đề xuất về cách giải quyết xung quanh nó có thể phù hợp với vấn đề cụ thể của bạn (câu hỏi của bạn rất chung chung, có thể bạn có thể tìm ra cách giải quyết bằng các câu trả lời này).


1
Tôi nghĩ rằng đây là câu trả lời chính xác cho thời điểm này. Giải pháp của Nate đang hoạt động nhưng không giải quyết được hoàn toàn vấn đề của tôi.
snod

32

Bạn muốn tạo một lớp chung, với một ràng buộc kiểu yêu cầu các lớp được sử dụng với nó tuân theo SomeProtocol, như thế này:

class SomeClass<T: SomeProtocol> {
    typealias ElementType = T
    var protocols = [ElementType]()

    func addElement(element: ElementType) {
        self.protocols.append(element)
    }

    func removeElement(element: ElementType) {
        if let index = find(self.protocols, element) {
            self.protocols.removeAtIndex(index)
        }
    }
}

Làm thế nào bạn có thể khởi tạo một đối tượng của lớp đó?
snod

Hmmm ... Cách này khóa bạn bằng cách sử dụng một loại duy nhất phù hợp với SomeProtocol-let protocolGroup: SomeClass<MyMemberClass> = SomeClass()
Nate Cook

Bằng cách này bạn chỉ có thể thêm các đối tượng của lớp MyMemberClassvào mảng?
snod

hoặclet foo = SomeClass<MyMemberClass>()
DarkDust

@snod Vâng, đó không phải là những gì bạn đang tìm kiếm. Vấn đề là Equatablesự phù hợp - mà không có điều đó bạn có thể sử dụng mã chính xác của mình. Có thể gửi một yêu cầu lỗi / tính năng?
Nate Cook

15

Trong Swift có một lớp giao thức đặc biệt không cung cấp tính đa hình đối với các kiểu thực hiện nó. Các giao thức như vậy sử dụng Selfhoặc associatedtypetừ khóa trong định nghĩa của họ (và Equatablelà một trong số họ).

Trong một số trường hợp, có thể sử dụng trình bao bọc loại để làm cho bộ sưu tập của bạn đồng nhất. Dưới đây là một ví dụ.

// This protocol doesn't provide polymorphism over the types which implement it.
protocol X: Equatable {
    var x: Int { get }
}

// We can't use such protocols as types, only as generic-constraints.
func ==<T: X>(a: T, b: T) -> Bool {
    return a.x == b.x
}

// A type-erased wrapper can help overcome this limitation in some cases.
struct AnyX {
    private let _x: () -> Int
    var x: Int { return _x() }

    init<T: X>(_ some: T) {
        _x = { some.x }
    }
}

// Usage Example

struct XY: X {
    var x: Int
    var y: Int
}

struct XZ: X {
    var x: Int
    var z: Int
}

let xy = XY(x: 1, y: 2)
let xz = XZ(x: 3, z: 4)

//let xs = [xy, xz] // error
let xs = [AnyX(xy), AnyX(xz)]
xs.forEach { print($0.x) } // 1 3

12

Giải pháp hạn chế mà tôi tìm thấy là đánh dấu giao thức là giao thức chỉ dành cho lớp. Điều này sẽ cho phép bạn so sánh các đối tượng bằng toán tử '==='. Tôi hiểu điều này sẽ không hoạt động cho các cấu trúc, vv, nhưng nó là đủ tốt trong trường hợp của tôi.

protocol SomeProtocol: class {
    func bla()
}

class SomeClass {

    var protocols = [SomeProtocol]()

    func addElement(element: SomeProtocol) {
        self.protocols.append(element)
    }

    func removeElement(element: SomeProtocol) {
        for i in 0...protocols.count {
            if protocols[i] === element {
                protocols.removeAtIndex(i)
                return
            }
        }
    }

}

Điều này không cho phép các mục trùng lặp trong protocols, nếu addElementđược gọi nhiều lần với cùng một đối tượng?
Tom Harrington

Có, mảng trong swift có thể chứa các mục trùng lặp. Nếu bạn nghĩ rằng điều này có thể xảy ra trong mã của bạn, thì hãy sử dụng Tập thay vì mảng hoặc đảm bảo rằng mảng đó không chứa đối tượng đó.
almas

Bạn có thể gọi removeElement()trước khi nối phần tử mới nếu bạn muốn tránh trùng lặp.
Georgios

Tôi có nghĩa là làm thế nào bạn kiểm soát mảng của bạn là trong không khí, phải không? Cảm ơn bạn đã trả lời
Reimond Hill

9

Giải pháp khá đơn giản:

protocol SomeProtocol {
    func bla()
}

class SomeClass {
    init() {}

    var protocols = [SomeProtocol]()

    func addElement<T: SomeProtocol where T: Equatable>(element: T) {
        protocols.append(element)
    }

    func removeElement<T: SomeProtocol where T: Equatable>(element: T) {
        protocols = protocols.filter {
            if let e = $0 as? T where e == element {
                return false
            }
            return true
        }
    }
}

4
Bạn đã bỏ lỡ điều quan trọng: OP muốn giao thức kế thừa Equatablegiao thức. Nó làm cho sự khác biệt rất lớn.
werediver

@werediver Mình không nghĩ vậy. Anh ta muốn lưu trữ các đối tượng phù hợp SomeProtocoltrong một mảng đánh máy. Equatablesự phù hợp chỉ được yêu cầu để loại bỏ các yếu tố từ mảng. Giải pháp của tôi là phiên bản cải tiến của giải pháp @almas vì có thể được sử dụng với bất kỳ loại Swift nào phù hợp với Equatablegiao thức.
bzz

2

Tôi cho rằng mục đích chính của bạn là giữ một bộ sưu tập các đối tượng tuân theo một số giao thức, thêm vào bộ sưu tập này và xóa khỏi nó. Đây là chức năng như đã nêu trong ứng dụng khách của bạn, "Một số kính". Kế thừa tương đương đòi hỏi bản thân và điều đó là không cần thiết cho chức năng này. Chúng tôi có thể thực hiện công việc này trong các mảng trong Obj-C bằng cách sử dụng chức năng "index" có thể lấy một bộ so sánh tùy chỉnh nhưng điều này không được hỗ trợ trong Swift. Vì vậy, giải pháp đơn giản nhất là sử dụng từ điển thay vì một mảng như trong đoạn mã dưới đây. Tôi đã cung cấp getElements () sẽ cung cấp cho bạn mảng giao thức bạn muốn. Vì vậy, bất cứ ai sử dụng someClass thậm chí sẽ không biết rằng một từ điển đã được sử dụng để thực hiện.

Vì trong mọi trường hợp, bạn sẽ cần một số thuộc tính riêng biệt để phân tách các chướng ngại vật của mình, tôi đã cho rằng đó là "tên". Vui lòng đảm bảo rằng phần tử do.name = "foo" của bạn khi bạn tạo một phiên bản someProtocol mới. Nếu tên không được đặt, bạn vẫn có thể tạo cá thể, nhưng nó sẽ không được thêm vào bộ sưu tập và addEuity () sẽ trả về "false".

protocol SomeProtocol {
    var name:String? {get set} // Since elements need to distinguished, 
    //we will assume it is by name in this example.
    func bla()
}

class SomeClass {

    //var protocols = [SomeProtocol]() //find is not supported in 2.0, indexOf if
     // There is an Obj-C function index, that find element using custom comparator such as the one below, not available in Swift
    /*
    static func compareProtocols(one:SomeProtocol, toTheOther:SomeProtocol)->Bool {
        if (one.name == nil) {return false}
        if(toTheOther.name == nil) {return false}
        if(one.name ==  toTheOther.name!) {return true}
        return false
    }
   */

    //The best choice here is to use dictionary
    var protocols = [String:SomeProtocol]()


    func addElement(element: SomeProtocol) -> Bool {
        //self.protocols.append(element)
        if let index = element.name {
            protocols[index] = element
            return true
        }
        return false
    }

    func removeElement(element: SomeProtocol) {
        //if let index = find(self.protocols, element) { // find not suported in Swift 2.0


        if let index = element.name {
            protocols.removeValueForKey(index)
        }
    }

    func getElements() -> [SomeProtocol] {
        return Array(protocols.values)
    }
}

0

Tôi tìm thấy một giải pháp Swift không thuần túy trên bài đăng trên blog đó: http://blog.inferis.org/blog/2015/05/27/swift-an-array-of-prot Protocol /

Bí quyết là tuân thủ NSObjectProtocolnhư nó giới thiệu isEqual(). Do đó, thay vì sử dụng Equatablegiao thức và cách sử dụng mặc định của nó, ==bạn có thể viết hàm riêng của mình để tìm phần tử và xóa nó.

Đây là việc thực hiện find(array, element) -> Int?chức năng của bạn :

protocol SomeProtocol: NSObjectProtocol {

}

func find(protocols: [SomeProtocol], element: SomeProtocol) -> Int? {
    for (index, object) in protocols.enumerated() {
        if (object.isEqual(element)) {
            return index
        }
    }

    return nil
}

Lưu ý: Trong trường hợp này, các đối tượng của bạn tuân thủ SomeProtocolphải kế thừa từ NSObject.

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.