I'm looking to see whether there's a way to easily expose my containers from c# classes without revealing unnecessary implementation details. I'm learning C# but I'm experienced in other OO languages, so I know that where possible it's better to expose base classes / interfaces rather than concrete types. What I'm doing is storing collections of key to (collection of object) in my class. I find that I can easily expose this as IDictionary(key, collection of object), but what I'd like is IDictionary(key, IEnumerable(object)). Hopefully a simple example will make it clearer.
class AddressBook
{
public IDictionary<string, List<string>> Name2Addresses // ok, but I want IDictionary<string, IEnumerable<string>>
{
get { return name2Addresses; }
}
public IEnumerable<string> GetAddresses(string name)// ok, but I really just want the convenience of exposing container as a "one-liner"
{
return name2Addresses[name];
}
public AddressBook()
{
name2Addresses = new Dictionary<string,List<string>>();
List<string> addresses = new List<string>(); // I'd prefer IList<string> addresses = ...
name2Addresses.Add("Anne Best", addresses);
addresses.Add("Home address");
addresses.Add("Work address");
}
Dictionary<string, List<string>> name2Addresses;
}
I know that exposing container of derived as container of base is a tricky problem in OO because, e.g., you could add a different derived via the container base class. But hopefully because I want to expose the container as read-only via IEnumerable there might be an easy way to do this. This is the code I want.
public IDictionary<string, IEnumerable<string>> Name2Addresses
{
get { return name2Addresses; }
}
I tried adding a cast as the compiler complained about not having one
public IDictionary<string, IEnumerable<string>> Name2Addresses
{
get { return (IDictionary<string, IEnumerable<string>>) name2Addresses; }
}
but then I got an exception:
Additional information: Unable to cast object of type 'System.Collections.Generic.Dictionary
2[System.String,System.Collections.Generic.IList
1[System.String]]' to type 'System.Collections.Generic.IDictionary2[System.String,System.Collections.Generic.IEnumerable
1[System.String]]'.
Any ideas? I was hoping there might be an easy / elegant way to do this, as on the whole it's been a joy to move from a lower-level OO language to C# where it's much quicker and easier to put what I want into practise. This isn't a show-stopper by any means as I can just reveal List rather than IEnum and trust myself not to abuse the List, but I would like to do things properly especially as I will hopefully have other people using my code one day.
PS I remember reading somewhere that C# has recently improved support for this sort of thing. I'm on VS2010 if this makes any difference.
Edit
I did some searching before asking, but after some more searching, I found this has been asked before, in a slightly different context, Why can't Dictionary<T1, List<T2>> be cast to Dictionary<T1, IEnumerable<T2>>?. So my question possibly reduces to whether there's a way of having a read-only IDictionary (see also Is there a read-only generic dictionary available in .NET?)
Another Edit
As Alex G / code4life suggested, this would work: without rolling my own read-only dictionary class I can create a new dictionary. E.g.
public IDictionary<string, IEnumerable<string>> Name2AddressesNewDictionary
{
get
{
return name2Addresses.ToDictionary(na=>na.Key, na=>na.Value.AsEnumerable());
}
}
This incurs performance penalty every time accessed + space penalty of creating the new key-value pairs (and the dict itself). Plus extra developer time. Doesn't seem better than exposing a List, in general, if I'm using a property (especially as work is going on under the covers). But would be good as a method / in other circumstances.
Third Edit
As Jens Kloster suggested, change the definition of the container to use IEnumerable as the value
Dictionary<string, IEnumerable<string>> name2Addresses;
and then cast when you need to in the implementation.
((IList<string>)name2Addresses["Anne Best"]).Add("Alternative address");