0

To be able to treat ArraySegment directly like an array, you must cast it to IList with "as". This is described as the proper behavior here:

use of ArraySegment class?

and here:

dotNet ArraySegment Struct

Specifically, Microsoft document says:

If you want to retrieve an element by its index in the ArraySegment object, you must cast it to an IList object and retrieve it or modify it by using the IList.Item[Int32] property. The following example retrieves the element in an ArraySegment object that delimits a section of a string array.

What's perplexing is that IList has methods like "Remove" and "RemoveAt". You would expect those to not work on an arraysegment cast as a List. I tested this, and in fact calling Remove throws a runtime error. But the compiler doesn't catch the problem.

I'm surprised that Microsoft considered this acceptable behavior in the design of ArraySegment.

I was trying to brainstorm a wrapper class or some way to hide the List methods that shouldn't be called on the ArraySegment as List. But I couldn't come up with anything.

Does anyone have a suggestion on how to fix this?

EDIT: IReadOnlyList has been suggested instead of IList. IReadOnlyList causes the List to completely read-only, preventing you from modifying the value of elements stored in underlying array. I want to be able to modify the original array values. I just don't want to be able to write list.Remove() or list.Add() since it's clearly wrong and the compiler shouldn't be allowing it.

To anyone who might suggest Span as an alternative:

I am aware of Span, but Span currently has limitations in .NET Framework and Standard. Specifically, it can only be used as a local variable, and thus cannot be passed to other methods.

And to be honest, I actually think Microsoft's IEnumerable heirarchy leaves a bit to be desired -- I can't figure out any way to make an Indexable sequence like List without it offering Add/Remove functionality. ICollection doesn't support Indexing. If anyone has suggestions on that issue itself, I'm all ears.

JamesHoux
  • 2,999
  • 3
  • 32
  • 50

3 Answers3

1

Turns out, in .NET 4.7.2, the ArraySegment<T> doesn't expose an indexer unless if it's cast to the IList<T> interface, but it does in .NET Core 2.1.

You may cast to the IReadOnlyList<T> interface; note that it doesn't prevent you from changing the contained objects themselves if they are mutable:

The IReadOnlyList<T> represents a list in which the number and order of list elements is read-only. The content of list elements is not guaranteed to be read-only.

So, it only guarantees that the collection itself (the container) is immutable (no adds, removes, replacements). You can't replace an element because the indexer doesn't have a setter.

There's also the ReadOnlyCollection<T> wrapper class (add using System.Collections.ObjectModel;). Also no setter on the indexer though.

If none of these work for you, and you want to implement a wrapper, just create a class that takes in an IList<T> in the constructor, and implement your own indexer. I'd also implement IEnumerable<T>, so that you get LINQ support, and it will also make it work with the foreach loop.

// Add these...
using System.Collections;
using System.Collections.Generic;

//...

public class MyWrapper<T> : IEnumerable<T>
{
  private IList<T> _internalList;

  public MyWrapper(IList<T> list)
  {
    _internalList = list;
  }

  public int Count => _internalList.Count;

  // the indexer
  public T this[int index]
  {
    get => _internalList[index];
    set => _internalList[index] = value;
  }

  public IEnumerator<T> GetEnumerator()
  {
    return _internalList.GetEnumerator();
  }

  IEnumerator IEnumerable.GetEnumerator() 
  {
    return this.GetEnumerator();
  }
}

And then just pass the array segment to the constructor (without casting).

P.S. If this is some internal code, used in few places only, writing a wrapper may or may not be worth the trouble; it's OK to use an IList<T> as long as its clear what the intention was.

Also, if you don't mind working with a copy of the array that's only limited to the range defined by the array segment, and if the copy operation cost is not a concern, you can call either the ToArray or ToList extension method on the segment. But I suspect you want the original array to be updated and you just need a "window" into a segment.

  • Accepted this answer for its thoroughness. Especially the mention that this problem is fixed in .NET Core 2.1. – JamesHoux Oct 01 '18 at 21:31
0

You can still use ArraySegment, which will prevent adding/removing items and access/update items by index through .Array property

var numbers = new[] { 1, 2, 3, 4 };
var segment = new ArraySegment<int>(numbers);

segment.Array[2] = 101;

// segment contains now { 1, 2, 101, 4}

You can always create extension methods to improve readability

public static void UpdateAt<T>(this ArraySegment<T> segment, int index, T value)
{
    segment.Array[index] = value;
}
Fabio
  • 31,528
  • 4
  • 33
  • 72
  • I'm familiar with this. But it looks ugly and requires the user to think consciously about the underlying array. That kind of nullifies the whole benefit of having a handy array segment. The reason Microsoft recommends casting as IList, is because it provides a much more read-able and attractive way of interacting with the arraysegment as if it was just an ordinary indexed container. – JamesHoux Oct 01 '18 at 01:30
  • @JamesHoux, correct, but then you need accept bunch of other "features" coming within `IList` ;) – Fabio Oct 01 '18 at 01:35
  • The extension method looks like a reasonable solution. Better than the IList garbage recommended by Microsoft. I don't understand why they couldn't just make a public Index operator for array segment. Seems ridiculous. – JamesHoux Oct 01 '18 at 01:43
  • Because `ArraySegment` is another object and updating some "hidden/original" array while you working with another object will be **big** surprise. I think Microsoft correctly following "Principle of least astonishment " – Fabio Oct 01 '18 at 01:49
  • "Because ArraySegment is another object and updating some "hidden/original" array while you working with another object will be big surprise. I think Microsoft correctly following "Principle of least astonishment" <-- That's exactly what you can do when you cast arraysement as IList. Hiding the array indexer from ArraySegment is literally doing nothing when they cancel its effect by suggesting cast to IList – JamesHoux Oct 01 '18 at 21:19
  • in regards to my previous comment about IList, I just read Filip's response about .Net Core 2.0 supporting indexer on array segment. That explains a lot. I'm wondering if there was some limitation in prior versions of .NET that prevented MS from doing that. – JamesHoux Oct 01 '18 at 21:22
  • The `UpdateAt` extension method is incorrect, or confusing at best. `segment.Offset` should be taken into account when indexing `segment.Array` (and preferably its length as well). – IS4 Aug 12 '22 at 12:18
0

You can create extension methods that allow calling the indexer directly, without unnecessary boxing:

public static T Get<T>(this ArraySegment<T> segment, int index)
{
    return GetList<ArraySegment<T>, T>(segment, index);
}

private static T GetList<TList, T>(TList list, int index) where TList : struct, IReadOnlyList<T>
{
    return list[index];
}

public static void Set<T>(this ArraySegment<T> segment, int index, T value)
{
    SetList<ArraySegment<T>, T>(segment, index, value);
}

private static void SetList<TList, T>(TList list, int index, T value) where TList : struct, IReadOnlyList<T>
{
    list[index] = value;
}

Sadly extension indexers are not yet possible, but this at least allows to use the provided indexer in a convenient and fast way.

IS4
  • 11,945
  • 2
  • 47
  • 86