What the difference between array indexer and any other object indexer

Consider following two data types:

class C
{
    public int I { get; set; }
}

struct S
{
    public int I { get; set; }
}

Let's try to use them inside the list, for example:

var c_list = new List<C> { new C { I = 1 } };
c_list[0].I++;

var s_list = new List<S> { new S { I = 1 } };
s_list[0].I++; // (a) CS1612 compilation error

As expected, there is compilation error on line (a): CS1612 Cannot modify the return value of 'List<UserQuery.S>.this[int]' because it is not a variable. This is fine, because actually we trying to modify temporary copy of S, which is r-value in giving context.

But let's try to do same thing for an array:

var c_arr = new[] { new C { I = 1 } };
c_arr[0].I++;

var s_arr = new[] { new S { I = 1 } };
s_arr[0].I++; // (b)

And.. this works.

But

var s_arr_list = (IList<S>) s_arr;
s_arr_list[0].I++;

will not compile, as expected.

If we look at the produced IL, we will find following:

IL_0057:  ldloc.1     // s_arr
IL_0058:  ldc.i4.0    // index
IL_0059:  ldelema     UserQuery.S // manager pointer of element

ldelema loads address of the array element to the top of the evaluation stack. Such behavior is expected with fixed array and unsafe pointers. But for safe context this is a bit unexpected. Why there is a special unobvious case for arrays? Any why there is no option to achieve same behavior for members of other types?

Jon Skeet
people
quotationmark

An array access expression is classified as a variable. You can assign to it, pass it by reference etc. An indexer access is classified separately... in the list of classifications (C# 5 spec section 7.1.)

  • An indexer access. Every indexer access has an associated type, namely the element type of the indexer. Furthermore, an indexer access has an associated instance expression and an associated argument list. When an accessor (the get or set block) of an indexer access is invoked, the result of evaluating the instance expression becomes the instance represented by this (ยง7.6.7), and the result of evaluating the argument list becomes the parameter list of the invocation.

Think of this as similar to the difference between a field and a property:

 public class Test
 {
     public int PublicField;
     public int PublicProperty { get; set; }
 }

 ...

 public void MethodCall(ref int x) { ... }

 ...

 Test test = new Test();
 MethodCall(ref test.PublicField); // Fine
 MethodCall(ref test.PublicProperty); // Not fine

Fundamentally, an indexer is a pair of methods (or a single one) whereas an array access gives you a storage location.

Note that if you weren't using a mutable struct to start with, you wouldn't see the difference in this way - I'd strongly advise against using mutable structs at all.

people

See more on this question at Stackoverflow