Skip to content

Add CollectionsMarshal.SetCount API for List<T> #55217

@MichalPetryka

Description

@MichalPetryka

Background and Motivation

The existing CollectionsMarshal.AsSpan(List) API allows developers to use List for performant code as a Span, however it restricts modifications of it by having no way to change the Count of the List. Exposing an API in CollectionsMarshal for doing that would solve that and help to remove redundant array allocations and AddRange copies when adding data to a List that comes from an API that takes a Span (AddRange doesn't accept Spans so the buffers can't even be stack allocated).

Implementation details:

  • Passing the current count would be an noop
  • List version would be changed unless the count would be the same as the current one
  • Passing a count bigger than the current capacity, the List would be resized
  • Passing a count smaller than the current one would shrink the list and clear the data ONLY if it contains GC references (just like Clear does)
  • Passing a count bigger than the current one would increase the count and it would NOT initialize the exposed data.

Proposed API

namespace System.Runtime.InteropServices
{
    public static class CollectionsMarshal {
+        public static void SetCount<T>(List<T> list, int count); // alternative could be EnsureCount
    }
}

Example implementation:

public static void SetCount<T>(List<T> list, int count)
{
    if (count < 0)
        throw new ArgumentOutOfRangeException(); // would probably be a throw helper

    if (count == list._size)
        return;

    list._version++; // would need to be made internal

    if (count < list._size)
    {
        if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
            Array.Clear(list._items, count, list._size - count);
        list._size = count;
        return;
    }

    if (count > list.Capacity)
        list.Capacity = count;

    list._size = count;
}

Usage Examples

The API could be used for creating an extension method in place of the missing ReadOnlySpan AddRange:

public static void AddRange<T>(this List<T> list, ReadOnlySpan<T> span)
{
    int oldCount = list.Count;
    CollectionMarshal.SetCount(list, oldCount + span.Length);
    span.CopyTo(CollectionMarshal.AsSpan(list).Slice(oldCount))
}

Random.GetBytes is an example API that fills a Span with data here.

List<byte> list = new(16);
CollectionsMarshal.SetCount(list, 16);
Random.Shared.GetBytes(CollectionsMarshal.AsSpan(list));
// we have a count 16 list filled with random data

This would be as valid, just a bit less performant:

List<byte> list = new();
CollectionsMarshal.SetCount(list, 16);
Random.Shared.GetBytes(CollectionsMarshal.AsSpan(list));
// we have a count 16 list filled with random data

With this the end count would also be 16

List<byte> list = new() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 };
CollectionsMarshal.SetCount(list, 16);
Random.Shared.GetBytes(CollectionsMarshal.AsSpan(list));
// we have a count 16 list filled with random data

With this the end count would also be 16 and we'd have the original content from before Clear after SetLength (not guaranteed, implementation detail)

List<byte> list = new() {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 };
list.Clear();
CollectionsMarshal.SetCount(list, 16);
// accessing the list here would give { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 } since Clear wouldn't zero the data
Random.Shared.GetBytes(CollectionsMarshal.AsSpan(list));
// we have a count 16 list filled with random data

Alternative Designs

Currently the developer is required to do this, which requires an array allocation and a copy coming from AddRange:

List<byte> list = new(16);
list.AddRange(new byte[16]);

The first example usage would look like this:

List<byte> list = new(16);
byte[] array = new byte[16];
RandomNumberGenerator.Fill(array);
list.AddRange(array);

Risks

The API could expose non cleared memory for value types with no GC references (when the List was cleared or if Lists would use GetUninitalizedArray when the initial array was allocated unitialized).

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions