我想创建一个类,可以存储符合某种协议的对象。对象应该存储在类型数组中。根据 Swift 文档,协议可以用作类型:
因为它是一种类型,所以你可以在很多允许其他类型的地方使用协议,包括:
- 作为函数、方法或初始值设定项中的参数类型或返回类型
- 作为常量、变量或属性的类型
- 作为数组、字典或其他容器中的项目类型
但是以下会生成编译器错误:
协议“SomeProtocol”只能用作通用约束,因为它有 Self 或关联的类型要求
你应该如何解决这个问题:
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)
}
}
}
您遇到了 Swift 协议问题的变体,目前还没有好的解决方案。
另请参阅扩展数组以检查它是否在 Swift 中排序?,它包含有关如何解决它的建议,可能适合您的特定问题(您的问题非常通用,也许您可以使用这些答案找到解决方法).
您想要创建一个泛型类,其类型约束要求与其一起使用的类符合
SomeProtocol
,如下所示:
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)
}
}
}
在 Swift 中,有一类特殊的协议,它不提供实现它的类型的多态性。此类协议在其定义中使用
Self
或 associatedtype
关键字(Equatable
就是其中之一)。
在某些情况下,可以使用类型擦除的包装器来使您的集合同态。下面是一个例子。
// 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
我发现的有限解决方案是将协议标记为仅类协议。这将允许您使用“===”运算符来比较对象。 我知道这不适用于结构等,但对我来说已经足够好了。
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
}
}
}
}
解决方案非常简单:
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
}
}
}
现在可以使用
any
来解决这个问题,该解决方案于 2022 年 3 月随 Swift 5.6 和 Xcode 13.3 一起发布。
any
protocol SomeProtocol: Equatable {
func bla()
}
class SomeClass {
var protocols = [any SomeProtocol]()
func addElement(element: any SomeProtocol) {
protocols.append(element)
}
func removeElement(element: any SomeProtocol) {
if let index = find(protocols, element) {
protocols.remove(at: index)
}
}
}
我认为您的主要目标是保存符合某种协议的对象集合,添加到该集合并从中删除。这是您的客户端“SomeClass”中所述的功能。 Equatable 继承需要 self,但此功能不需要 self。我们可以使用“index”函数在 Obj-C 的数组中完成这项工作,该函数可以采用自定义比较器,但这在 Swift 中不受支持。因此,最简单的解决方案是使用字典而不是数组,如下面的代码所示。我提供了 getElements() ,它将返回您想要的协议数组。所以任何使用 SomeClass 的人甚至都不知道使用了字典来实现。
由于无论如何,您都需要一些区分属性来分隔您的对象,我假设它是“名称”。创建新的 SomeProtocol 实例时,请确保您的 do element.name = "foo" 。如果未设置名称,您仍然可以创建实例,但它不会被添加到集合中,并且 addElement() 将返回“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)
}
}
我在那篇博文中找到了一个not纯粹的 Swift 解决方案: http://blog.inferis.org/blog/2015/05/27/swift-an-array-of-protocols/
诀窍是遵循
NSObjectProtocol
,因为它引入了isEqual()
。
因此,您可以编写自己的函数来查找元素并将其删除,而不是使用 Equatable
协议及其默认用法 ==
。
这是您的 find(array, element) -> Int?
函数的实现:
protocol SomeProtocol: NSObjectProtocol {
}
func find(protocols: [SomeProtocol], element: SomeProtocol) -> Int? {
for (index, object) in protocols.enumerated() {
if (object.isEqual(element)) {
return index
}
}
return nil
}
注意:在这种情况下,符合
SomeProtocol
的对象必须继承自
NSObject
。