9

Over in java I'm pretty used to working with generics and the wildcard. Things like: List<? extends Animal>. This allows you to have a collection of subtypes of Animals and run generic routines on each element (e.g. makeNoise()). I'm trying to accomplish this in C# but am a bit confused, since there's no wildcards.

Domain wise, what we're doing here is working with the SQL SMO libraries to collect scripts out of our database. We've got a base interface type that is extended a number of times to script and collect different objects (table, view, function, etc -- this is the T)

public interface IScripter<T> where T : IScriptable
{
    IList<T> CollectScripts(params...)
}

public abstract class AbstractScripter<T> : IScripter<T> where T : IScriptable
{
    ....
}

public class TableScripter : AbstractScripter<Table>
{
    ....
}

public class ViewScripter : AbstractScripter<View>
{
    ....
}

So far so good. Seems like a perfectly sensible object hierarchy right? Here's what I intended to do, until I found out there's no wildcards:

public class Program
{
    static void Main(string[] args)
    {
        // auto discover all scripter modules, table, view, etc
        IList<Iscripter<? extends IScriptable>> allScripters = GetAllScripterModules(); 
        foreach (IScripter<? extends IScriptable> scripter in allScripters)
        {
            IList<? extends IScriptable> scriptedObjects = scripter.CollectScripts(...);
            // do something with scripted objects
        }
    }
 }

Now since <? extends IScriptable> doesn't exist here, what am I supposed to do instead? I've tried a number of things, generic method, just using the base type, all sorts of nasty casting, but nothing really did the trick.

What would you suggest to replace the IList<Iscripter<? extends IScriptable> piece?

TIA

Pierluc SS
  • 3,138
  • 7
  • 31
  • 44
John Newman
  • 113
  • 9

2 Answers2

12

By using out and only passing T or other covarient T interfaces out from the interface, you can make the interface covariant;

public interface IScripter<out T> where T : IScriptable
{
    IEnumerable<T> CollectScripts(params...)
}

You then can't add to the result, because you cannot use the not covarient IList so add separate interface for when you want to add:

public interface IScripterAddable<T> where T : IScriptable
{
    //either:
    IList<T> CollectScripts(params...)
    //or add just what you need to, this is usually better
    //than exposing the underlying IList - basic encapsulation principles
    void AddScript(...)
}

Then just remove the ? extends.

    // auto discover all scripter modules, table, view, etc
    IList<Iscripter<IScriptable>> allScripters = GetAllScripterModules(); 
    foreach (IScripter<IScriptable> scripter in allScripters)
    {
        IEnumerable<IScriptable> scriptedObjects = scripter.CollectScripts(...);
        // do something with scripted objects
    }
weston
  • 54,145
  • 21
  • 145
  • 203
  • Almost, but it doesn't compile: Error 1 Invalid variance: The type parameter 'T' must be invariantly valid on 'IScripter.CollectAndScript(...)'. 'T' is covariant. IScripter.cs 8 33 – John Newman Mar 27 '13 at 15:52
  • Is it because it's putting things IN to the list as well as reading it out? Should I return an IEnumerable instead – John Newman Mar 27 '13 at 15:55
  • Yes, you can't do that, see the [link](http://msdn.microsoft.com/en-gb/library/dd469487.aspx). You should [segregate your interfaces](http://en.wikipedia.org/wiki/Interface_segregation_principle). – weston Mar 27 '13 at 15:58
  • By which I mean have 2 interfaces. One is read only, the other can descend from read only and add the writting capability. ISP may not be best link for what I meant. – weston Mar 27 '13 at 16:20
  • Yep that now works perfectly. I wish I would have seen this nice "out" keyword yesterday. Also I am using the old style "_accessor" object over in the test project, which is now irreparably broken, so I had to delete that and convert it to use the PrivateObject which is probably better practice anyway. Thanks all for the help – John Newman Mar 27 '13 at 17:18
12

Covariance is the property that "if an apple is a fruit, then a bowl of apples is a bowl of fruit". This immediately presents a problem: you can put an orange into a bowl of fruit. If a bowl of apples is a bowl of fruit, and you can put an orange into a bowl of fruit, then you can put an orange into a bowl of apples. At which time clearly it is no longer a bowl of apples.

C# and Java take two different approaches to preventing this violation of type safety. C#'s approach is to say that a covariant interface must have its covariance declared up front, and that the interface expose no method whatsoever that could be used to violate type safety.

Thus IEnumerable<T> is covariant in T in C# because there is no way to put an orange into a sequence of apples; there's no "Add" method on IEnumerable<T>. There is an Add method on IList<T> and therefore it is not covariant in C#.

Java takes a different approach. It says "you can use this bowl of apples as a bowl of fruit right now, provided that you don't actually add an orange to it. The variance happens at specific sites, rather than being an overall property of the interface.

To address your actual question: if you are unable to make your IScripter<T> interface covariant in T because it can hand back an IList<T>, you might be stuck. But if you can make it contain an IEnumerable<T> then you might be in luck. Mark the interface as IScripter<out T> and then make sure that T is only used in "output" positions.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Thanks for the good explanation Eric, luckily I can change the API to use IEnumerable instead so we are all good here. It makes sense. Thanks much. – John Newman Mar 27 '13 at 17:19
  • @JohnNewman: You are very welcome! This is a tricky bit of the type system in both Java and C#. (An interesting historical note is that my former colleague Mads Torgersen worked on the design of this feature in both Java and in C#.) – Eric Lippert Mar 27 '13 at 17:25
  • Thanks, really good analogy – Pierluc SS Mar 27 '13 at 17:53
  • 2
    Best and most clear explanation I have seen so far on this! I have really been banging my head on this since coming from Java to C# but I think I finally understand how to work with it properly now. THANK YOU!!! – james_s_tayler Jan 31 '17 at 10:08