Files
RobustToolbox/Robust.Shared/Collections/ValueList.cs
Pieter-Jan Briers ce240773e8 ValueList<T> extensions (#5534)
Stack-like functions. Just some code I had lying around and never committed.

Add ROS overload for AddRange
2024-11-28 19:49:51 +01:00

697 lines
21 KiB
C#

// This file includes code based on the List<T> class from https://github.com/dotnet/runtime/
// The original code is Copyright © .NET Foundation and Contributors. All rights reserved. Licensed under the MIT License (MIT).
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Robust.Shared.Collections;
/// <summary>
/// Implementation of <see cref="List{T}"/> that is stored in a struct instead.
/// </summary>
/// <remarks>
/// <para>
/// Storing this implementation in a struct reduces GC and memory overhead from list instances drastically.
/// It is only recommended you use this class for private data;
/// public APIs probably shouldn't expose it unless you know what you're doing.
/// </para>
/// <para>
/// This implementation does not complain if you modify it during iteration. Be careful!
/// </para>
/// <para>
/// The implementation uses an array to store the contained items.
/// This array may be larger (<see cref="Capacity"/>) than the amount of "actual" items stored (<see cref="Count"/>).
/// Adding or removing elements to the list shrinks or grows the available capacity at the end of the array.
/// If there is no remaining capacity left when inserting,
/// a new, larger, array is allocated and elements are copied over.
/// </para>
/// </remarks>
/// <typeparam name="T">The type of item to store in the list.</typeparam>
public struct ValueList<T> : IEnumerable<T>
{
private const int DefaultCapacity = 4;
// List can be null so that the list instance is valid if = defaulted.
// Null backing list is equal to empty array everywhere.
// It follows from this that having a count or capacity > 0 means the list is not null.
private T[]? _items;
// Constructs a List with a given initial capacity. The list is
// initially empty, but will have room for the given number of elements
// before any reallocations are required.
//
public ValueList(int capacity)
{
_items = capacity == 0 ? null : new T[capacity];
Count = 0;
}
/// <summary>
/// Creates a list by copying the contents of the source list.
/// </summary>
public ValueList(List<T> list) : this(list, 0, list.Count)
{
}
/// <summary>
/// Creates a list by copying the contents of the source list.
/// </summary>
public ValueList(List<T> list, int start, int count)
{
_items = new T[count];
var liSpan = CollectionsMarshal.AsSpan(list)[start..(start + count)];
liSpan.CopyTo(_items);
Count = count;
}
/// <summary>
/// Creates a list by copying the contents of the source list.
/// </summary>
public ValueList(IReadOnlyCollection<T> list)
{
var count = list.Count;
_items = new T[count];
foreach (var entry in list)
{
var size = Count;
AddNoResize(entry, size);
}
}
/// <summary>
/// Create a list by copying the contents from another enumerable.
/// </summary>
/// <param name="collection">The enumerable to copy the items from.</param>
public ValueList(IEnumerable<T> collection)
{
_items = collection.ToArray();
Count = _items.Length;
}
/// <summary>
/// Create a list by taking ownership of an existing array.
/// Mutations of the list may mutate the passed array.
/// The count and capacity of the list are both set equal to the array length.
/// </summary>
/// <remarks>
/// If null is passed, it is treated equivalently to an empty array.
/// </remarks>
public static ValueList<T> OwningArray(T[]? array)
{
ValueList<T> list = default;
list._items = array;
list.Count = list.Capacity;
return list;
}
/// <summary>
/// Create a list by taking ownership of an existing array.
/// Mutations of the list may mutate the passed array.
/// The capacity is set to the length of the list.
/// The count can be set separately if the array has more space than valid items.
/// </summary>
/// <remarks>
/// If null is passed, it is treated equivalently to an empty array.
/// </remarks>
/// <exception cref="ArgumentException">
/// Thrown if count is negative or if count is greater than the array capacity.
/// </exception>
public static ValueList<T> OwningArray(T[]? array, int count)
{
ValueList<T> list = default;
list._items = array;
if (count < 0)
throw new ArgumentException("Count cannot be negative.");
if (count >= list.Capacity)
throw new ArgumentException("Count cannot be greater than the size of the array.");
list.Count = count;
return list;
}
public int Count { get; private set; }
// Sets or Gets the element at the given index.
public readonly ref T this[int index]
{
get
{
// Following trick can reduce the range check by one
if ((uint)index >= (uint)Count)
throw new IndexOutOfRangeException();
return ref _items![index];
}
}
public int Capacity
{
readonly get => _items?.Length ?? 0;
set
{
if (value < Count)
throw new ArgumentException("Cannot set capacity lower than contained count");
if (value == Capacity)
return;
if (value > 0)
{
var newItems = new T[value];
if (Count > 0)
Array.Copy(_items!, newItems, Count);
_items = newItems;
}
else
{
_items = null;
}
}
}
/// <summary>
/// Span containing the items inside the list.
/// Note that resizing of the backing array will cause this span to be invalidated.
/// </summary>
public readonly Span<T> Span => new(_items, 0, Count);
// Adds the given object to the end of this list. The size of the list is
// increased by one. If required, the capacity of the list is doubled
// before adding the new element.
//
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(T item)
{
var size = Count;
if ((uint)size < (uint)Capacity)
{
AddNoResize(item, size);
}
else
{
AddWithResize(item);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AddNoResize(T item, int size)
{
var array = _items;
Count = size + 1;
array![size] = item;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T AddRef()
{
var array = _items;
var size = Count;
if ((uint)size < (uint)Capacity)
{
Count = size + 1;
return ref array![size];
}
return ref AddRefWithResize();
}
// Non-inline from List.Add to improve its code quality as uncommon path
[MethodImpl(MethodImplOptions.NoInlining)]
private void AddWithResize(T item)
{
Debug.Assert(Count == Capacity);
var size = Count;
Grow(size + 1);
Count = size + 1;
_items![size] = item;
}
// Non-inline from List.Add to improve its code quality as uncommon path
[MethodImpl(MethodImplOptions.NoInlining)]
private ref T AddRefWithResize()
{
Debug.Assert(Count == Capacity);
var size = Count;
Grow(size + 1);
Count = size + 1;
return ref _items![size];
}
// Clears the contents of List.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear()
{
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
{
var size = Count;
Count = 0;
if (size > 0)
{
Array.Clear(_items!, 0, size); // Clear the elements so that the gc can reclaim the references.
}
}
else
{
Count = 0;
}
}
// Contains returns true if the specified element is in the List.
// It does a linear, O(n) search. Equality is determined by calling
// EqualityComparer<T>.Default.Equals().
//
public readonly bool Contains(T item)
{
return IndexOf(item) >= 0;
}
/// <summary>
/// Ensures that the capacity of this list is at least the specified <paramref name="capacity"/>.
/// If the current capacity of the list is less than specified <paramref name="capacity"/>,
/// the capacity is increased by continuously twice current capacity until it is at least the specified <paramref name="capacity"/>.
/// </summary>
/// <param name="capacity">The minimum capacity to ensure.</param>
/// <returns>The new capacity of this list.</returns>
public int EnsureCapacity(int capacity)
{
if (capacity < 0)
throw new ArgumentException("Capacity cannot be negative");
if (Capacity < capacity)
Grow(capacity);
if (capacity == 0)
return capacity;
return _items!.Length;
}
/// <summary>
/// Increase the capacity of this list to at least the specified <paramref name="capacity"/>.
/// </summary>
/// <param name="capacity">The minimum capacity to ensure.</param>
private void Grow(int capacity)
{
Debug.Assert(Capacity < capacity);
int newcapacity = Capacity == 0 ? DefaultCapacity : 2 * _items!.Length;
// Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow.
// Note that this check works even when _items.Length overflowed thanks to the (uint) cast
if ((uint)newcapacity > Array.MaxLength) newcapacity = Array.MaxLength;
// If the computed capacity is still less than specified, set to the original argument.
// Capacities exceeding Array.MaxLength will be surfaced as OutOfMemoryException by Array.Resize.
if (newcapacity < capacity) newcapacity = capacity;
Capacity = newcapacity;
}
// Returns an enumerator for this list with the given
// permission for removal of elements. If modifications made to the list
// while an enumeration is in progress, the MoveNext and
// GetObject methods of the enumerator will throw an exception.
//
public readonly Enumerator GetEnumerator()
=> new Enumerator(this);
IEnumerator<T> IEnumerable<T>.GetEnumerator()
=> new Enumerator(this);
IEnumerator IEnumerable.GetEnumerator()
=> new Enumerator(this);
// Returns the index of the first occurrence of a given value in a range of
// this list. The list is searched forwards from beginning to end.
// The elements of the list are compared to the given value using the
// Object.Equals method.
//
// This method uses the Array.IndexOf method to perform the
// search.
//
public readonly int IndexOf(T item)
=> _items == null ? -1 : Array.IndexOf(_items, item, 0, Count);
// Returns the index of the first occurrence of a given value in a range of
// this list. The list is searched forwards, starting at index
// index and ending at count number of elements. The
// elements of the list are compared to the given value using the
// Object.Equals method.
//
// This method uses the Array.IndexOf method to perform the
// search.
//
public readonly int IndexOf(T item, int index)
{
if (index > Count)
throw new ArgumentOutOfRangeException();
return _items == null ? -1 : Array.IndexOf(_items, item, index, Count - index);
}
// Returns the index of the first occurrence of a given value in a range of
// this list. The list is searched forwards, starting at index
// index and upto count number of elements. The
// elements of the list are compared to the given value using the
// Object.Equals method.
//
// This method uses the Array.IndexOf method to perform the
// search.
//
public readonly int IndexOf(T item, int index, int count)
{
if (index > Count)
throw new ArgumentException("Start index out of bounds");
if (count < 0 || index > Count - count)
throw new ArgumentException("Count out of range");
return _items == null ? -1 : Array.IndexOf(_items, item, index, count);
}
// Inserts an element into this list at a given index. The size of the list
// is increased by one. If required, the capacity of the list is doubled
// before inserting the new element.
//
public void Insert(int index, T item)
{
// Note that insertions at the end are legal.
if ((uint)index > (uint)Count)
{
throw new ArgumentOutOfRangeException();
}
if (Count == _items!.Length) Grow(Count + 1);
if (index < Count)
{
Array.Copy(_items, index, _items, index + 1, Count - index);
}
_items[index] = item;
Count++;
}
// Returns the index of the last occurrence of a given value in a range of
// this list. The list is searched backwards, starting at the end
// and ending at the first element in the list. The elements of the list
// are compared to the given value using the Object.Equals method.
//
// This method uses the Array.LastIndexOf method to perform the
// search.
//
public readonly int LastIndexOf(T item)
{
if (Count == 0)
{
// Special case for empty list
return -1;
}
return LastIndexOf(item, Count - 1, Count);
}
// Returns the index of the last occurrence of a given value in a range of
// this list. The list is searched backwards, starting at index
// index and ending at the first element in the list. The
// elements of the list are compared to the given value using the
// Object.Equals method.
//
// This method uses the Array.LastIndexOf method to perform the
// search.
//
public readonly int LastIndexOf(T item, int index)
{
if (index >= Count)
throw new ArgumentOutOfRangeException(nameof(index), "Index out of range");
return LastIndexOf(item, index, index + 1);
}
// Returns the index of the last occurrence of a given value in a range of
// this list. The list is searched backwards, starting at index
// index and upto count elements. The elements of
// the list are compared to the given value using the Object.Equals
// method.
//
// This method uses the Array.LastIndexOf method to perform the
// search.
//
public readonly int LastIndexOf(T item, int index, int count)
{
if (Count == 0)
{
// Special case for empty list
return -1;
}
if (index < 0)
throw new ArgumentException("Index cannot be negative");
if (count < 0)
throw new ArgumentException("Count cannot be negative");
if (index >= Count)
throw new ArgumentException("Range outside of collection bounds");
if (count > index + 1)
throw new ArgumentException("Range outside of collection bounds");
return Array.LastIndexOf(_items!, item, index, count);
}
// Removes the element at the given index. The size of the list is
// decreased by one.
public bool Remove(T item)
{
var index = IndexOf(item);
if (index >= 0)
{
RemoveAt(index);
return true;
}
return false;
}
// Removes the element at the given index. The size of the list is
// decreased by one.
public void RemoveAt(int index)
{
if ((uint)index >= (uint)Count)
throw new ArgumentOutOfRangeException(nameof(index));
Count--;
if (index < Count)
Array.Copy(_items!, index + 1, _items!, index, Count - index);
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
_items![Count] = default!;
}
public void Sort() => Span.Sort();
public void Sort(IComparer<T>? comparer) => Span.Sort(comparer);
public void Sort(Comparison<T> comparison) => Span.Sort(comparison);
public readonly T[] ToArray() => Span.ToArray();
// Sets the capacity of this list to the size of the list. This method can
// be used to minimize a list's memory overhead once it is known that no
// new elements will be added to the list. To completely clear a list and
// release all memory referenced by the list, execute the following
// statements:
//
// list.Clear();
// list.TrimExcess();
//
public void TrimExcess()
{
var threshold = (int)(Capacity * 0.9);
if (Count < threshold)
Capacity = Count;
}
public struct Enumerator : IEnumerator<T>
{
private readonly ValueList<T> _list;
private int _index;
internal Enumerator(ValueList<T> list)
{
_index = -1;
_list = list;
}
public void Dispose()
{
}
public bool MoveNext()
{
return ++_index < _list.Count;
}
public T Current => RefCurrent;
public ref T RefCurrent => ref _list._items![_index];
object? IEnumerator.Current => Current;
void IEnumerator.Reset()
{
_index = -1;
}
}
/// <summary>
/// <see cref="CollectionExtensions"/>
/// </summary>
public T RemoveSwap(int index)
{
var old = this[index];
var replacement = this[Count - 1];
this[index] = replacement;
RemoveAt(Count - 1);
return old;
}
/// <summary>
/// Adds a range of values from the source list.
/// </summary>
public void AddRange(ValueList<T> list)
{
var liSpan = list.Span;
AddRange(liSpan);
}
/// <summary>
/// Adds a range of values from the source list.
/// </summary>
public void AddRange(List<T> list)
{
var liSpan = CollectionsMarshal.AsSpan(list);
AddRange(liSpan);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddRange(Span<T> span)
{
AddRange((ReadOnlySpan<T>) span);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddRange(ReadOnlySpan<T> span)
{
var spanCount = span.Length;
EnsureCapacity(Count + spanCount);
var target = new Span<T>(_items, Count, spanCount);
span.CopyTo(target);
Count += spanCount;
}
/// <summary>
/// Fills this with default data up to the specified count.
/// </summary>
public void EnsureLength(int newCount)
{
if (Count > newCount)
return;
EnsureCapacity(newCount);
var region = new Span<T>(_items, Count, (newCount - Count));
region.Clear();
Count = newCount;
}
public void AddRange(IEnumerable<T> select)
{
foreach (var result in select)
{
Add(result);
}
}
/// <summary>
/// Push a value onto the end of this list. This is equivalent to <see cref="Add"/>.
/// </summary>
/// <remarks>
/// This method is added to provide completeness with other stack-like functions.
/// </remarks>
/// <param name="item">The item to add to the list.</param>
public void Push(T item)
{
Add(item);
}
/// <summary>
/// Remove and return the value at the end of the list.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the list is empty.</exception>
public T Pop()
{
if (!TryPop(out var value))
throw new InvalidOperationException("List is empty");
return value;
}
/// <summary>
/// Return the value at the end of the list, but do not remove it.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the list is empty.</exception>
public T Peek()
{
if (!TryPeek(out var value))
throw new InvalidOperationException("List is empty");
return value;
}
/// <summary>
/// Remove and return the value at the end of the list, only if the list is not empty.
/// </summary>
/// <returns>True if the list was not empty and an item was removed.</returns>
public bool TryPop([MaybeNullWhen(false)] out T value)
{
if (Count == 0)
{
value = default;
return false;
}
value = _items![--Count];
return true;
}
/// <summary>
/// Remove and return the value at the end of the list, only if the list is not empty.
/// </summary>
/// <returns>True if the list was not empty and an item was removed.</returns>
public bool TryPeek([MaybeNullWhen(false)] out T value)
{
if (Count == 0)
{
value = default;
return false;
}
value = _items![Count];
return true;
}
}