Why does `decode(_:forKey:)` ignore its first parameter?

118 views Asked by At

It seems like decode(_forKey:) ignores its first parameter, and instead relies on the generic parameter to decide what type to decode. If this is the case, what is the first parameter for?

class Cat: Codable {
    func speak() -> String { return "Meow" }
}

class Lion: Cat {
    override func speak() -> String { return "Roar!" }
}

class Person: Codable {
    let firstPet: Cat
    let secondPet: Cat
    init(firstPet: Cat, secondPet: Cat) {
        self.firstPet = firstPet
        self.secondPet = secondPet
    }

    enum CodingKeys: CodingKey { case firstPet, secondPet }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.firstPet = try container.decode(Lion.self, forKey: .firstPet)
        let typeOfCat: Cat.Type = Lion.self
        self.secondPet = try container.decode(typeOfCat, forKey: .secondPet)
    }
}

let before = Person(firstPet: Lion(), secondPet: Lion())
let after = try! JSONDecoder().decode(Person.self, from: JSONEncoder().encode(before))
after.firstPet.speak() //"Roar!"
after.secondPet.speak() //"Meow" ...really?
1

There are 1 answers

0
Itai Ferber On BEST ANSWER

The metatype parameter to the decode(...) calls is used to specialize the generic parameter. Swift doesn't have syntax for manually specializing generics like C++ does (e.g. decode<Int>(forKey: ...)), so this is a way to bind the generic parameter to a concrete type.

The benefit of passing in a metatype (instead of, say, relying on the return type to provide resolution) is that the result of the expression is unambiguous. Relying on the return result can lead to some surprising situations:

protocol DefaultInitializable {
    init()
}

func defaultValue<T : DefaultInitializable>() -> T {
    return T()
}

func foo(_ value: Int) {
    print(value)
}

foo(defaultValue())

results in

Untitled.swift:13:5: error: generic parameter 'T' could not be inferred
foo(defaultValue())
    ^
Untitled.swift:5:6: note: in call to function 'defaultValue'
func defaultValue<T : DefaultInitializable>() -> T {
     ^

With an explicit metatype, this is a non-issue.

As for why the generic type is used over the concrete instance of the metatype you pass in — it's generally unexpected to have a concrete metatype instance have a different static type than itself.