在Swift中,协议可用作数组类型和函数参数的使用方法

146

我想创建一个类,可以存储符合特定协议的对象。这些对象应该存储在类型化数组中。根据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)
        }
    }
}

2
在Swift中,有一类特殊的协议,它不提供对实现它的类型的多态性。这些协议在其定义中使用Self或associatedtype(其中Equatable是其中之一)。在某些情况下,可以使用类型擦除包装器使您的集合同态化。例如,请参见此处 - werediver
8个回答

55
你遇到了Swift协议中不存在良好解决方案的问题变体。
另请参考扩展数组以检查其是否已排序,其中包含适用于您特定问题的解决方法(您的问题非常通用,也许您可以使用这些答案找到解决方法)。

1
我认为这是目前正确的答案。Nate的解决方案可以工作,但并不能完全解决我的问题。 - snod

36
您想创建一个通用的类,并带有类型约束,要求与之一起使用的类符合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)
        }
    }
}

你会如何实例化该类的对象? - snod
1
嗯... 这种方式让你只能使用符合SomeProtocol的单个类型 -- let protocolGroup: SomeClass<MyMemberClass> = SomeClass() - Nate Cook
1
这样你只能将 MyMemberClass 类的对象添加到数组中? - snod
让foo等于SomeClass<MyMemberClass>() - DarkDust
@snod 是的,这不是你要找的。问题在于Equatable一致性 - 没有它,你就无法使用你的代码。也许可以提交一个错误/功能请求? - Nate Cook
我需要实现 Equatable 协议,否则你将无法使用 find 函数搜索数组。一个解决方法是手动迭代数组并比较指针。 - snod

19

在Swift中,有一类特殊的协议不提供对实现它的类型的多态性。这些协议在其定义中使用Selfassociatedtype关键字(其中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

13

我找到的有限解决方案是将协议标记为仅类协议。这将允许您使用'==='运算符比较对象。我知道这对于结构体等可能不起作用,但在我的情况下已经足够好了。

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

}

如果使用addElement方法多次添加相同的对象,这是否会在protocols中允许重复条目? - Tom Harrington
是的,在Swift中,数组可以包含重复的条目。如果您认为这可能发生在您的代码中,则可以使用Set而不是数组,或者确保数组不包含该对象。 - almas
如果您希望避免重复,可以在附加新元素之前调用 removeElement() - Georgios
我的意思是,你如何控制你的数组还没有确定,对吧?谢谢你的回答。 - Reimond Hill

8
解决方案非常简单:
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
        }
    }
}

5
你错过了重要的事情:OP想让协议继承Equatable协议,这非常重要。 - werediver
@werediver 我不这么认为。他想要在一个类型化的数组中存储符合 SomeProtocol 的对象。只有在从数组中删除元素时才需要符合 Equatable 协议。我的解决方案是 @almas 解决方案的改进版本,因为它可以用于符合 Equatable 协议的任何 Swift 类型。 - bzz

2
我理解你的主要目的是保存符合某个协议的对象集合,并向其中添加和删除元素。这正是你的客户端"SomeClass"所述的功能。Equatable继承需要self,而这对于此功能是不必要的。我们可以使用Obj-C中的数组通过“index”函数来实现自定义比较器,但Swift不支持。因此,最简单的解决方案是使用字典代替数组,如下所示的代码。我已经提供了getElements()函数,它将返回您想要的协议数组。因此,任何使用SomeClass的人甚至不知道字典被用于实现。
由于在任何情况下,您都需要一些区分属性来区分您的对象,我假设它是“name”。请确保在创建新的SomeProtocol实例时执行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)
    }
}

1
从 Swift 5.7 / Xcode 14 开始,可以使用 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)
        }
    }
}

你知道如何将一个[任意协议]数组作为参数传递给期望 [协议] 的函数吗?我在处理图表时遇到了这个问题。有一个方法需要 [可绘制],而我有一个类,需要指定返回值为 [任意可绘制],因为它可能是 Int 或 Double 数组。但是该方法无法接受我的 [任意可绘制] 数组。 - chasepeeler

0

我在那篇博客文章中找到了一个不是纯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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接