Ken Kin
Ken Kin

Reputation: 4693

Why doesn't Array class expose its indexer directly?

something to mention for answering:

  1. Don't worry about variance, while the item in question is Array rather than T[].

  2. A similar case for multi-dimension arrays is [here]

That is, N-dims to linear transform, is always possible. So this question especially caught my attention, since it already implemented IList for a linear indexer.


Question:

In my code, I have following declaration:

public static Array ToArray<T>(this T source); 

My code knows how to make souce presents an array(at runtime). And I'm trying to allow the consuming code to access its indexer directly. But without "as IList", it cannot not be done. To return object[] might require extra converting/casting, that's what I'm preventing to do. What I can do is:

public static IList ToArray<T>(this T source); 

But I would think that a method named ToArray returns an IList looked strange.

Thus, I'm confused with that:

In the declaration of Array, there is

object IList.this[int index];

So that we can

Array a;
a=Array.CreateInstance(typeof(char), 1);
(a as IList)[0]='a';

But we cannot

a[0]='a';

except if it was declared as

public object this[int index]; 

The only difference I can see is that it requires we use its indexer explicitly through the interface IList by which it was implemented, but why? Are there benefits? Or are there exposing issues?

Upvotes: 22

Views: 4410

Answers (9)

Jimmy
Jimmy

Reputation: 3264

From msdn:

The Array class is the base class for language implementations that support arrays. However, only the system and compilers can derive explicitly from the Array class. Users should employ the array constructs provided by the language.

If it provide you an indexer it contradicts with the original intention of Array class. You should use the compiler implementation.

Again from msdn:

Important: Starting with the .NET Framework 2.0, the Array class implements the System.Collections.Generic.IList, System.Collections.Generic.ICollection, and System.Collections.Generic.IEnumerable generic interfaces. The implementations are provided to arrays at run time, and therefore are not visible to the documentation build tools. As a result, the generic interfaces do not appear in the declaration syntax for the Array class, and there are no reference topics for interface members that are accessible only by casting an array to the generic interface type (explicit interface implementations). The key thing to be aware of when you cast an array to one of these interfaces is that members which add, insert, or remove elements throw NotSupportedException.

It is an afterthought, I presume.

Upvotes: 1

noypi
noypi

Reputation: 991

C# Specs "12.1.1 The System.Array type" says, "Note that System.Array is not itself an array-type"

Because it is not an array type.

And note that "6.1.6 Implicit reference conversions" says, "From a single-dimensional array type S[] to System.Collections.Generic.IList and its base interfaces, provided that there is an implicit identity or reference conversion from S to T"

C# Specs: http://www.microsoft.com/en-us/download/details.aspx?id=7029

About why the indexer access is so much of a mystery, check this other SO post: implicit vs explicit interface implementation

hope it helps.

Upvotes: 0

Michael B
Michael B

Reputation: 7577

Technically there are two types of arrays. Vector types or Matrix types. The runtime refers to Vector types as Sz_Array and they are the type you get when you declare a 1d array*. I have no clue why. Matrix types represent multidimensional arrays. Unfortunately they both inherit from Array and no other intermediary types.

Why doesn't Array class expose its indexer directly?

The reason why you can only access the indexer for 1d arrays when you have it as a T[] is because the indexer for 1d arrays is implemented in the runtime through IL opcodes.

e.g.

static T GetFirst<T>(T[] a)
{
   return a[0]:
}

Translates to the following il::

L_0000: ldarg.0 
L_0001: ldc.i4.0 
L_0002: ldelem.any !!T
L_0007: ret 

Where as the following C#

private static T GetFirst<T>(IList<T> a)
{
    return a[0];
}

translates to this IL.

L_0000: ldarg.0 
L_0001: ldc.i4.0 
L_0002: callvirt instance !0 [mscorlib]System.Collections.Generic.IList`1<!!T>::get_Item(int32)
L_0007: ret 

So we can see that one is using an opcode ldelem.any, and the other is callvirt a method.

The runtime injects IList<T>,IEnumerable<T> at runtime for arrays. The logic for them in the MSIL is located in the class SZArrayHelper

The runtime which provides the implementation, also creates two helper methods for every array generated to help languages that do not support indexers (if such a language exists) C# does not expose them but they are valid methods to call. They are:

T Get(Int32)
void Set(Int32, T)

These methods are also generated for matrix types arrays depending on the dimension and are used by C# when you call indexers.

However, unless you actually specify that your is a typed array you don't get the indexer. As Array does not have an indexer method. The opcodes can't be used because at compile time you need to know that the array in question is a vector type and the element type of the array.

But Array implements IList! That has an indexer can't I call it?

Yes it does, but the implementation for the IList methods are explicit implementations so they can only be called in C# when cast or when bound by a generic constraint. Probably, because for any non vector type of array it throws a not supported exception when you call any of its methods. Since its only conditionally supported the creators of the run-time probably want you to cast it for times when you know for a fact this is a 1d array, but I can't name the type right now.

Why does array implement IList if for any multidimensional arrays the implementation throws not supported?

This is probably a mistake that was there since 1.0. They can't fix it now as someone for whatever reason might be casting a multidimensional array to an IList. In 2.0 they added some runtime magic to add implementation to the vector type classes at runtime so that only 1d arrays implement IList<T> and IEnumerable<T>.

And we can interpolate your next question::

How can I get the indexers to show up without casting? Change the method signature to something like::

public static T[] ToArray<T>(this T source)

But you might say your ToArray does not return a T[], It returns something else what do I do? If you can specify the return type explicitly just do that. If its a reference kind you can always abuse array covariance and change the return type to object[] but then you are at the mercy of ArrayTypeMismatchException. This won't work if you are getting a valuetype back as that cast is illegal. At this point you can just return IList but then you are boxing the elements and you are still at the mercy of ArrayTypeMismatchException. Array is the base class for all array types for a reason it has helper methods to help you access the content like GetValue and SetValue and you'll note they have overloads that take arrays of indices so that you can access elements in Nd as well as 1d arrays. e.g.

IList myArray = ToArray(myValues);
// myArray is actually a string array.
myArray[0]='a';
// blows up here as you can't explicitly cast a char to a string.

So the short of it is you don't know the explicit type. And every inheritor of Array implementing IList, even when it doesn't make much sense, is an implementation detail that they can't change since it was there since 1.0.

  • Technically this isn't a 100% true. You can create a matrix type array that only has 1 dimension. This eldritch abomination can be created in IL or you can create the type using the typeof(int).MakeArrayType(1) Notice how you now have a System.Int32[*] instead of System.Int32[]

Upvotes: 1

Danny Varod
Danny Varod

Reputation: 18068

Even if array's were all 1D, you'd still have a Covariance and Contravariance issue:

If the base class had a

public Object this[int index] { get; set; }

indexer property, then the concrete types indexer properties

public TValue this[int index] { get; set; }

would collide with that of the base type (since the parameter is of the setter is the same however the return value isn't).

Casting the base class into either a base interface or a generic interface like either IList or IList solves this, since the non-specific indexer can be implemented explicitly. This is the same with the

Add(Object value)

vs.

Add(TValue value)

methods.

The multi dimensional issue could, theoretically, be overcome by defining a conversion between 1D indexes and n-D indexes (e.g. [n] = [n / length(0), n % length(0)]) since n-D matrices are stored as one continuous buffer.

Upvotes: 2

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726639

The problem with IList<T>'s methods in the Array class, including its indexer, is that their explicit implementations are added to Array objects of the class at run time:

Starting with the .NET Framework 2.0, the Array class implements the System.Collections.Generic.IList<T>, System.Collections.Generic.ICollection<T>, and System.Collections.Generic.IEnumerable<T> generic interfaces. The implementations are provided to arrays at run time, and therefore are not visible to the documentation build tools. As a result, the generic interfaces do not appear in the declaration syntax for the Array class, and there are no reference topics for interface members that are accessible only by casting an array to the generic interface type (explicit interface implementations).

When classes implement interfaces explicitly, accessing interface methods requires a cast:

A class that implements an interface can explicitly implement a member of that interface. When a member is explicitly implemented, it cannot be accessed through a class instance, but only through an instance of the interface.

The problem with providing a "regular" (as opposed to an "explicit") interface implementation is the fact that the Array class is not generic: without a type parameter, you cannot write

class Array : IList<T>

simply because T is undefined. The environment cannot slap an interface implementation onto the Array class until the type of the T parameter becomes known, which may happen only at run time:

// The type of [T] is char
Array a = Array.CreateInstance(typeof(char), 1);
// The type of [T] is int
Array b = Array.CreateInstance(typeof(int), 1);
// The type of [T] is string
Array c = Array.CreateInstance(typeof(string), 1);

At the same time, the static type of a, b, and c remains the same - it's System.Array. However, at run time a will be implementing IList<char>, b will be implementing IList<int>, and c - IList<string>. None of it is known at compile time, prevents the compiler from "seeing" the indexer and other methods of IList<T>.

Moreover, not all instances of Array implement IList<T> - only arrays with a single dimension do:

Array x = new int[5];
Console.WriteLine(x.GetType().GetInterface("IList`1") != null); // True
Array y = new int[5,5];
Console.WriteLine(y.GetType().GetInterface("IList`1") != null); // False

All of the above prevents the compiler from accessing IList<T> methods, including the indexer, without an explicit cast.

Upvotes: 3

Philipp Munin
Philipp Munin

Reputation: 5903

Short answer:

System.Array is a base class for N-D arrays (not only 1-D), that's why 1-D indexer (object this[i]{get;set;}) cannot be a base member.

Long answer:

If you let's say create 2-dimensional array and try to access it's IList indexer:

Array a;
a=Array.CreateInstance(typeof(char), 1,1);
(a as IList)[0]='a';

You will get not supported exception.

Good question would be:

Why System.Array implement IList and IEnumerable while most of its implementation will throw NotSupportedException for non 1-D array??

One more interesting thing to mention. Technically non of the arrays have class-indexer internally in classic meaning. Classic meaning of Indexer is a property "Item" + get(+set) method(s). If you go deep to reflection you will see that typeof(string[]) does not have indexer property and it only has 2 methods Get and Set - those method declared in string[] class (not in base class, unlike Array.SetValue, Array.GetValue) and they are used for compile-time indexing.

Upvotes: 2

svick
svick

Reputation: 244837

I think one reason why Array doesn't implement that indexer directly is because all the specific array types (like char[]) derive from Array.

What this means is that code like this would be legal:

char[] array = new char[10];
array[0] = new object();

Code like this shouldn't be legal, because it's not type-safe. The following is legal and throws an exception:

char[] array = new char[10];
array.SetValue(new object(), 0);

But SetValue() is not normally used, so this is not a big problem.

Upvotes: 4

Servy
Servy

Reputation: 203833

Array can't have an indexer because it needs to be able to represent an array with any number of dimensions. The indexer for a two dimensional array has a different signature than for a one dimensional array.

If an indexer was provided and used on an Array that represented a two dimensional array what should happen?

The solution that the language designers choose was to just not include an indexer at all.

If you know that your ToArray method will always return a one dimensional array then consider using:

public static T[] ToArray<T>(this T source); 

That will have an indexer.

If the elements in the array will not all be of type T then you can return an object[]:

public static object[] ToArray<T>(this T source); 

Upvotes: 20

Dinah
Dinah

Reputation: 54017

a as IList is (basically) casting. So just cast it first:

char[] a = (char[])Array.CreateInstance(typeof(char), 1);
a[0] = 'a';

Edit: the reason is: because the interface for Array simply doesn't define an indexer. It uses SetValue(Object, Int32) and Object GetValue(Int32). Notice the ominous Object stuff in there. Array isn't type specific; it's built for the lowest common denominator: Object. It could have just as easily defined an indexer, but in practice you'd still have the un/boxing problem.

Upvotes: 5

Related Questions