简体   繁体   English

在 swift 中使用协议作为数组类型和函数参数

[英]Usage of protocols as array types and function parameters in swift

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:根据 Swift 文档协议可以用作类型:

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协议“SomeProtocol”只能用作通用约束,因为它具有 Self 或关联的类型要求

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

You've hit a variant of a problem with protocols in Swift for which no good solution exists yet. 你已经遇到了Swift协议问题的一个变种,但尚未找到好的解决方案。

See also Extending Array to check if it is sorted in Swift? 另请参阅扩展数组以检查它是否在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). ,它包含有关如何解决它的建议,可能适合您的特定问题(您的问题非常通用,也许您可​​以找到使用这些答案的解决方法)。

You want to create a generic class, with a type constraint that requires the classes used with it conform to SomeProtocol , like this: 您想要创建一个泛型类,其类型约束要求与其一起使用的类符合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)
        }
    }
}

In Swift there is a special class of protocols which doesn't provide polymorphism over the types which implement it. 在Swift中有一类特殊的协议,它不提供实现它的类型的多态性。 Such protocols use Self or associatedtype keywords in their definitions (and Equatable is one of them). 此类协议在其定义中使用Selfassociatedtype关键字( Equatable就是其中之一)。

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

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

}

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

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". 这是您的客户端“SomeClass”中所述的功能。 Equatable inheritance requires self and that is not needed for this functionality. Equatable继承需要self,而这个功能不需要。 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. 我们可以使用“索引”函数在Obj-C中的数组中完成这项工作,该函数可以使用自定义比较器,但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. 我提供了getElements(),它会返回你想要的协议数组。 So anyone using SomeClass would not even know that a dictionary was used for implementation. 因此,任何使用SomeClass的人都不会知道字典是用于实现的。

Since in any case, you would need some distinguishing property to separate your objets, I have assumed it is "name". 因为在任何情况下,你需要一些区分属性来分隔你的objets,我认为它是“名字”。 Please make sure that your do element.name = "foo" when you create a new SomeProtocol instance. 在创建新的SomeProtocol实例时,请确保执行do element.name =“foo”。 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". 如果未设置名称,您仍然可以创建实例,但不会将其添加到集合中,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)
    }
}

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/ 我在博客文章中找到了一个纯粹的Swift解决方案: http//blog.inferis.org/blog/2015/05/27/swift-an-array-of-protocols/

The trick is to conform to NSObjectProtocol as it introduces isEqual() . 诀窍是遵循NSObjectProtocol因为它引入了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. 因此,您可以编写自己的函数来查找元素并将其删除,而不是使用Equatable协议及其默认用法==

Here is the implementation of your find(array, element) -> Int? 这是你的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 . 注意:在这种情况下,符合SomeProtocol的对象必须从NSObject继承。

As of Swift 5.7 / Xcode 14 this can now elegantly be solved using any .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)
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM