1

I want to write better and cleaner code using parametrized classes in Swift but I'm getting a strange build error:

Cannot convert value of type 'CustomAdapter' to expected argument type 'TableTestParametrizedAdapter<ETableViewCell>'

What I actually want is to be able to create a base adapter class with one method (overridden in adapter subclasses) used to bind cell subclasses with the corresponding data model and get rid of casting every time.

I'll post the code below, in order to understand better what I mean.

class TestParametrizedAdapter<C>: NSObject {

    func doSmth(cell: C) {

    }

}

class TableTestParametrizedAdapter<C>: TestParametrizedAdapter<C> where C:ETableViewCell {

}

class PeopleTableViewCell: ETableViewCell {

}

class CustomAdapter: TableTestParametrizedAdapter<PeopleTableViewCell> {

    override func doSmth(cell: PeopleTableViewCell) {

    }
}


class TestBaseController: UIViewController {

    var adapter: TableTestParametrizedAdapter<ETableViewCell>?

    override func viewDidLoad() {
        super.viewDidLoad()

        setAdapter(adapter: CustomAdapter()) // this is the line with build error
    }

    func setAdapter(adapter: TableTestParametrizedAdapter<ETableViewCell>) {
        self.adapter = adapter
    }
}

I have read on some other posts about this and there was pointed out that GenericClass<B> and GenericClass<A> are completely unrelated even if B is a subclass of A, hence you cannot cast one to the other. (https://stackoverflow.com/a/50859053/10115072)

Anywat, are there any solutions for this? How can we use the power of parametrization of Swift in this case? I use Swift 4.

Thanks in advance.

Daniel Z.
  • 458
  • 1
  • 3
  • 23

3 Answers3

1

Even if Swift would support variance in custom generics your code would be wrong, since you try to use object that can only handle PeopleTableViewCell instances in place of object that can handle any ETableViewCell. If that is indeed what you want and you don't mind some run-time checks you can do something similar with little type erasure:

class TestAnyAdapter: NSObject {
    func doSmth(cell: Any) {}
}

class TableTestParametrizedAdapter<C>: TestAnyAdapter where C:ETableViewCell {
    override func doSmth(cell: Any) {
        guard let cell = cell as? C else {
            return
        }

        self.doSmth(cell: cell)
    }

    func doSmth(cell: C) {}
}

the rest of the code will be the same as you already have, only without compile-time error.

Konstantin Oznobihin
  • 5,234
  • 24
  • 31
1

I agree with Konstantin about the fundamental problem here. This code is simply incorrect, and Swift is telling you so. CustomAdapter.doSmth cannot accept any arbitrary ETableViewCell, but adapter claims it must.

There are many solutions, depending on the actual problem you're trying to solve. You indicated you want to write "better and cleaner code." That suggests you have existing code where you're finding excessive duplication or casting. So what you want to do is look at that code, and see what code is being duplicated, and then we can help you design generic solutions to avoid that duplication. There is no universal answer to this question; abstraction choices you make in one direction will make other directions less flexible. Abstraction is choices; they need to be made in context.

As a rule in Swift, you should avoid relying on subclassing. There is some that is required, because of bridging to ObjC, but Swift-focused code should avoid subclasses. In your particular example, the interesting class has just one function. If that's really true, then implementing it is easy. Use one function:

func customAdapter(cell: PeopleTableViewCell) {}

class TestBaseController: UIViewController {
    let adapter: (PeopleTableViewCell) -> Void = customAdapter
}

"But my real problem is more complex than that!" Ok. Then we have to talk about your real problem. Abstracting these things down to their simplest forms rightly should lead to the simplest solutions. If things are actually a bit more complex, you could use a struct and a protocol.

protocol Adapter {
    associatedtype Cell: UITableViewCell
    func doSmth(cell: Cell)
}

struct CustomAdapter<Cell: ETableViewCell>: Adapter {
    func doSmth(cell: Cell) {}
}

class TestBaseController: UIViewController {
    let adapter: CustomAdapter<PeopleTableViewCell> = CustomAdapter()
}

I'm glossing over what may be your question, which is how to make a function that only accepts PeopleTableViewCell be used where a function that accepts any ETableViewCell is required. That's impossible. It's not a limitation in Swift; it's just type-wise impossible. The best you could do is add "do nothing" or "crash" as Konstantin explains.

If you can nail down a little more what particular problem in your existing code you're trying to fix, we can probably help you design better solutions. Adding generics does not make your code "better or cleaner" by themselves (and most of the best solutions barely need generics at all in my experience).

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
1

Let's try to get some facts straight.

  • Let's say we have generic class C<T>.

  • And let's also say we have classes D and D2, where D2 is the subclass of T.

Then C<D2> is not a subclass of C<D>. They are just separate types. (We say there is not covariance.)

  • Let's say our generic class C<T> has a subclass C2<T>.

Then C2<D> is a subclass of C<D>, and C2<D2> is a subclass of C<D2>.

So as long as the parameterized types are the same, there's polymorphism. But there's no covariance if the parameterized types are different, even if parameterized types are class and subclass.

(Swift Optional and Swift collections get a special covariance dispensation here, but that's baked into the language; you can't the same dispensation.)

matt
  • 515,959
  • 87
  • 875
  • 1,141