In C#: How to declare a generic Dictionary with a type as key and an IEnumerable<> of that type as value?

54,235

Solution 1

You may not even need a dictionary to be able to do this - but that depends on your needs. If you only ever need 1 such list per type per appdomain (i.e. the "dictionary" is static), the following pattern can be efficient and promotes type-inference nicely:

interface IBase {}

static class Container {
    static class PerType<T> where T : IBase {
        public static IEnumerable<T> list;
    }

    public static IEnumerable<T> Get<T>() where T : IBase 
        => PerType<T>.list; 

    public static void Set<T>(IEnumerable<T> newlist) where T : IBase 
        => PerType<T>.list = newlist;

    public static IEnumerable<T> GetByExample<T>(T ignoredExample) where T : IBase 
        => Get<T>(); 
}

Note that you should think carefully before adopting this approach about the distinction between compile-time type and run-time type. This method will happily let you store a runtime-typed IEnumerable<SomeType> variable both under SomeType and -if you cast it- under any of SomeType's base types, including IBase, with neither a runtime nor compiletype error - which might be a feature, or a bug waiting to happen, so you may want an if to check that.

Additionally, this approach ignores threading; so if you want to access this data-structure from multiple threads, you probably want to add some locking. Reference read/writes are atomic, so you're not going to get corruption if you fail to lock, but stale data and race conditions are certainly possible.

Solution 2

Use System.ComponentModel.Design.ServiceContainer that is already available in .Net framework.

        ServiceContainer container = new ServiceContainer();

        IList<int> integers = new List<int>();
        IList<string> strings = new List<string>();
        IList<double> doubles = new List<double>();

        container.AddService(typeof(IEnumerable<int>), integers);
        container.AddService(typeof(IEnumerable<string>), strings);
        container.AddService(typeof(IEnumerable<double>), doubles);

Solution 3

You don't constrain T in the private member; you constrain it at the class level.

class Foo<T> where T : BaseClass
{
    private IDictionary<T, IEnumerable<T>> _dataOfType;

    public Foo(IDictionary<T, IEnumerable<T>> dataOfType)
    {
        this._dataOfType = dataOfType;
    }   
}   

Solution 4

  1. You can't constrain a specific variable. It only works on classes and methods. It really doesn't make any sense in the variable level, to be honest.
  2. What you want is a custom class - class WeirdDictionary : IDictionary<Type, IEnumerable>, that will overload the Add method to take a Type and an IEnumerable of that type, which you can do using constraints, and cast the IEnumerable<> to IEnumerable. Overload the indexer aswell, and cast it back to the relevant type.
    All this casting is needed, since generics are strict about IEnumerable<Base> being as good as IEnumerable<Derived> (This is called variance, I believe?)

This solution is slightly generalized, since reuse rocks

Edit by 280Z28:

At first I marked this down because point #2 was confusing and I misinterpreted it. By using explicit implementation of methods in IDictionary<Type, IEnumerable> and providing generic alternatives, you can get a pretty clean interface. Note that you cannot create generic indexers, so you'll have to always use TryGet<T> (which is a good idea anyway). I only included explicit implementation of one of the IDictionary<> methods to show how to perform the checks. Do not derive WeirdDictionary directly from Dictionary<Type, IEnumerable> or you will lose the ability to guarantee constraints in the underlying data.

class WeirdDictionary : IDictionary<Type, IEnumerable>
{
    private readonly Dictionary<Type, IEnumerable> _data =
        new Dictionary<Type, IEnumerable>();

    public void Add<T>(IEnumerable<T> value)
    {
        _data.Add(typeof(T), value);
    }

    public bool TryGet<T>(out IEnumerable<T> value)
    {
        IEnumerable enumerable;
        if (_data.TryGetValue(typeof(T), out enumerable)
        {
            value = (IEnumerable<T>)enumerable;
            return true;
        }

        value = null;
        return false;
    }

    // use explicit implementation to discourage use of this method since
    // the manual type checking is much slower that the generic version above
    void IDictionary<Type, IEnumerable>.Add(Type key, IEnumerable value)
    {
        if (key == null)
            throw new ArgumentNullException("key");
        if (value != null && !typeof(IEnumerable<>).MakeGenericType(key).IsAssignableFrom(value.GetType()))
            throw new ArgumentException(string.Format("'value' does not implement IEnumerable<{0}>", key));

        _data.Add(key, value);
    }
}

End 280Z28

Solution 5

Make a custom Dictionary class:

public class BaseClassDictionary<T, IEnumerable<T>> : Dictionary<T, IEnumerable<T>>
    where T : BaseClass
{
}

Then you can use this specialized dictionary instead as field type:

private BaseClassDictionary<BaseClassDerivedType, IEnumerable<BaseClassDerivedType>> myDictionary;
Share:
54,235
Marcel
Author by

Marcel

I am an experienced software developer for both technical and business software, mainly in C#/.NET. Most professional projects are web applications or web services in the telecommunications field, for large corporate customers. I work and live in Switzerland. In my spare time I build and hack hardware stuff and occasionally, I blog on https://qrys.ch about it.

Updated on December 02, 2020

Comments

  • Marcel
    Marcel over 3 years

    I want to declare a dictionary that stores typed IEnumerable's of a specific type, with that exact type as key, like so: (Edited to follow johny g's comment)

    private IDictionary<Type, IEnumerable<T>> _dataOfType where T: BaseClass; //does not compile!
    

    The concrete classes I want to store, all derive from BaseClass, therefore the idea to use it as constraint. The compiler complains that it expects a semicolon after the member name.

    If it would work, I would expect this would make the later retrieval from the dictionary simple like:

    IEnumerable<ConcreteData> concreteData;
    _sitesOfType.TryGetValue(typeof(ConcreteType), out concreteData);
    

    How to define such a dictionary?

  • Michał Poreda
    Michał Poreda almost 14 years
    @Lucas I don't see what you mean
  • Marcel
    Marcel almost 14 years
    That is right, and probably the reason it does not compile. However this is not what I intend, I would like to have a type constrained member, but not a type constraint on the class.
  • Marcel
    Marcel almost 14 years
    Well-crafted answer. However, together with other good answers, this turns out to be more complicated that I thought. Since currently I only have 2 derived classes, I now tend to manage two separate enumerables directly as properties in my class. :-(
  • Marcel
    Marcel almost 14 years
    Not what I really need, see also my comment to johnny g.
  • Marcel
    Marcel almost 14 years
    +1 for the first remark. You are right about the constraint of the key.
  • Marcel
    Marcel almost 14 years
    Thanks for the code. This suits my case very well. I use this now.
  • Rubys
    Rubys almost 14 years
    I did some testing on the implementation 280Z28 provided, it works and it works great, on all classes. This is a neat, weird idea.
  • Marcel
    Marcel almost 14 years
    That's nice. However, I now have already implemented the solution provided by Eamon Nerbonne
  • Charles Bretana
    Charles Bretana almost 14 years
    @Marcel, Responding here, but this applies to my answer below as well... This will NOT give you multiple dictionaries. it will, as you specify, give you ONE dictionary, whose key collection is a collection of Ts, and whose Values collection is a collection of any objects that implememt IEnumerable of T, i.e. (that could be an array of Ts, a List of Ts, a Linked List of Ts, etc. Or any combination of these items... ) This may still not be what you want, but it is what you said...
  • Charles Bretana
    Charles Bretana almost 14 years
    @Marcel, Commented above under Enrico's answer
  • Marcel
    Marcel almost 14 years
    @Charles Bretana: you are right about just needing ONE dictionary. I deleted my other comment.
  • jimjim
    jimjim about 12 years
    This is the correct answer, using what is already available in the framework.