C# Covariance and Contravariance when implementing interfaces
I've recently decided to refresh my memory regarding C# basics, so this might be trivial, but i've bumped into the following issue:
StringCollection
was used in .NET v1.0 in order to create a strongly typed collection for strings as opposed to an object
based ArrayList
(this was later enhanced by including Generic collections):
Taking a quick glance at StringCollection
definition, you can see the following:
// Summary:
// Represents a collection of strings.
[Serializable]
public class StringCollection : IList, ICollection, IEnumerable
{
...
public int Add(string value);
...
}
You can see it implements IList
, which contains the following declaration (a开发者_StackOverflowmong a few other declarations):
int Add(object value);
But not:
int Add(string value);
My first assumption was that it is possible due to the .NET framework covariance rules.
So just to make sure, I tried writing my own class which implements IList
and changed
int Add(object value);
to retrieve a string type instead of an object type, but for my surprise, when trying to compile the project, I got an compile-time error:
does not implement interface member 'System.Collections.IList.Add(object)'
Any ideas what causes this?
Thanks!
The behavior is caused by the explicit implementation of IList.Add(object)
rather than co/contravariance. Per the MSDN documentation, StringCollection explicitly implements IList.Add(object)
; the Add(string)
method is unrelated. The implementation may resemble something like this:
class StringCollection : IList
{
...
public int Add(string value)
{} // implementation
public int IList.Add (object value)
{
if (!value is string)) return -1;
return Add(value as string)
}
}
This distinction can be observed:
StringCollection collection = new StringCollection();
collection.Add(1); // compile error
(collection as IList).Add(1); // compiles, runtime error
(collection as IList).Add((object)"") // calls interface method, which adds string to collection
Addendum
The above doesn't address why this pattern is implemented. The C# language specification states that [§13.4.1, emphasis added]:
In some cases, the name of an interface member may not be appropriate for the implementing class, in which case the interface member may be implemented using explicit interface member implementation. [...]
It is not possible to access an explicit interface member implementation through its fully qualified name in a method invocation, property access, or indexer access. An explicit interface member implementation can only be accessed through an interface instance, and is in that case referenced simply by its member name.
StringCollection adheres to the required IList behavior -- IList makes no guarantee that any arbitrary object can be added to it. StringCollection makes stronger guarantees -- primarily, that it will contain only strings. The class includes its own strongly-typed methods for Add
, Contains
, Item
, and others for the standard use case where it is accessed as a StringCollection
rather than an IList
. But it still functions perfectly well as an IList
, accepting and returning objects, but returning an error code (as IList permits) if an attempt is made to add an item that is not a string.
Ultimately, whether an interface shows up in the class (i.e., is explicitly implemented) is at the discretion of the class author. In the case of framework classes, explicit implemenentations are included in the MSDN documentation but are not accessible as class members (e.g., shown in autocompletion contexts).
If you are using .net 2.0+, I would just use generics:
IList<string> list = new List<string>();
That should give you everything you want.
IList.Add(object)
can accept parameters other than strings -- it can accept any type. So if you declare your implementation of the interface to only accept strings, it no longer matches the interface specification, because now I couldn't pass in a Stream
for example.
Variance can work the other way: if the interface method was declared to accept strings, then accepting objects would be alright, since strings are also objects, and therefore any input to the interface's method would also be acceptable input to your implementation. (However, you would still have to provide an explicit interface implementation with a method accepting string, because in C# an interface method implementation much exactly match the interface method declaration.)
What IList
basically specifies is that you can call Add
and pass any object as a parameter, from a boxed Int
to another IList
to a System.DivideByZeroException
. If you only supply an Add( string )
method, you haven't fulfilled this requirement since you can only add strings.
In other words, you wouldn't be able to call StringCollection.Add( new Object() );
, which should be perfectly doable if the interface was implemented properly. :D
精彩评论