4

I am having difficulty using Swift Dictionaries to store generic objects.

I am trying to stores objects in the following way:

[C: Container<C>], where C: ContentType

Such that the dictionary is structured as below:

Key        Value
---        -----
A          Container<A>
B          Container<B>
...

ContentType

I have a class called "ContentType" which stores metadata about a type of content (i.e. name, size, weight, etc).

class ContentType: NSObject { ... }

I have many different subclasses of "ContentType", each subclass describing a particular type of content.

class ContentTypeA: ContentType { ... }
class ContentTypeB: ContentType { ... }

Container

I also have a class called "Container" which is designed to store information relating to the content type. Appropriately, it is a generic class, with it's generic type as "ContentType".

class Container<C: ContentType>: NSObject { }

I have also implemented extensions on "Container" that define functions for specific "ContentType".

extension Container where C: ContentTypeA {
    func fancyFunctionOnlyFoundInContentTypeA() { }
}

extension Container where C: ContentTypeB {
    func fancyFunctionOnlyFoundInContentTypeB() { }
}

Controller

In my main controller class, I have implemented the following subscript overloader.

subscript<C: ContentType>(contentType: C) -> Container<C> {
    // If Container for ContentType "C" does not exist
    if (self.containers[contentType] == nil) {
        // Then create it
        self.containers[contentType] = (Container<C>.init() as! Container<ContentType>)
    }
    // And return it
    return self.containers[contentType]! as! Container<C>
}

The idea is that you can obtain/reuse a "Container" of a particular "ContentType" and it will be returned ready.

// Define a ContentType singletons somewhere
static let ContentTypeASingleton = ContentTypeA.init()
static let ContentTypeBSingleton = ContentTypeA.init()
...
// Using the subscript, the Container for ContentTypeA/ContentTypeB can be obtained
let containerForTypeA = self[ContentTypeASingleton]
let containerForTypeB = self[ContentTypeBSingleton]

From here, it would be possible to call the Container extensions, specific to each ContentType.

containerForTypeA.fancyFunctionOnlyFoundInContentTypeA()
containerForTypeB.fancyFunctionOnlyFoundInContentTypeB()

The Problem

I believe all of this should work theoretically, and it certainly does compile in Swift 4, Xcode 9.0. However, upon executing the following:

// Then create it
self.containers[contentType] = (Container<C>.init() as! Container<ContentType>)

I get the following error:

Could not cast value of type 'Test.Container<Test.ContentTypeA>' (0x109824a00) to 'Test.Container<Test.ContentType>' (0x109824638).

As ContentTypeA inherits from ContentType, I don't understand why this is a problem. It seems Swift Dictionaries are unable to store objects of type ContentType < AnySubclassOfContentType > , and must store objects as ContentType < ContentType > .

Does anyone have any suggestions? Cheers.

Full code below:

class ViewController: UIViewController {

    var containers = [ContentType : Container<ContentType>]()

    override func viewDidLoad() {
        super.viewDidLoad()

        let contentTypeA = ContentTypeA.init()
        let contentTypeB = ContentTypeB.init()

        let containerForTypeA = self[contentTypeA]
        let containerForTypeB = self[contentTypeB]

        containerForTypeA.fancyFunctionOnlyFoundInContentTypeA()
        containerForTypeB.fancyFunctionOnlyFoundInContentTypeB()
    }

    subscript<C: ContentType>(contentType: C) -> Container<C> {
        if (self.containers[contentType] == nil) {
            self.containers[contentType] = (Container<C>.init() as! Container<ContentType>)
        }
        return self.containers[contentType]! as! Container<C>
    }

}

class ContentType: NSObject { }
class Container<C: ContentType>: NSObject { }
class ContentTypeA: ContentType { }
class ContentTypeB: ContentType { }

extension Container where C: ContentTypeA {
    func fancyFunctionOnlyFoundInContentTypeA() { }
}

extension Container where C: ContentTypeB {
    func fancyFunctionOnlyFoundInContentTypeB() { }
}
Luke Fletcher
  • 348
  • 2
  • 12
  • 3
    Does Swift support *contravariant* generics (and related casts)? – user2864740 Oct 14 '17 at 23:11
  • `Test.Container` has basically nothing to do with `Test.Container` since the generics are invariant. Consider a `set`ter `Test.Container` which sets a variable of the generic type: what would happen if you "upcast" the Container to a more general generic, then call the setter with an instance of that super class? And then resume using the uncasted object, what value would the variable now have? – luk2302 Oct 14 '17 at 23:19

1 Answers1

4

The crash happens because Swift generics are invariant, thus Container<B> is not a subclass of Container<A>, even if B would be a subclass of A, and any downcasts will fail, forced casts leading to crash.

You can work around this by using AnyObject as base class in your dictionary, as Container is derived from NSObject:

var containers = [ContentType : AnyObject]()

subscript<C: ContentType>(contentType: C) -> Container<C> {
    if let container = containers[contentType] {
        return container as! Container<C>
    } else {
        let container = Container<C>()
        containers[contentType] = container
        return container
    }
}
Cristik
  • 30,989
  • 25
  • 91
  • 127
  • This should do it ;) Going forward, if this code grows more complex, creating a non-generic `Container` superclass might be worth considering as well. – Paulo Mattos Oct 15 '17 at 00:41
  • Thanks Cristik (and sorry for the slow reply)! Your solution works brilliantly, but I have since abandoned my attempts at using generics in this example because they have proven to be more trouble than they are worth. Cheers! – Luke Fletcher Oct 24 '17 at 09:50
  • @LukeFletcher yeah, sometimes generics add unnecessary complexity, and if type safety can be achieved via more conventional ways, then it's recommendable to go those ways. – Cristik Oct 24 '17 at 12:13