I read a book "CLR via C# Fourth Edition". And I cannot understand one statement:
So, for example, if you have the following line of code:
FileStream[] fsArray;
then when the CLR creates the FileStream[]
type, it will
cause this type to automatically implement the
IEnumerable<FileStream>
, ICollection<FileStream>
, and
IList<FileStream>
interfaces. Furthermore, the FileStream[]
type
will also implement the interfaces for the base types:
IEnumerable<Stream>
, IEnumerable<Object>
,
ICollection<Stream>
, ICollection<Object>
,
IList<Stream>
, and IList<Object>
.
I tested this statement with this code:
FileStream[] fsArray = new FileStream[0];
string s = null;
foreach (var m in fsArray.GetType().GetInterfaces())
s += m.ToString() + Environment.NewLine;
and as a result, I have this:
System.ICloneable
System.Collections.IList
System.Collections.ICollection
System.Collections.IEnumerable
System.Collections.IStructuralComparable
System.Collections.IStructuralEquatable
System.Collections.Generic.IList`1[System.IO.FileStream]
System.Collections.Generic.ICollection`1[System.IO.FileStream]
System.Collections.Generic.IEnumerable`1[System.IO.FileStream]
System.Collections.Generic.IReadOnlyList`1[System.IO.FileStream]
System.Collections.Generic.IReadOnlyCollection`1[System.IO.FileStream]
There is no implementation of IEnumerable<Stream>
and others!
Did I make mistake somewhere? Or did Jeffrey Richter make mistake?
Furthermore, I think it is mean-less. Because Arrays support co-variance.
There is no implementation of IEnumerable and others!
Nope. And yet, IList<Stream> streamList = fsArray;
will work. And you can use streamList
as you'd expect, though with runtime exceptions if you try to do something not valid on an array (as long as the array is zero-based and has a single dimension—"SZ arrays" in Microsoft parlance—otherwise it's not allowed).
Want to see something worse?
var listMap = typeof(List<FileStream>).GetInterfaceMap(typeof(IList<FileStream>)); // This works fine.
var arrMap = typeof(typeof(FileStream[]).GetInterfaceMap(typeof(IList<FileStream>)); // This throws `ArgumentException`
So in this regard, FileStream[]
doesn't even implement IList<FileStream>
; if it does then surely the above line should work.
We get an interesting clue as of .NET 4.0. Prior to that, the ArgumentException
would have a message of "Interface not found"
, the same as it would if we'd tried to get that interface on int
or string[]
. Now it is "Interface maps for generic interfaces on arrays cannot be retrived."
[sic]
And it also give us this if we try to get the interface map for IList<Stream>
but not for a completely unsupported interface like IList<bool>
.
Something unusual is happening here.
And what it is, is that FileStream[]
does not directly support any generic interface at all, in the same way that a class
or struct
would.
Instead there is a stub class called SZArrayHelper
which provides these interfaces at runtime for zero-based one-dimension arrays. The comment on the .NET Core Version is informative:
//----------------------------------------------------------------------------------------
// ! READ THIS BEFORE YOU WORK ON THIS CLASS.
//
// The methods on this class must be written VERY carefully to avoid introducing security holes.
// That's because they are invoked with special "this"! The "this" object
// for all of these methods are not SZArrayHelper objects. Rather, they are of type U[]
// where U[] is castable to T[]. No actual SZArrayHelper object is ever instantiated. Thus, you will
// see a lot of expressions that cast "this" "T[]".
//
// This class is needed to allow an SZ array of type T[] to expose IList<T>,
// IList<T.BaseType>, etc., etc. all the way up to IList<Object>. When the following call is
// made:
//
// ((IList<T>) (new U[n])).SomeIListMethod()
//
// the interface stub dispatcher treats this as a special case, loads up SZArrayHelper,
// finds the corresponding generic method (matched simply by method name), instantiates
// it for type <T> and executes it.
//
// The "T" will reflect the interface used to invoke the method. The actual runtime "this" will be
// array that is castable to "T[]" (i.e. for primitivs and valuetypes, it will be exactly
// "T[]" - for orefs, it may be a "U[]" where U derives from T.)
//----------------------------------------------------------------------------------------
And that's what happens. If you try to cast fsArray
to IList<Stream>
then you get this class doing the calls for you. If you call GetInterfaces()
you get similar stub code providing just those related to the type of the array. In either case fsArray
does implement all the interfaces mentioned in the book you quote, but it doesn't do it in the same way as a class
or struct
can.
(Consider for analogy how an int
can be both four bytes of the 32-bit value and also a "full" object with interface implementations, method overrides, etc.)
So the book is correct, but you aren't missing anything either, because some of the things we'd expect to happen when a type implements an interface don't.
Furthermore, I think it is mean-less. Because Arrays support co-variance.
Supporting co-variance does not mean they'll implement a given interface, or vice-versa. Especially since arrays' (arguably broken) covariance is very different to that in interfaces, and predates it, and indeed having arrays implement generic interfaces also predates interface covariance.
However, that it was decided that FileStream[]
should indeed implement Stream[]
does relate to arrays being covariant (the decision would have been just bizarrely wrong otherwise), but it needs the extra help that SZArrayHelper
provides, rather than being automatically entailed by it.
Because Arrays support co-variance.
It is because arrays are covariant that they must also implement the generic interfaces of the element's base classes. In other words, everybody expects this to work:
var a = new FileStream[] { new FileStream("a", FileMode.Create) };
Stream[] b = a; // Fine, covariant
var iterb = (IList<Stream>)b; // Fine of course, actually iterates FileStreams
The assignment to the Stream[] object reference does not in any way alter the object, it is still a FileStream[] under the hood. So rock hard requirement here is that FileStream[] also implements IList<Stream>
. And IList<Object>
. And IEnumerable<Stream>
, etcetera.
So what you really discovered is that Reflection does not perfectly emulate array covariance. For which it can be forgiven. Arrays don't actually implement these interfaces, the CLR just knows how to provide a substitute object that has the desired behavior. Quacks-like-a-duck typing. More about this behavior in this Q+A.