croX
croX

Reputation: 1725

Swift Dictionary with Protocol Type as Key

short question here:

I got a protocol protocol SCResourceModel {..} and a dictionary which shall use this protocol type as key: [SCResourceModel : String]. This obviously does not work, as a key in a dictionary has to conform to the protocol Hashable. Making my SCResourceModel inherit from Hashable or trying something like this [protocol<SCResourceModel, Hashable> : String] obviously does not work as well, since Hashable or Equatable can only be used as generic constraints and not as types themselves.

I watched the WWDC 2015 and in Swift 2.0 it is possible to add constraints to a protocol like: protocol SCResourceModel where Self: Hashable {..} which directly addresses this issue (very nice).

Anyway, my question is: Can I do something similar with the current Swift 1.2 version and somehow use this protocol as the key of a dictionary? Or can anyone propose a nice workaround or something else that I might have overlooked?

The only solution I see at the moment for Swift 1.2, is to transform the protocol to a class which inherits from e.g. NSObject and must be subclassed for further usage in my API.

Thanks for your help!

Upvotes: 6

Views: 7430

Answers (4)

nightwill
nightwill

Reputation: 818

Actually you can't use a protocol as a key, but you can use a workaround.

protocol PropertyStoreKey {
    
    associatedtype Value
    
}

final class PropertyStore {
    
    private var propertyList: [Any] = []
    
    public subscript<K: PropertyStoreKey>(key: K.Type) -> K.Value? {
        get {
            value(of: key)
        }
        set(newValue) {
            update(value: newValue, of: key)
        }
    }
    
    private func value<K: PropertyStoreKey>(of type: K.Type) -> K.Value? {
        guard let index = self.index(of: type) else {
            return nil
        }
        return (propertyList[index] as? PropertyItem<K>)?.value
    }
    
    private func update<K: PropertyStoreKey>(value: K.Value?, of type: K.Type) {
        if let index = self.index(of: type) {
            if let value = value {
                guard var existedItem = propertyList[index] as? PropertyItem<K> else {
                    return
                }
                existedItem.value = value
                propertyList[index] = existedItem
            } else {
                propertyList.remove(at: index)
            }
        } else {
            guard let value = value else {
                return
            }
            let newItem = PropertyItem(type: type, value: value)
            propertyList.append(newItem)
        }
    }
    
    private func index<K: PropertyStoreKey>(of type: K.Type) -> Int? {
        propertyList.firstIndex(where: { $0 is PropertyItem<K> })
    }
    
}

private struct PropertyItem<K: PropertyStoreKey> {
    let type: K.Type
    var value: K.Value
}

Usage:

struct TimeKey: PropertyStoreKey {
    typealias Value = Double
}

let propertyStore = PropertyStore()
propertyStore[TimeKey.self] = 5

Upvotes: 1

Rahul
Rahul

Reputation: 2100

Why not use AnyHashable?

  protocol SCResourceModel: Hashable {}
  extension SCResourceModel { // Implement the hashable required methods }

  struct SCResource: SCResourceModel {}

Now declare a Dictionary like this:

 var container = Dictionary<AnyHashable, String>
 contianer[SCResource] = "My first resource"

Upvotes: 0

0x416e746f6e
0x416e746f6e

Reputation: 10136

I would probably think in the direction of:

protocol SCResourceModel {
    var hashValue: Int { get }
    func isEqualTo(another: SCResourceModel) -> Bool

    // ...
}

struct SCResourceModelWrapper: Equatable, Hashable {
    let model: SCResourceModel

    var hashValue: Int {
        return model.hashValue ^ "\(model.dynamicType)".hashValue
    }
}

func == (lhs: SCResourceModelWrapper, rhs: SCResourceModelWrapper) -> Bool {
    return lhs.model.isEqualTo(rhs.model)
}

struct SCResourceModelDictionary<T> {
    private var storage = [SCResourceModelWrapper: T]()

    subscript(key: SCResourceModel) -> T? {
        get {
            return storage[SCResourceModelWrapper(model: key)]
        }
        set {
            storage[SCResourceModelWrapper(model: key)] = newValue
        }
    }
}

Upvotes: 6

dillondrenzek
dillondrenzek

Reputation: 73

Alright, so as far as I can tell, there isn't a really nice way to have the protocol itself as a key. But I'm pretty sure a string version of the protocol name would work nicely for your purpose. As a bonus, you can also have protocol objects as the value in a dictionary (if that is useful in your circumstance)

Thing is, I can't find a nice way to do this in Swift either, but here is what I've come up with in Objective-C, maybe you would be better than me at finding a way to do this in Swift:

// With a protocol declared...
@protocol TestProtocol <NSObject>
@end

@implementation

// Inside the implementation you can use NSStringFromProtocol()
Protocol *proto = @protocol(TestProtocol);
NSLog(@"Protocol: %@", NSStringFromProtocol(proto));

@end

outputs:

Protocol: TestProtocol

The @protocol part of that code is the part I'm not real sure how to do in Swift and if worse comes to worst, you can always bridge to an Objective-C file. Hope this gives you some ideas!

Upvotes: 2

Related Questions