Usage of protocols as array types and function parameters in swift

33,300

Solution 1

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

Solution 2

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

Solution 3

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

Solution 4

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

}

Solution 5

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
        }
    }
}
Share:
33,300
snod
Author by

snod

Updated on November 11, 2020

Comments

  • snod
    snod over 3 years

    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)
            }
        }
    }
    
  • snod
    snod almost 10 years
    How would you instantiate an object of that class?
  • Nate Cook
    Nate Cook almost 10 years
    Hmmm... This way locks you into using a single type that conforms to SomeProtocol -- let protocolGroup: SomeClass<MyMemberClass> = SomeClass()
  • snod
    snod almost 10 years
    This way you could only add objects of class MyMemberClass to the array?
  • DarkDust
    DarkDust almost 10 years
    or let foo = SomeClass<MyMemberClass>()
  • Nate Cook
    Nate Cook almost 10 years
    @snod 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?
  • snod
    snod almost 10 years
    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 pointers
  • snod
    snod almost 10 years
    I think this is the correct answer for the moment. Nate's solution is working but doesn't solve my problem entirely.
  • Srivathsalachary Vangeepuram
    Srivathsalachary Vangeepuram about 8 years
    Doesn't this allow duplicate entries in protocols, if addElement is called more than once with the same object?
  • almas
    almas about 8 years
    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.
  • werediver
    werediver almost 8 years
    You missed the important thing: the OP wants the protocol to inherit Equatable protocol. It makes huge difference.
  • bzz
    bzz almost 8 years
    @werediver 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 @almas solution because is can be used with any Swift type that conforms to Equatable protocol.
  • Georgios
    Georgios almost 5 years
    You can call removeElement() before appending the new element if you wish to avoid duplicates.
  • Reimond Hill
    Reimond Hill over 4 years
    I mean how you control your array is up in the air, right? Thank you for the answer