ResizableArray and ListSlice

ResizableArray<T> and ListSlice<T> are runtime collection utilities used by Ultimate Inventory System APIs that need to reuse buffers or pass a range of results without creating a new list every time. You are most likely to encounter them while extending inventory queries, item actions, crafting, currency calculations, or UI filtering.

Use them only when their ownership rules are useful and measured allocations matter. A normal array or List<T> is clearer for most project data, Inspector-authored settings, and code whose size changes unpredictably.

The published URL keeps the historical resizeablearrays-and-listslices spelling. The released Version 1 C# names are exactly ResizableArray<T> and ListSlice<T> in the Opsive.Shared.Utility namespace.

Choose the appropriate collection

Need Prefer Why
A fixed set of Inspector or saved data Array Ownership, length, and Unity serialization are straightforward.
A general-purpose collection with unpredictable additions and removals List<T> Familiar behavior and geometric capacity growth suit frequent changes.
A reusable runtime buffer whose approximate maximum size is known ResizableArray<T> Logical Count can be reset while the backing array is retained.
A temporary, read-only view passed to or returned from a UIS API ListSlice<T> The struct refers to an existing source instead of copying its elements.
A result that must outlive or be independent of its source buffer An owned array or list copy Later source changes cannot alter the result.

Neither utility is automatically faster. ResizableArray<T> still allocates when it exhausts its backing array, and ListSlice<T> saves a copy only while the caller can safely share the source and its lifetime.

Understand ResizableArray ownership

ResizableArray<T> is a class that wraps a T[] and maintains two sizes:

  • Count is the number of logical elements.
  • Array.Length is the capacity of the exposed backing array.

new ResizableArray<T>(8) creates capacity for eight elements but starts with Count == 0. Index, enumerate, sort, and copy only the range from zero up to Count - 1, even though the Array property may expose more slots.

Calling Add writes at Count and increases Count. If the buffer is full, released Version 1 grows it to exactly the required count rather than using the geometric growth of List<T>. Preallocate a realistic capacity when additions happen in a hot path; otherwise repeated additions beyond capacity can repeatedly allocate and copy the array.

Reset or release retained values

The reset methods have different effects:

Operation Logical Count Backing array
Clear() Becomes zero Capacity and old slot values are retained.
RemoveAll() Becomes zero Every previously counted slot is set to default(T); capacity is retained.
Trim(newCount) Can only become smaller Capacity and trimmed slot values are retained.
Resize(newCount) Becomes the requested nonnegative count Capacity grows only when required; retained values can become logical again.
Truncate() Unchanged The backing array is resized to exactly Count.

Use Clear() when retaining references is intentional and every new logical slot will be overwritten. Use parameterless RemoveAll() when the buffer should release references to old Item data. After Clear() or Trim(), expanding Count within existing capacity can expose retained values; assign every newly active element before reading it.

Borrow or copy an input array

Initialize(array, false) and new ResizableArray<T>(array, false, count) keep the supplied array as the backing storage. Mutating that array mutates the ResizableArray<T>, and mutating the wrapper’s Array mutates the caller’s array.

Pass true for the copy argument when the wrapper must own an independent array. The overload that initializes from another ResizableArray<T> also copies its entire backing capacity and logical Count.

The static ResizableArray.Copy methods copy active elements into a caller-supplied ref T[] buffer, then return a ResizableArray<T> backed by that result buffer. The new wrapper no longer shares the source, but it does share the supplied result buffer. Do not reuse or overwrite that buffer while the returned wrapper is still needed.

Understand ListSlice lifetime

ListSlice<T> is a struct containing a reference to an IReadOnlyList<T> plus start and end metadata. Constructing or passing the struct does not copy the elements.

For the safe and common Version 1 case, create a full-range view whose start is zero:

using Opsive.Shared.Utility;

var itemNames = new ResizableArray<string>(8);
itemNames.Add("Health Potion");
itemNames.Add("Iron Sword");
itemNames.Add("Fire Wand");

ListSlice<string> visibleItems = itemNames;

for (var i = 0; i < visibleItems.Count; i++) {
    UnityEngine.Debug.Log(visibleItems[i]);
}

ListSlice<T> does not implement IEnumerable<T>, so use Count and the indexer rather than foreach.

The slice records its end when it is created. If itemNames later adds a fourth element, the existing slice still reports three. Changing an existing source element is visible through the slice. Removing elements, clearing the source, resizing a reused output array, or reusing a pooled result buffer can make an older slice stale or invalid. Treat a slice as borrowed data that is valid only until its source is changed.

Null, empty, and one-element views

  • default(ListSlice<T>) and ListSlice<T>.Empty both have Count zero and IsNull == true in released Version 1.
  • A slice created from a non-null empty array or list has Count zero and IsNull == false.
  • The implicit conversion from one T creates a one-element virtual slice without a backing list.
  • Last is valid only when Count is greater than zero.

Use IsNull only when the difference between "no source" and "an empty source" matters. Use Count == 0 for ordinary empty-result handling.

Keep ref-buffer results local

Several UIS methods accept a reusable ref T[] and return a ListSlice<T> over the populated part. This avoids allocating a result collection on every call, but the caller owns the buffer lifetime.

using Opsive.UltimateInventorySystem.Core.DataStructures;
using Opsive.UltimateInventorySystem.Core.InventoryCollections;

public sealed class BagQuery
{
    private ItemInfo[] m_ItemInfoBuffer;

    public void CheckBag(Inventory inventory)
    {
        var results = inventory.GetItemInfos(
            ref m_ItemInfoBuffer,
            itemInfo => itemInfo.Item != null);

        for (var i = 0; i < results.Count; i++) {
            var itemInfo = results[i];
            // Inspect Health Potion, Iron Sword, or Fire Wand here.
        }

        // Consume or copy results before another call reuses m_ItemInfoBuffer.
    }
}

The example uses the released Inventory.GetItemInfos pattern and a start index of zero. The returned slice and the field buffer are working storage, not durable inventory state.

If the result must outlive the next query, copy the active entries into storage that your code owns. ListSlice<T>.CopyTo(ref buffer) can reuse a destination array, but its returned slice then borrows that destination buffer. ToArray() creates a new array for a full, zero-based list-backed slice, with the Version 1 limitations described below.

Use a reusable buffer safely

The following component rebuilds a small display list without retaining old references. It uses the shared Health Potion, Iron Sword, and Fire Wand examples, but the same pattern applies to ItemInfo results.

using Opsive.Shared.Utility;
using UnityEngine;

public sealed class InventoryNameBuffer : MonoBehaviour
{
    private readonly ResizableArray<string> m_Names =
        new ResizableArray<string>(8);

    public void Rebuild()
    {
        m_Names.RemoveAll();
        m_Names.Add("Health Potion");
        m_Names.Add("Iron Sword");
        m_Names.Add("Fire Wand");

        ListSlice<string> names = m_Names;
        for (var i = 0; i < names.Count; i++) {
            Debug.Log(names[i]);
        }
    }
}

Use a for loop in frequently executed code. The ResizableArray<T> enumerator is implemented with yield return; foreach is supported for ResizableArray<T>, but it creates an enumerator rather than providing a special allocation-free struct enumerator.

Verify the behavior with tests

Test Count, aliasing, and lifetime assumptions before using either type in a performance-sensitive extension.

using NUnit.Framework;
using Opsive.Shared.Utility;

public class CollectionUtilityTests
{
    [Test]
    public void FullSliceSharesValuesButKeepsItsOriginalCount()
    {
        var source = new ResizableArray<string>(3);
        source.Add("Health Potion");
        source.Add("Iron Sword");

        ListSlice<string> view = source;
        source[0] = "Greater Health Potion";
        source.Add("Fire Wand");

        Assert.AreEqual("Greater Health Potion", view[0]);
        Assert.AreEqual(2, view.Count);
    }

    [Test]
    public void RemoveAllReusesCapacityAndClearsReferences()
    {
        var source = new ResizableArray<string>(3);
        source.Add("Health Potion");

        source.RemoveAll();

        Assert.AreEqual(0, source.Count);
        Assert.AreEqual(3, source.Array.Length);
        Assert.IsNull(source.Array[0]);
    }
}

For a UIS-specific test, query Bag into a reusable ItemInfo[], assert the returned Count and Item Definitions, mutate Bag, query again, and verify the new result. Do not keep the first slice and expect it to remain a snapshot after the buffer is reused.

Serialization and editor use

These utilities are runtime and developer-facing types, not normal UIS database objects.

  • ResizableArray<T> is not marked [Serializable] in Opsive Shared 2.0. UIS commonly uses it for runtime buffers or serializes its data explicitly.
  • ListSlice<T> is not a serializable data container; it stores a temporary reference and range.
  • Do not expect a [SerializeField] ResizableArray<T> or ListSlice<T> field to behave like a normal Inspector-authored list.
  • Do not save a slice as inventory state. Save Items, collections, attributes, or project data through the appropriate UIS saver, then rebuild temporary buffers after loading.

Use an array, List<T>, ScriptableObject, or UIS data object for authoring. Use these utilities inside runtime code where their buffer-sharing behavior is explicit.

Troubleshooting

Symptom Check Fix
Allocations still occur while adding results Compare Count with Array.Length and inspect whether Add repeatedly exhausts capacity. Preallocate a realistic capacity, reuse the same instance, or use List<T> when growth is unpredictable.
Old Item references appear after rebuilding Check whether code called Clear() or Trim() and then enlarged Count without assigning every slot. Call RemoveAll() to clear references, or overwrite every newly active element before reading it.
A saved slice changes after another query Check whether both results share the same ref T[] buffer. Consume the slice before the next query or make an owned copy of its active elements.
Indexing a slice throws after its source changes Compare the slice’s recorded Count with the source’s current length or Count. Treat the slice as invalid after structural source changes and request a new slice.
foreach does not compile for ListSlice<T> Check the type; it does not implement an enumerable interface. Iterate from zero to Count with a for loop.
Last throws Check whether Count is zero. Test Count before reading Last.
Removing an equal reference object returns false Check whether it is the same object instance. Version 1 Remove(T) uses reference identity for class types. Pass the stored instance, or locate the intended index and call RemoveAt after validating it.
The collection does not appear or persist as expected in the Inspector Check whether a runtime utility was used as authored or saved data. Store authored data in a supported array, list, UIS object, or saver and rebuild the runtime utility.

Current Version 1 boundaries

  • Ultimate Inventory System 1.3.0 depends on Opsive Shared 2.0.0; its namespace is Opsive.Shared.Utility, not the Version 3 Opsive.Shared.Runtime.Utility namespace.
  • The historical page slug says resizeable, but the type and source filename are ResizableArray and ResizableArray.cs.
  • The Version 1 ListSlice<T> implementation is reliable for the common start-zero view used by UIS. Nonzero-start constructors, ToListSlice(start, end), and NewSlice have inconsistent offset behavior in the indexer, copy, array-conversion, and sort paths. Avoid nonzero starts unless the Shared package has been patched and tested for the project.
  • ListSlice<T>.ToArray() does not handle its virtual one-element form and is unsafe with the nonzero-start limitation. Use CopyTo(ref buffer) for a one-element slice, or copy a tested start-zero list-backed slice.
  • ListSlice constructors do not validate start and end. Count can be invalid, and an empty Last access throws.
  • ResizableArray<T> exposes its backing array, and its indexer validates only the backing array bounds, not logical Count. Always enforce 0 <= index < Count in project code.
  • Insert on an empty, preallocated Version 1 ResizableArray<T> can access the element before index zero. Use Add for the first element.
  • RemoveAt, copy overloads, and constructor Count values do not add full range or capacity validation. Validate indexes, Count, source ranges, and destination capacity before calling them.
  • Remove(T) cannot remove null and uses reference identity for class instances, while Contains and IndexOf use EqualityComparer<T>.Default.
  • The utilities are not synchronized, job-safe, native collections, save records, or immutable snapshots.
  • Inventory explains the collections queried by many ListSlice<ItemInfo> APIs.
  • Item Info explains the Item, amount, collection, and stack context carried by common result buffers.
  • Item Info Filter Sorters covers a runtime workflow that uses slices and reusable arrays.
  • Crafting uses list slices for ingredients and selected results.
  • Events explains UIS event signatures, some of which pass ListSlice<T> values.
  • Save System covers persistent data; temporary buffers and slices should be rebuilt after loading.

API reference

ResizableArray members

ResizableArray<T> implements IList, IList<T>, and IReadOnlyList<T>.

  • Constructors create an empty buffer, preallocate capacity, wrap or copy an array, or copy an IReadOnlyList<T> into a new array.
  • Initialize replaces the current backing data from a size, array, or another ResizableArray<T>.
  • Count, Array, Last, and the indexer expose logical size, storage, the last logical entry, and direct indexed access.
  • Add, AddRange, Insert, Remove, RemoveAt, RemoveAll, and Clear change the logical contents.
  • Trim, Resize, and Truncate change Count or physical capacity with the distinctions described above.
  • Contains, IndexOf, and MoveElementIndex search or reorder the active range.
  • Sort(Comparer<T>) sorts the active range from zero through Count.
  • CopyTo writes Count elements to a supplied array or ResizableArray<T>; destination storage must already be large enough for the instance overloads.
  • Static ResizableArray.Copy copies active elements into a reusable ref T[] and returns a wrapper over that buffer.

ListSlice members

  • Constructors accept an IReadOnlyList<T>, a source with start and end indexes, or one virtual element.
  • Implicit conversions exist from List<T>, T[], ResizableArray<T>, one T, and (IReadOnlyList<T>, start, end) tuples.
  • ToListSlice extension methods create views from an IReadOnlyList<T>.
  • Count, Last, the read-only indexer, IsNull, and Empty expose the view state.
  • Contains compares active values with EqualityComparer<T>.Default.
  • CopyTo(ref T[]) copies into a reusable array and returns a new start-zero slice over it.
  • ToArray() allocates an array, subject to the Version 1 restrictions above.
  • Sort(T[], Comparer<T>) sorts the supplied array using the slice’s stored range; it does not replace or copy the slice’s source.
  • NewSlice(start, end) creates another view over the stored list, but nonzero ranges require the Version 1 caution above.