1

Since C# version 8, there is a new way of indexing, as shown in the example:

void Main()
{
    Index idx = ^5; // counted from last to first element

    var sentence = new string[]
    {
        "The", "quick", "brown", "fox",         // position 0 ... 3
        "jumped", "over", "the", "lazy", "dog"  // position 4 ... 8
    };
    var lstSentence = sentence.ToList(); // applicable to list as well

    Console.WriteLine(sentence[idx]);
    Console.WriteLine(lstSentence[idx]);
}

It returns jumped.

So far so good. But how can you get the absolute position representing ^5 efficiently ?
I saw .NET internally is using Index (which I added to this example), and there is a value property.

How can I get the position of sentence[^5] ? Something like

var absPos = sentence[^5].Index.Value; // get absolute position (which is = 4)

(but unfortunately this doesn't seem to exist)

I know there is a formula (number of elements - position from right), but the compiler / .net calculates and probably stores this somewhere.

Update: idx.Value isn't the solution, it just stores the relative value 5 together with .IsFromEnd, which is true. But you can't access it directly, like sentence[^5].Index.Value which is what I wanted to know.

Note: This is also useful for Dictionary, if you take a look at the RudimentaryMultiValuedDictionary Example - if you want to add an additional indexer like public List<TValue> this[Index idx] you will require that. internalDictionary.ElementAtOrDefault(i), which you need for this purpose, does only work with integer values (absolute positions).

Matt
  • 25,467
  • 18
  • 120
  • 187
  • 3
    The ^0 is equal to _sentence.Length_ so your index should be _sentence.Length - 5_ https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8#indices-and-ranges – Steve Oct 07 '20 at 08:46
  • What do you mean with absolute position? – Emanuele Oct 07 '20 at 08:46
  • No, this is not possible. There is no language feature that tells you this. The `^5` construct literally means "5 back from the end", the index is evaluated during array indexing, and what you get back is the content of the slot in the array at that index, not something that knows its position in the array. You would have to create a method that mimicks the code that evaluates `^5` and returns the index for you. – Lasse V. Karlsen Oct 07 '20 at 08:48
  • @Emanuele - the index of the array item - in the example the value 4. – Matt Oct 07 '20 at 08:51
  • And no, the compiler or .NET does not store this anywhere. This is calculated when you use it. `^5` knows nothing about the array it will be used with, and the exact index is worked out during indexing into the array using the `Index` type. – Lasse V. Karlsen Oct 07 '20 at 08:54
  • @Lasse V. Karlsen - I modified the example a bit so that you now have the variable `idx`, which essentially stores `^5`. But I was curious how to get it from the expression `sentence[^5]`. – Matt Oct 07 '20 at 09:00
  • You need to use `idx.GetOffset(sentence.Length)` for your example. An `Index` is not associated with any collection, so you need a collection's length to get at the offset represented by an `Index`. – Matthew Watson Oct 07 '20 at 09:01
  • @Matt You can't. `sentence[^5]` will return the 5th element counted back from the end of the array. That element is a string, and does not have information about whether it is referenced from an array and what index that might be. You will have to do something else. – Lasse V. Karlsen Oct 07 '20 at 09:01
  • @Steve - yes, that is the definition. But I was curious how to get it from the expression `sentence[^5]` - is there a property stored, is there a method from which I can get it? – Matt Oct 07 '20 at 09:01
  • @BiesiGrr - right, you can calculate it. But how to access it from the expression `sentence[^5]` ? – Matt Oct 07 '20 at 09:02
  • 1
    @Matt Answer to your latest comment is no. It is not stored. `Index.GetOffset` is the method you want. `(^5).GetOffset(sentence.Length)` will tell you the index into `sentence`. – Lasse V. Karlsen Oct 07 '20 at 09:03
  • So from what you told me it seems there is no such support built inC# ... you need to use a separate variable and can't obtain it inline. – Matt Oct 07 '20 at 09:06

1 Answers1

1

If you use a decompiler to see what code is generated for your example, it looks like this:

private static void Main()
{
    Index index = new Index(5, true);
    string[] strArray = new string[] { "The", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog" };
    Console.WriteLine(strArray[index.GetOffset((int)strArray.Length)]);
}

As you can see, it just uses index.GetOffset((int)strArray1.Length) to calculate the offset. There's no way to do this yourself other than by doing it the same way. There's no other language construct that you can use.

(I'm not sure why it casts the strArray1.Length property to int, since it's already an int.)

Also note that if you put the index inline using code like this:

var sentence = new string[]
{
    "The", "quick", "brown", "fox",
    "jumped", "over", "the", "lazy", "dog"
};

Console.WriteLine(sentence[^5]);

That gets compiled to:

string[] strArray = new string[] { "The", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog" };
Console.WriteLine(strArray[(int)strArray.Length - 5]);

Which has optimised away the use of an Index object altogether.


As I mentioned in the comments below, you could write an extension method which accepts an IList. This would then work with anything that implements IList such as List<T>, arrays, ImmutableSortedSet<T> and so on.

For example:

public static class IndexExt
{
    public static int AbsIndexFor(this IList self, Index index)
    {
        return index.GetOffset(self.Count);
    }
}

Then you can do:

var array = new string[]
{
    "The", "quick", "brown", "fox",
    "jumped", "over", "the", "lazy", "dog"
};

Console.WriteLine(array.AbsIndexFor(^5));

var list = new List<string>
{
    "The", "quick", "brown", "fox",
    "jumped", "over", "the", "lazy", "dog"
};

Console.WriteLine(list.AbsIndexFor(^5));
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • The cast to `int` is probably just part of the generated code as "defensive programming", since the JITter will ignore the cast in this case anyway. – Lasse V. Karlsen Oct 07 '20 at 09:17
  • 1
    Thank you, that explains it very well. So there is no way but using an index variable and, maybe to write a helper extension method that allows to say `strArray.GetAbsIndex(index)`: Something like `public static int GetAbsIndex(this string[] strArray, Index index) => index.GetOffset((int)strArray.Length);` – Matt Oct 07 '20 at 09:49
  • Generic version of the extension method above (works for all kind of arrays): `public static int GetAbsIndex(this T[] strArray, Index index) => index.GetOffset((int)strArray.Length);` – Matt Oct 07 '20 at 10:06
  • @Matt If you use `ICollection` instead of `T[]` it would work for all kinds of collections. – Matthew Watson Oct 07 '20 at 10:07
  • Tried it, but then I am getting `CS1929 'string[]' does not contain a definition for 'GetAbsIndex' and the best extension method overload 'ext.GetAbsIndex(ICollection[], Index)' requires a receiver of type 'ICollection[]'` - while the generic way works ... – Matt Oct 07 '20 at 11:51
  • @Matt You should be using `ICollection` not `ICollection[]` and also you should then use `collection.Count` rather than `collection.Length`. I'll update my answer to demonstrate. – Matthew Watson Oct 07 '20 at 11:54
  • @Matt Actually it's better to use `IList` rather than `ICollection` because otherwise you could use the extension method for something that implements `ICollection` but doesn't have an indexer, for example `Stack`. It would be pointless to use the extension method for such a type. – Matthew Watson Oct 07 '20 at 12:04
  • Yep - but in this case it is slightly different - IList has .Count instead of .Length. So one could implement the method: `public static int GetAbsIndex(this IList strArray, Index index) => index.GetOffset((int)strArray.Count);` – Matt Oct 07 '20 at 12:40
  • 1
    @Matt You don't need to - the `IList.Count` maps on to `.Length`. Look at my updated answer - you only need to use `IList` to support both normal arrays AND `List`. See here for an explanation: https://stackoverflow.com/questions/11163297/how-do-arrays-in-c-sharp-partially-implement-ilistt – Matthew Watson Oct 07 '20 at 12:41
  • Yes, I tried it out and it works both on lists and arrays, too. Thanks for the addition! – Matt Oct 07 '20 at 12:43
  • 1
    N.B. - I've recently used this in the `RudimentaryMultiValuedDictionary`, an example from Microsoft which you can find [here](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers) - there I added `public int GetAbsIndex(Index index) => index.GetOffset(internalDictionary.Count());` because I wanted to have an additional indexer with `Index idx` parameter: `internalDictionary.ElementAtOrDefault(i)` needs an absolute position. Very useful! – Matt Oct 08 '20 at 12:26