Использование протоколов в качестве типов массивов и параметров функций в swift
Я хочу создать класс, который может хранить объекты, соответствующие определенному протоколу. Объекты должны храниться в типизированном массиве. Согласно документации 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)
}
}
}
8 ответов
Вы столкнулись с вариантом проблемы с протоколами в 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
}
}
}
Я полагаю, что ваша главная цель - хранить коллекцию объектов, соответствующую какому-либо протоколу, добавлять в эту коллекцию и удалять из нее. Это функциональность, как указано в вашем клиенте "SomeClass". Для равноправного наследования требуется собственная личность, а для этой функциональности это не нужно. Мы могли бы выполнить эту работу в массивах в Obj-C, используя функцию "index", которая может принимать собственный компаратор, но это не поддерживается в Swift. Поэтому самое простое решение - использовать словарь вместо массива, как показано в коде ниже. Я предоставил getElements(), который вернет вам требуемый массив протоколов. Поэтому любой, кто использует SomeClass, даже не знает, что для реализации использовался словарь.
Так как в любом случае вам понадобится отличительное свойство для разделения ваших объектов, я предположил, что это "имя". Пожалуйста, убедитесь, что ваш do element.name = "foo" при создании нового экземпляра SomeProtocol. Если имя не задано, вы все равно можете создать экземпляр, но он не будет добавлен в коллекцию, а 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)
}
}
Начиная с 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)
}
}
}
В этом сообщении в блоге я нашел не совсем чистое решение 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
,