Usage of protocols as array types and function parameters in Swift
Asked Answered
L

9

146

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)
        }
    }
}
Lungi answered 22/7, 2014 at 13:21 Comment(1)
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 in its definition (and Equatable is one of them). In some cases it's possible to use a type-erased wrapper to make your collection homomorphic. Look here for example.Radloff
J
2

This can now be solved using any, released with Swift 5.6 and Xcode 13.3 in March 2022.

SE-0335: Introduce existential 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)
        }
    }
}
Joule answered 23/7, 2022 at 23:6 Comment(1)
Do you know of a way to use a [any Protocol] array as a parameter value to a function that expects [Protocol]? I was running into that issue working with charts. A method wanted [Plottable] and I had a class where I need to specify the return as [any Plottable] because it could have been an array of Int or Double. The method wouldn't accept my [any Plottable] array though.Metaxylem
W
56

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

Wisp answered 22/7, 2014 at 14:25 Comment(1)
I think this is the correct answer for the moment. Nate's solution is working but doesn't solve my problem entirely.Lungi
R
36

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)
        }
    }
}
Rumor answered 22/7, 2014 at 13:54 Comment(6)
How would you instantiate an object of that class?Lungi
Hmmm... This way locks you into using a single type that conforms to SomeProtocol -- let protocolGroup: SomeClass<MyMemberClass> = SomeClass()Rumor
This way you could only add objects of class MyMemberClass to the array?Lungi
or let foo = SomeClass<MyMemberClass>()Wisp
@Lungi Yeah, which isn't what you're looking for. The issue is Equatable conformance - without that you can use your exact code. Maybe file a bug/feature request?Rumor
I need the Equatable conformance because otherwise you can't search the array using the find function. A workaround could be to iterate manually over the array and compare pointersLungi
R
19

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
Radloff answered 25/7, 2016 at 13:47 Comment(0)
A
13

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

}
Absenteeism answered 9/2, 2016 at 21:55 Comment(4)
Doesn't this allow duplicate entries in protocols, if addElement is called more than once with the same object?Bosworth
Yes, arrays in swift may contain duplicate entries. If you think that this may happen in your code, then either use the Set instead of array, or make sure that array doesn't contain that object already.Absenteeism
You can call removeElement() before appending the new element if you wish to avoid duplicates.Roorback
I mean how you control your array is up in the air, right? Thank you for the answerDirge
P
8

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
        }
    }
}
Perusse answered 23/7, 2016 at 13:15 Comment(2)
You missed the important thing: the OP wants the protocol to inherit Equatable protocol. It makes huge difference.Radloff
@Radloff I don't think so. He wants to store objects conforming to SomeProtocol in a typed array. Equatable conformance is required only for removing elements from array. My solution is an improved version of @Absenteeism solution because is can be used with any Swift type that conforms to Equatable protocol.Perusse
J
2

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)
    }
}
Jannajannel answered 26/7, 2016 at 16:18 Comment(0)
J
2

This can now be solved using any, released with Swift 5.6 and Xcode 13.3 in March 2022.

SE-0335: Introduce existential 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)
        }
    }
}
Joule answered 23/7, 2022 at 23:6 Comment(1)
Do you know of a way to use a [any Protocol] array as a parameter value to a function that expects [Protocol]? I was running into that issue working with charts. A method wanted [Plottable] and I had a class where I need to specify the return as [any Plottable] because it could have been an array of Int or Double. The method wouldn't accept my [any Plottable] array though.Metaxylem
H
1

It's very easy to get into the weeds on this with generics and Associated Types and existential types and any and some and whatever else.

You can instead use an enum with associated values. The array is storing the "same type" (the enum), which Swift likes, but each case of the enum holds a different object type as the associated value. These objects can then be made to all conform to a protocol.

enum AnimalEnum: Equatable, Codable {
    case dog(Dog)
    case cat(Cat)
    
    var animal: any Animal {
        switch self {
        case .dog(let dog): dog
        case .cat(let cat): cat
        }
    }
}

struct SomeStruct: Equatable, Codable {
    
    // ERROR:
    var animalProtocols: [any Animal] = [Dog(), Cat()]
    // Type 'SomeStruct' does not conform to protocol 'Equatable'
    // Type 'SomeStruct' does not conform to protocol 'Decodable'
    // And various other errors and complications...
    
    // Instead, try...
    var animals: [AnimalEnum] = [.dog(Dog()), .cat(Cat())]
    
    func playWithAnimals() {
        animals.forEach {
            let animal = $0.animal // any Animal
            animal.makeNoise()
            
            switch $0 {
            case .dog(let dog):
                dog.doDogThings()
            case .cat(let cat):
                cat.doCatThings()
            }
        }
    }
}

protocol Animal: Equatable, Codable {
    func makeNoise()
}

struct Dog: Animal {
    func makeNoise() { print("bark") }
    func doDogThings() { print("dig") }
}

struct Cat: Animal {
    func makeNoise() { print("meow") }
    func doCatThings() { print("doze") }
}

Note that this works for both Equatable and Codable, which has been difficult for me to accomplish in other manners.

Probably not a great solution if you are writing a maintainable API contract with generics or whatever. But when I've needed this often what I really wanted was to just store 2 or 3 similar but different types in a single array, in my own code. This will work just fine and is easy and conceptually simple to implement.

Doing this technique via enums was enabled by SE-0295: Codable synthesis for enums with associated values, which was released with Swift 5.5 in 2021. You could also previously do it with a struct that emulated the behavior of an enum, which you can see in the edit history of this answer.

Hamza answered 25/4 at 21:46 Comment(0)
C
0

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.

Cowrie answered 10/11, 2018 at 10:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.