views:

546

answers:

5

I need to write a generic class where the type parameter must be something implementing ICollection<T>. Inside of MyClass I need the collection's item type (in the code snippet marked as ???).

class MyClass<TCollection> where TCollection : ICollection<???>
{
  // ...

  public void store(??? obj) { /* put obj into collection */ }

  // ...
}

Often the collection will actually be a dictionary. Sometimes, it will be something simple as a list.

I know exactly how to do this in C++. How would I do this is C#?

+5  A: 

Well, you'd normally do this using another type parameter:

class MyClass<TCollection, TElement> where TCollection : ICollection<TElement>
{
  // ...

  public void store(TElement obj) { }

  // ...
}

Do you actually need TCollection in this case? Could you just make do with TElement?

Jon Skeet
Yes, users of `MyClass` need to be able to specify the collection used. - Given your class definition, would users have to specify both `TCollection` and `TElement` or can `TElement` be automagically deduced from the collection passed? (I ask this because often those collections will be nested dictionaries which are quite something to type.)
sbi
You'd have to specify both type arguments explicitly. You could write a helper method to utilise generic type inference in some cases though. It depends on the exact use case.
Jon Skeet
Given that the class is used as member of other classes, I suppose there is no way to use generic type inference?
sbi
If you have an instance of the relevant type, you could use generic type inference via a generic method (which could then create an instance of the generic type).
Jon Skeet
@Jon: Only one instance of every instantiation of `MyClass` will be needed, but there are many different instances of them and the data structures are still changed. `:(`
sbi
No, I mean an instance of TElement and TCollection.
Jon Skeet
You've lost me. `:(`
sbi
+1  A: 

Can't you just do this?:

class MyClass<T, TCollection>  where T: YourTypeOrInterface
                               where TCollection : ICollection<T>
{
    public void store(T obj) { }
}

And, not tested, but the only other way I could think to do it would be to specify the type when storing the item:

class MyClass<TCollection> where TCollection : System.Collections.ICollection
{

    TCollection Collection;

    public void Store<T>(T obj) 
    {
        ((ICollection<T>)this.Collection).Add(obj);
    }

}
GenericTypeTea
+3  A: 

The simplest thing to do is just specify the element type only and hard-code ICollection<T> wherever you need it, e.g.

class MyClass<T> {

    private ICollection<T> _items;

    public MyClass(ICollection<T> items) {
        _items = items;
    }

    public void Store(T obj) {
        _items.Add(obj);
    }

    public ICollection<T> Items {
        get {
            return _items;
        }
    }
}

I recommend that you pass in a collection instance to the constructor rather than create one internally. It makes the class simpler and more "generic" (excuse the pun), and allows you to construct collection instances with non-default constructors, e.g. a dictionary with a non-default comparator.

RE-EDIT (3rd attempt): Using class inheritance and namespace aliasing to simulate typedef are both OK up to a point, but both abstractions break down under certain circumstances. This code is the simplest I have found that actually compiles.

Step 1 - Define these classes:

// This KeyValuePair was being used to simulate a tuple. We don't need to simulate a tuple when we have a concrete class.
class BazAndListOfWrgl {
    Baz Baz { get; set; }
    List<Wrgl> Wrgls { get; set; }
}

// Simple typedef.
class BazAndListOfWrglDictionary : Dictionary<Bar, BazAndListOfWrgl> { }

Step 2 - Define these namespace aliases. All identifiers must be fully qualified. All types referenced must already be defined in a different physical file (since namespace aliases have to come before all code in a file).

using OuterDictionary = System.Collections.Generic.Dictionary<MyNamespace.Foo, MyNamespace.BazAndListOfWrglDictionary>;
using OuterDictionaryItem = System.Collections.Generic.KeyValuePair<MyNamespace.Foo, MyNamespace.BazAndListOfWrglDictionary>;

Step 3 - Use them like this:

class Program {

    static void Main() {

        // List-based example.
        var listWrapper = new MyClass<BazAndListOfWrgl>(new List<BazAndListOfWrgl>());
        listWrapper.Store(new BazAndListOfWrgl());
        Console.WriteLine(listWrapper.Items.Count);

        // Dictionary-based example.
        var dictionaryWrapper = new MyClass<OuterDictionaryItem>(new OuterDictionary());
        dictionaryWrapper.Store(new OuterDictionaryItem(new Foo(), new BazAndListOfWrglDictionary()));
        Console.WriteLine(dictionaryWrapper.Items.Count);

    }
}

The reasoning being: BazAndListOfWrglDictionary cannot be a namespace alias because namespace aliases cannot depend on each other. OuterDictionary and OuterDictionaryItem cannot be derived classes because otherwise the compiler does not recognise one as being the element of the other.

Christian Hayter
This class _will_ need to store different types of collections. I'd like to do this as simple as possible - I just don't know how to do that in C#. `:-x`
sbi
The `ICollection<T> _items` field will store all types of collection. I think you are confusing generics with plain interfaces.
Christian Hayter
@Christian: This is what Freed already suggested in his comments. Yes, this would work, but it also suffers from the need to spell out `T` twice - which isn't nice if `ICollection<T>` actually is three nested dictionaries. I guess I'm spoiled by C++' powerful templates...
sbi
Sorry, but I don't understand you. Why would you have to spell out T twice? What difference would it make if `ICollection<T>` is three nested dictionaries? By treating three nested dictionaries as a collection interface you are hiding all that complex functionality and pretending that it's a simple list of `KeyValuePair`. If you want to access the dictionary inside your class, don't make it generic because anything you do won't be valid for non-dictionary collections.
Christian Hayter
How about you give us an actual example of (a) a non-generic class that handles a list, and (b) a non-generic class that handles a dictionary. We can then suggest how you can refactor the two classes into one generic class.
Christian Hayter
I can understand your frustration: C++ templates are duck-typed, but C# generics are strongly-typed. There is very little 1-1 correspondence in functionality.
Christian Hayter
@Christian: See the example in my comment to Rob's answer. There I have to spell out `<baz, List<wrgl>>>>, <bar, KeyValuePair<baz, List<wrgl>>>` twice. Is there a way to avoid this?
sbi
`var myclass = new MyClass<Dictionary< foo, Dictionary<bar, KeyValuePair<baz, List<wrgl>>>>, <bar, KeyValuePair<baz, List<wrgl>>>>();`
Christian Hayter
There, only spelled out once.
Christian Hayter
See my edited answer, you could also shorten it with the using directive.
Rob van Groenewoud
Nice one Rob, I thought only VB could do that. YLSNED :-)
Christian Hayter
@Christian: Our typos and omissions added up. It _should_ have been `MyClass<Dictionary<foo, Dictionary<bar, KeyValuePair<baz, List<wrgl>>>>, KeyValuePair<foo, Dictionary<bar, KeyValuePair<baz, List<wrgl>>>>>` and this already spells out `<foo, Dictionary<bar, KeyValuePair<baz, List<wrgl>>>>` twice. That we both messed it up just proves my point that it is messy. Unfortunately I had already found `using` to be usable only at namespace scope, otheriwse I'd use that. I guess I'll just have to create classes as you describe. It seems this is just the way to go in a language without type aliases.
sbi
"Constantly nesting types ad infinitum just makes the code unreadable." Yes, that's why I'm searching for a way to do this without that nesting. (Did I say I miss `typedef`?)
sbi
Simulating typedefs with inheritance would be my recommendation. It's compatible across all .NET languages (i.e. not dependent on one compiler's syntax), compatible with the Liskov Substitution Principle, and you can add to the classes later on if you wish.
Christian Hayter
@Christian: Yes, this is what I ended up doing.
sbi
+1  A: 

This works for me:

class MyClass<TCollection,T> where TCollection : ICollection<T> , new()
{
    private TCollection collection;           

    public MyClass()
    {
        collection = new TCollection();
    }

    public void Store(T obj)
    {
        collection.Add(obj);
    }

    public TCollection Items
    {
        get { return collection; }
    }
}

Usage:

MyClass<List<string>, string> myclass = new MyClass<List<string>, string>();
myclass.Store("First element");
myclass.Store("Second element");
myclass.Items.ForEach(s => Console.WriteLine(s));

EDIT: When T is getting more complicated, you might want to take a look at the using directive, you can (mis)use it as a typedef.

using HugeType = System.Collections.Generic.Dictionary<int, string>; // Example 'typedef'
...

//Create class, note the 'typedef' HugeType
MyClass<List<HugeType>, HugeType> myclass = new MyClass<List<HugeType>, HugeType>();
//Fill it
HugeType hugeType1 = new HugeType();
hugeType1.Add(1, "First");
hugeType1.Add(2, "Second");
HugeType hugeType2 = new HugeType();
hugeType1.Add(3, "Third");
hugeType1.Add(4, "Fourth");
myclass.Store(hugeType1);
myclass.Store(hugeType2);
//Show it's values.
myclass.Items.ForEach(element => element.Values.ToList().ForEach(val => Console.WriteLine(val)));
Rob van Groenewoud
This is what I'm currently doing, except that where you have `MyClass<List<string, string>` I have things like `MyClass<Dictionary< foo, Dictionary<bar, KeyValuePair<baz, List<wrgl>>>>, <bar, KeyValuePair<baz, List<wrgl>>>>`. (Have I said I'm missing `typedef` badly? No? Well, so: I am missing `typedef` badly. _Very_ badly. In fact, it's one of the main annoyances of C# that it misses `typedef`.)
sbi
I understand you concerns, edited my answer, not sure whether this solution is recommended, but it seems to work in my example.
Rob van Groenewoud
@Rob: Yes, I had found the `using` directive a while ago. However, IIRC, it can only appear at namespace scope, not, for example, within classes. That's quite annoying.
sbi
@sbi: you're right about the scope. Besides that, VS auto completion also inserts the complete string again instead of the 'alias', which is quite annoying too.
Rob van Groenewoud
+1  A: 

Are there any public members which expose the type TCollection?

If there are any such members, do you gain any value exposing the direct type TCollection where you could expose ICollection<T>?

Your clients are expected to implement the ICollection<T> interface, so it doesn't really affect your object; you will only ever be making calls to that interface, not their specific type. As such, your object only needs to know the type of element being stored in the collection.

Under this premise, the class can be written like this:

public class MyClass<T>
{
    public MyClass(ICollection<T> collection) 
    { 
        /* store reference to collection */ 
    }

    // ...

    public void store(T obj) 
    { 
        /* put obj into collection */ 
    }

    // ...
}
Programming Hero
"Are there any public members which expose the type `TCollection`?" Yes, there are. I could get away with just `ICollection<T>`, but it results in ugly and probably error-prone code.
sbi