Why doesn't Array class expose its indexer directly?

前端 未结 9 1470
谎友^
谎友^ 2020-12-15 18:10

something to mention for answering:

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

相关标签:
9条回答
  • 2020-12-15 18:23

    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); 
    
    0 讨论(0)
  • 2020-12-15 18:23

    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.

    0 讨论(0)
  • 2020-12-15 18:23

    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.

    0 讨论(0)
  • 2020-12-15 18:24

    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[]
    0 讨论(0)
  • 2020-12-15 18:27

    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.

    0 讨论(0)
  • 2020-12-15 18:38

    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.

    0 讨论(0)
提交回复
热议问题