Search code examples
c#listcovariance

How to work with a collection of objects with an incompatible type?


I have a table controller that I'm using for several tables (Data Models). Here is a simplified version of it:

public abstract class TableBase
    {
        public virtual void TableName()
        {
            Console.WriteLine("I am Table: " + this.GetType().Name);
        }
    }

public class TableA : TableBase
    {
    }

public class TableB : TableBase
    {
    }

public class TableC : TableBase
    {
    }

public class Controller<T> where T : TableBase
    {
        public Controller(T table)
        {
            table.TableName();
        }

        public void Synchronize();
    }

Then I basically use it like this:

Controller<TableA> Controller1 = new Controller<TableA>(new TableA());
Controller<TableB> Controller2 = new Controller<TableB>(new TableB());
Controller<TableC> Controller3 = new Controller<TableC>(new TableC());

Everything is easy breezy but the problem comes when I want to add the controllers to a list of controllers:

List<Controller<TableBase>> ControllerList = new List<Controller<TableBase>>();
ControllerList.Add(Controller1);
ControllerList.Add(Controller2);
ControllerList.Add(Controller3);

It tells me that I can't convert Table(A,B,C) to type of TableBase, for some reason using the base as a type in the controller class freaks everything out. I wouldn't think this is causing a variance issue but it seems to be. All I'm wanting to do is call Synchronize() on each controller in a loop. How do i get this to work?


Solution

  • If you need to call a common method in otherwise incompatible types, you can define an interface that exposes the functionality you need to call.

    In this code sample, I added a new interface ICanSync which has method signature matching Synchronize() from your Controller<T>, and modified Controller<T> to implement the new interface. This means you can create a List<ICanSync> and add controllers with incompatible generic types.

    // new interface to use
    public interface ICanSync
    {
        void Synchronize();
    }
    
    public abstract class TableBase
    {
        public virtual void TableName()
        {
            Console.WriteLine("I am Table: " + this.GetType().Name);
        }
    }
    
    public class TableA : TableBase
    {
    }
    
    public class TableB : TableBase
    {
    }
    
    public class TableC : TableBase
    {
    }
    
    public class Controller<T> : ICanSync where T : TableBase
    {
    
        private TableBase _t;
    
        public Controller(T table)
        {
            _t = table;
        }
    
        public void Synchronize()
        {
            _t.TableName();
        }
    }
    

    You can declare a List<ICanSync> and call Synchronize() on all of them.

    Controller<TableA> Controller1 = new Controller<TableA>(new TableA());
    Controller<TableB> Controller2 = new Controller<TableB>(new TableB());
    Controller<TableC> Controller3 = new Controller<TableC>(new TableC());
    
    List<ICanSync> ControllerList = new List<ICanSync> {Controller1, Controller2, Controller3};
    
    foreach (var controller in ControllerList) 
    {
        controller.Synchronize();
    }
    

    This is type safe, because ICanSync is naturally compatible with instances of itself. If you need richer common functionality that uses T, you can declare a covariant or contravariant interface.