5

I'm attempting to create a common interface which will allow me n methods of interacting with a database. I want my business application to be able to instantiate any of the connection methodologies and be assured the interface is identical.

Here's a simplified version of what I'm trying now.

Database Interface where IElement is another interface which would define a table.

public interface IDatabase
{
    void setItem( IElement task );  //this works fine
    List<T> listTasks<T>() where T : IElement; // this doesn't
}

IElement interface:

public interface IElement
{
    int id { get; set; }
}

Implementation of IElement:

public class TaskElement: IElement
{
    public int id { get; set; }
    public string name {get; set; }
}

Implementation of IDatabase:

public class SQLiteDb: IDatabase
{
    public SqLiteDb( SQLiteConnection conn )
    {
        database = conn;
    }

    public void setItem( IElement task )
    {
        // works fine when passed a new TaskElement() which is an implementation of IElement.
        database.Insert( task ); 
    }

    //it all goes off the rails here
    public List<T> listItems<T>() where T : IElement
    {
        var returnList = new List<IElement>

        foreach (var s in database.Table<TaskElement>())
        { returnList.Add(s); }

        return returnList;
    }

I've tried a lot of variations on this but each one gives me a new issue. Here, for instance, there are two errors.

1)

The type arguments for method 'SQLiteDb.listTasks<T>()' cannot be inferred from the usage. Try specifying the type arguments explicitly.

2)

Cannot implicitly convert type 'System.Collections.Generic.List<TaskElement>' to 'System.Collections.Generic.List<T>'

I've tried changing the method to use an explicit type but have had issues there. If I use IElement (my generic interface for all elements )I can't return a list of TaskElement objects (my implementation of IElement) as it doesn't match the return type (List<IElement>) and if I change the return type to List<TaskElement> I'm not longer implementing the interface.

It's worth noting that I can easily get this to work if I stop using the interface and generics, but this seems like an ideal situation to use an interface. Maybe I'm trying to hard to cram a lot of stuff into an interface when another application (like direct inheritance) might be better?

Question

How can I implement an interface with a generic return value while limiting the types which can be returned to only implementations of another interface.

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122
Nigel DH
  • 207
  • 3
  • 11
  • Have you tried `var returnList = new List();` – Scott Chamberlain Aug 01 '16 at 15:57
  • Just for notice, take a look on your TaskElement as you're implementing an interface you have to make Id public (as by default c# sets it as private) – ams4fy Aug 01 '16 at 16:02
  • 3
    Nothing to do with your question, but if you're just starting c# you should read the [coding conventions](https://msdn.microsoft.com/en-gb/library/ff926074.aspx) other devs will thank you! – Jamiec Aug 01 '16 at 16:03
  • 1
    Your method signature is `List listTasks() where T : IElement`. This mean that any implementation of the interface should be able to work with any type that implements `IElement`. Is this intentional? – Yacoub Massad Aug 01 '16 at 16:04
  • Maybe you need to make the interface itself generic like `public interface IDatabase where T:IElement`? – Yacoub Massad Aug 01 '16 at 16:09
  • Yes, that's intentional. I've read that it can cause some confusion (there's an amusing and good answer about it on stackoverflow in which you can make a giraffe roar like a lion) but as I'm not making this particular method available to anyone else it felt reasonable. – Nigel DH Aug 01 '16 at 16:11
  • @YacoubMassad how many database abstraction layers have you ever used which limit themselves to returning one type of entity? – Jamiec Aug 01 '16 at 16:25
  • Also note that in C#, method names usually start with a capital. So I would call the function `ListItems`. Or, maybe even better `listItems` => `Retrieve` and `setItem` => `Store`. – CompuChip Aug 02 '16 at 07:52
  • Thanks for the heads up. I'll modify things as I continue on the project! – Nigel DH Aug 02 '16 at 10:51

4 Answers4

3

Let's look closely at your implementation of listItems:

public List<T> listItems<T>() where T : IElement
{
    var returnList = new List<IElement>

    foreach (var s in database.Table<TaskElement>())
    { returnList.Add(s); }

    return returnList;
}

What you've done here is written a method where the caller is allowed to ask for any type they want in the list as long as that type implements IElement. But the code in your method doesn't give them a list of the type they want, it gives them a list of IElement. So it's violating the contract.

But the real root of your problem is database.Table<TaskElement>(). That can only ever give you instances of TaskElement. You need to make that T, but to do that you need an additional generic constraint:

public List<T> listItems<T>() where T : IElement, new
{
    var returnList = new List<T>

    foreach (var s in database.Table<T>())
    { 
        returnList.Add(s); 
    }

    return returnList;
}

This is because database.Table<T> has a new constraint, which means that it can only be given types that have a zero-parameter constructor (because that method is going to create instances of the given class).

Kyle
  • 6,500
  • 2
  • 31
  • 41
  • Oh great, that worked a treat. All of my digging and I never saw a reference to new. Incidentally, it needed to be `public List listItems() where T : IElement, new()` and also added to the interface so that the contract between the interface and implementation was maintained. This also made the method completely generic which was something I was going to do as a second step! Thanks very much, @Kyle! – Nigel DH Aug 01 '16 at 17:12
2

I belive it should be something like this

public List<T> listItems<T>() where T : IElement
    {
        var returnList = new List<T>

        foreach (var s in database.Table<T>())
        { returnList.Add(s); }

        return returnList;
    }
MikkaRin
  • 3,026
  • 19
  • 34
  • I'd given that a shot but unfortunately the SQLiteConnection Table requires a non-abstract type. That's why I wanted to use the explicit type TaskElement (which is implemented from IElement) as it defines the SQL table and is required by the Table method. – Nigel DH Aug 01 '16 at 16:16
  • @NigelDH - Im confused how you'll actually make this usable - assuming your class is going to return more than just `TaskElement` how would you code that method to be generic enough to return `OtherElement` or `ThirdElement` if the `Table` call requires a concrete type? – Jamiec Aug 01 '16 at 16:23
  • You are actually totally correct. I'm doing a more static implementation like this just to get my head around generics, but ideally I'd like to pass a parameter into the method like this: listItems( IElement type ) and then use that to define which table is searched. I'd also extend this to include options for searching tables, etc. Trying to keep it simple for now. – Nigel DH Aug 01 '16 at 16:47
1

I think you are on the right track with explicitly defining your list like this:

public interface IDatabase
{
    void setItem( IElement task );  //this works fine
    List<IElement> listTasks<IElement>();
}

Since you can't directly cast List <TaskElement> to List <IElement> you will have to do a conversion in your listTasks method. There are several methods recommended here: Shorter syntax for casting from a List<X> to a List<Y>?. I think the Linq method is the simplest if you are ok with using Linq:

List<IElement> listOfIElement = listOfTaskElement.Cast<IElement>().ToList()
Community
  • 1
  • 1
avariant
  • 2,234
  • 5
  • 25
  • 33
0

You need to use the Generic type when creating the object instance:

Instead of

var returnList = new List<IElement>();

Do this

var returnList = new List<T>();
Jamiec
  • 133,658
  • 13
  • 134
  • 193
Albert
  • 1,394
  • 1
  • 12
  • 16
  • That isn't enough since you won't be able to add an instance of `TaskElement` to `returnList`. – Lee Aug 01 '16 at 15:59