4

I have an interesting problem:

class ListCache {

    public func getCachedList<T: Codable>() -> [T]? {
        //loads list from file cache using Codable
    }

}

let's say I have class Foo:

class Foo: Codable {
    var bar = ""
}

Now, I can do something like this:

let array: [Foo] = ListCache().getCachedList()

but I cannot do something like this:

var listsToLoad: [AnyClass] = [Foo.self]
let clazz = listsToLoad[0]
let array: [Codable] = ListCache().getCachedList()

Compiler gives me an error:

Cannot explicitly specialize a generic function

This means I cannot call getCachedList() in a loop because I have to explicitly give it a class type.

Is there a way to achieve this? I've also tried using generic classes, but I pretty much end in the same point.

Edit:

I've tried creating:

class CodableClass: Codable {

}

then:

class Foo: CodableClass {
    //...
}

and now compiler says clazz is undeclared:

var listsToLoad: [CodableClass.Type] = [Foo.self]
for clazz in listsToLoad {
    if let array: [clazz] = ListCache().getCachedList() {
        print(array.count)
    }
}

I've tried clazz.Type and clazz.self as well.

Makalele
  • 7,431
  • 5
  • 54
  • 81
  • Why do yo need to use generics here? Can't you just use `public func getCachedList() -> [Codable]?`. I dont really understand what you are doing in that last block of code. `var listsToLoad: [AnyClass] = [Foo.self]` doesn't make sense to me, the assignment looks more like a type definition? – Scriptable Mar 23 '18 at 14:16
  • That's because in getCachedList I load json from file and using Codable I'm converting it to [T] array. – Makalele Mar 23 '18 at 14:20
  • You are trying to create array of Codable object by `let array: [Codable] = ListCache().getCachedList()`. but protocal 'Codable' cannot be constructed because it has no accessible initializers. – deoKasuhal Mar 23 '18 at 22:17
  • Is there a way to use different thing than Codable to use array like this? – Makalele Mar 24 '18 at 08:16
  • Another suggestion would be: Stop implementing a cache by yourself and let [URLCache](https://developer.apple.com/documentation/foundation/urlcache) do all the work. In your requests you can specify the caching strategy and your server could add `Cache-Control` information in the response header. – heyfrank Mar 26 '18 at 11:08
  • What's the reason for the array? I'm not sure what you're trying to solve by introducing it (over just requesting with the concrete type when you need them). – Oliver Atkinson Mar 29 '18 at 15:05
  • @fl034 I cannot change server. Oliver Atkinson Just for less code, because everything is repeating, just class changes. – Makalele Mar 30 '18 at 06:44
  • The value stored in `clazz` is determined at runtime, while generics need to be resolved at compile time, meaning you cannot use the contents of a variable to call a generic function. – Cristik Mar 30 '18 at 22:33
  • I mean, you *could* hack this [with a protocol extension](https://gist.github.com/hamishknight/6473352f5891c2bf8be60307e2755645) (compare https://stackoverflow.com/q/45234233/2976878). But what would you do with the resulting `[Decodable]`? You'd need to do some type casting to concrete type(s); so why not decode with those concrete type(s) to begin with? – Hamish Apr 01 '18 at 17:08

2 Answers2

4

I'm fairly certain that in order to call a generic function, that function's parameter types have to be known at compile time. And since [Foo] is really just a shorthand for Array<Foo>, that applies to typed arrays, too (try writing Array<clazz>; it won't compile). So when you have a type in a clazz variable, such that the actual class is only known at runtime, you can't use it to call generic functions, nor can you declare an array typed to that class.

AFAIK, the only solutions are to:

  1. Find a way to do what you're trying to do dynamically, Objective-C-style, using [Codable], or:

  2. Find another way to do what you're trying to do :-(

Since I don't know what it is that you're ultimately trying to do, it's hard to go too far beyond that, unfortunately.

Charles Srstka
  • 16,665
  • 3
  • 34
  • 60
  • In "real" app I have several lists downloaded from an external server when the app starts. However, using Codable, I wanted to cache these lists and don't always download them. So, I wanted to create an Array of classes, pass it to the function, which will load from cache and if it's not possible or data is too old will download it from an external server. I didn't want to copy-paste all code, but rather do this nicely in an array. – Makalele Mar 26 '18 at 07:51
  • @Makalele You could define an enum with a case for each data source, and just switch over the enum, calling `getCachedList()` using the appropriate static type. It's not quite as tidy as generic code, but it should work. – Charles Srstka Mar 26 '18 at 15:06
  • The same way I do this now: instead of enum I just if-check for each type. That's basically the same :) – Makalele Mar 26 '18 at 19:22
0

Can you try following

class ListCache {

  public func getCachedList<T: Cachable>() -> [T] {
    return [(Foo() as! T)]
  }

}

protocol Cachable: Codable { }

class CachableClass: Cachable {
  class func test() -> String {
    return "blo"
  }
}

extension Cachable {

  static func cachedValues() -> [Self] {
    return cachedValuesTemplate(type: self)
  }

  private static func cachedValuesTemplate<T: Cachable>(type: T.Type) -> [T] {
    return ListCache().getCachedList()
  }

}

class Foo: CachableClass { }

var listsToLoad: [CachableClass.Type] = [Foo.self]
for clazz in listsToLoad {
  print(clazz.cachedValues().count)
}
Timur Bernikovich
  • 5,660
  • 4
  • 45
  • 58