可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I want to create a class that can store objects conforming to a certain protocol. The objects should be stored in a typed array. According to the Swift documentation protocols can be used as types:
Because it is a type, you can use a protocol in many places where other types are allowed, including:
- As a parameter type or return type in a function, method, or initializer
- As the type of a constant, variable, or property
- As the type of items in an array, dictionary, or other container
However the following generates compiler errors:
Protocol 'SomeProtocol' can only be used as a generic constraint because it has Self or associated type requirements
How are you supposed to solve this:
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)
}
}
}
回答1:
You've hit a variant of a problem with protocols in Swift for which no good solution exists yet.
See also Extending Array to check if it is sorted in Swift?, it contains suggestions on how to work around it that may be suitable for your specific problem (your question is very generic, maybe you can find a workaround using these answers).
回答2:
You want to create a generic class, with a type constraint that requires the classes used with it conform to SomeProtocol
, like this:
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)
}
}
}
回答3:
In Swift there is a special class of protocols which doesn't provide polymorphism over the types which implement it. Such protocols use Self
or associatedtype
keywords in their definitions (and Equatable
is one of them).
In some cases it's possible to use a type-erased wrapper to make your collection homomorphic. Below is an example.
// 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
回答4:
The limited solution that I found is to mark the protocol as a class-only protocol. This will allow you to compare objects using '===' operator.
I understand this won't work for structs, etc., but it was good enough in my case.
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
}
}
}
}
回答5:
The solution is pretty simple:
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
}
}
}
回答6:
I take it that your main aim is to hold a collection of objects conforming to some protocol, add to this collection and delete from it. This is the functionality as stated in your client, "SomeClass". Equatable inheritance requires self and that is not needed for this functionality. We could have made this work in arrays in Obj-C using "index" function that can take a custom comparator but this is not supported in Swift. So the simplest solution is to use a dictionary instead of an array as shown in the code below. I have provided getElements() which will give you back the protocol array you wanted. So anyone using SomeClass would not even know that a dictionary was used for implementation.
Since in any case, you would need some distinguishing property to separate your objets, I have assumed it is "name". Please make sure that your do element.name = "foo" when you create a new SomeProtocol instance. If the name is not set, you can still create the instance, but it won't be added to the collection and addElement() will return "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)
}
}
回答7:
I found a not pure-pure Swift solution on that blog post:
http://blog.inferis.org/blog/2015/05/27/swift-an-array-of-protocols/
The trick is to conform to NSObjectProtocol
as it introduces isEqual()
.
Therefore instead of using the Equatable
protocol and its default usage of ==
you could write your own function to find the element and remove it.
Here is the implementation of your find(array, element) -> Int?
function:
protocol SomeProtocol: NSObjectProtocol {
}
func find(protocols: [SomeProtocol], element: SomeProtocol) -> Int? {
for (index, object) in protocols.enumerated() {
if (object.isEqual(element)) {
return index
}
}
return nil
}
Note: In this case your objects conforming to SomeProtocol
must inherits from NSObject
.