using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using Unity.Mathematics; using Unity.Jobs; using System.Runtime.InteropServices; using Unity.Jobs.LowLevel.Unsafe; namespace Unity.Collections.LowLevel.Unsafe { [StructLayout(LayoutKind.Sequential)] [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int) })] internal unsafe struct HashMapHelper where TKey : unmanaged, IEquatable { [NativeDisableUnsafePtrRestriction] internal byte* Ptr; [NativeDisableUnsafePtrRestriction] internal TKey* Keys; [NativeDisableUnsafePtrRestriction] internal int* Next; [NativeDisableUnsafePtrRestriction] internal int* Buckets; internal int Count; internal int Capacity; internal int Log2MinGrowth; internal int BucketCapacity; internal int AllocatedIndex; internal int FirstFreeIdx; internal int SizeOfTValue; internal AllocatorManager.AllocatorHandle Allocator; internal const int kMinimumCapacity = 256; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal int CalcCapacityCeilPow2(int capacity) { capacity = math.max(math.max(1, Count), capacity); var newCapacity = math.max(capacity, 1 << Log2MinGrowth); var result = math.ceilpow2(newCapacity); return result; } internal static int GetBucketSize(int capacity) { return capacity * 2; } internal readonly bool IsCreated { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Ptr != null; } internal readonly bool IsEmpty { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => !IsCreated || Count == 0; } internal void Clear() { UnsafeUtility.MemSet(Buckets, 0xff, BucketCapacity * sizeof(int)); UnsafeUtility.MemSet(Next, 0xff, Capacity * sizeof(int)); Count = 0; FirstFreeIdx = -1; AllocatedIndex = 0; } internal void Init(int capacity, int sizeOfValueT, int minGrowth, AllocatorManager.AllocatorHandle allocator) { Count = 0; Log2MinGrowth = (byte)(32 - math.lzcnt(math.max(1, minGrowth) - 1)); capacity = CalcCapacityCeilPow2(capacity); Capacity = capacity; BucketCapacity = GetBucketSize(capacity); Allocator = allocator; SizeOfTValue = sizeOfValueT; int keyOffset, nextOffset, bucketOffset; int totalSize = CalculateDataSize(capacity, BucketCapacity, sizeOfValueT, out keyOffset, out nextOffset, out bucketOffset); Ptr = (byte*)Memory.Unmanaged.Allocate(totalSize, JobsUtility.CacheLineSize, allocator); Keys = (TKey*)(Ptr + keyOffset); Next = (int*)(Ptr + nextOffset); Buckets = (int*)(Ptr + bucketOffset); Clear(); } internal void Dispose() { Memory.Unmanaged.Free(Ptr, Allocator); Ptr = null; Keys = null; Next = null; Buckets = null; Count = 0; BucketCapacity = 0; } internal static HashMapHelper* Alloc(int capacity, int sizeOfValueT, int minGrowth, AllocatorManager.AllocatorHandle allocator) { var data = (HashMapHelper*)Memory.Unmanaged.Allocate(sizeof(HashMapHelper), UnsafeUtility.AlignOf>(), allocator); data->Init(capacity, sizeOfValueT, minGrowth, allocator); return data; } internal static void Free(HashMapHelper* data) { if (data == null) { throw new InvalidOperationException("Hash based container has yet to be created or has been destroyed!"); } data->Dispose(); Memory.Unmanaged.Free(data, data->Allocator); } internal void Resize(int newCapacity) { newCapacity = math.max(newCapacity, Count); var newBucketCapacity = math.ceilpow2(GetBucketSize(newCapacity)); if (Capacity == newCapacity && BucketCapacity == newBucketCapacity) { return; } ResizeExact(newCapacity, newBucketCapacity); } internal void ResizeExact(int newCapacity, int newBucketCapacity) { int keyOffset, nextOffset, bucketOffset; int totalSize = CalculateDataSize(newCapacity, newBucketCapacity, SizeOfTValue, out keyOffset, out nextOffset, out bucketOffset); var oldPtr = Ptr; var oldKeys = Keys; var oldNext = Next; var oldBuckets = Buckets; var oldBucketCapacity = BucketCapacity; Ptr = (byte*)Memory.Unmanaged.Allocate(totalSize, JobsUtility.CacheLineSize, Allocator); Keys = (TKey*)(Ptr + keyOffset); Next = (int*)(Ptr + nextOffset); Buckets = (int*)(Ptr + bucketOffset); Capacity = newCapacity; BucketCapacity = newBucketCapacity; Clear(); for (int i = 0, num = oldBucketCapacity; i < num; ++i) { for (int idx = oldBuckets[i]; idx != -1; idx = oldNext[idx]) { var newIdx = TryAdd(oldKeys[idx]); UnsafeUtility.MemCpy(Ptr + SizeOfTValue * newIdx, oldPtr + SizeOfTValue * idx, SizeOfTValue); } } Memory.Unmanaged.Free(oldPtr, Allocator); } internal void TrimExcess() { var capacity = CalcCapacityCeilPow2(Count); ResizeExact(capacity, GetBucketSize(capacity)); } internal static int CalculateDataSize(int capacity, int bucketCapacity, int sizeOfTValue, out int outKeyOffset, out int outNextOffset, out int outBucketOffset) { var sizeOfTKey = sizeof(TKey); var sizeOfInt = sizeof(int); var valuesSize = sizeOfTValue * capacity; var keysSize = sizeOfTKey * capacity; var nextSize = sizeOfInt * capacity; var bucketSize = sizeOfInt * bucketCapacity; var totalSize = valuesSize + keysSize + nextSize + bucketSize; outKeyOffset = 0 + valuesSize; outNextOffset = outKeyOffset + keysSize; outBucketOffset = outNextOffset + nextSize; return totalSize; } internal readonly int GetCount() { if (AllocatedIndex <= 0) { return 0; } var numFree = 0; for (var freeIdx = FirstFreeIdx; freeIdx >= 0; freeIdx = Next[freeIdx]) { ++numFree; } return math.min(Capacity, AllocatedIndex) - numFree; } [MethodImpl(MethodImplOptions.AggressiveInlining)] int GetBucket(in TKey key) { return (int)((uint)key.GetHashCode() & (BucketCapacity - 1)); } internal int TryAdd(in TKey key) { if (-1 == Find(key)) { // Allocate an entry from the free list int idx; int* next; if (AllocatedIndex >= Capacity && FirstFreeIdx < 0) { int newCap = CalcCapacityCeilPow2(Capacity + (1 << Log2MinGrowth)); Resize(newCap); } idx = FirstFreeIdx; if (idx >= 0) { FirstFreeIdx = Next[idx]; } else { idx = AllocatedIndex++; } CheckIndexOutOfBounds(idx); UnsafeUtility.WriteArrayElement(Keys, idx, key); var bucket = GetBucket(key); // Add the index to the hash-map next = Next; next[idx] = Buckets[bucket]; Buckets[bucket] = idx; Count++; return idx; } return -1; } internal int Find(TKey key) { if (AllocatedIndex > 0) { // First find the slot based on the hash var bucket = GetBucket(key); var entryIdx = Buckets[bucket]; if ((uint)entryIdx < (uint)Capacity) { var nextPtrs = Next; while (!UnsafeUtility.ReadArrayElement(Keys, entryIdx).Equals(key)) { entryIdx = nextPtrs[entryIdx]; if ((uint)entryIdx >= (uint)Capacity) { return -1; } } return entryIdx; } } return -1; } [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int) })] internal bool TryGetValue(TKey key, out TValue item) where TValue : unmanaged { var idx = Find(key); if (-1 != idx) { item = UnsafeUtility.ReadArrayElement(Ptr, idx); return true; } item = default; return false; } internal int TryRemove(TKey key) { if (Capacity != 0) { var removed = 0; // First find the slot based on the hash var bucket = GetBucket(key); var prevEntry = -1; var entryIdx = Buckets[bucket]; while (entryIdx >= 0 && entryIdx < Capacity) { if (UnsafeUtility.ReadArrayElement(Keys, entryIdx).Equals(key)) { ++removed; // Found matching element, remove it if (prevEntry < 0) { Buckets[bucket] = Next[entryIdx]; } else { Next[prevEntry] = Next[entryIdx]; } // And free the index int nextIdx = Next[entryIdx]; Next[entryIdx] = FirstFreeIdx; FirstFreeIdx = entryIdx; entryIdx = nextIdx; break; } else { prevEntry = entryIdx; entryIdx = Next[entryIdx]; } } Count -= removed; return 0 != removed ? removed : -1; } return -1; } internal bool MoveNextSearch(ref int bucketIndex, ref int nextIndex, out int index) { for (int i = bucketIndex, num = BucketCapacity; i < num; ++i) { var idx = Buckets[i]; if (idx != -1) { index = idx; bucketIndex = i + 1; nextIndex = Next[idx]; return true; } } index = -1; bucketIndex = BucketCapacity; nextIndex = -1; return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool MoveNext(ref int bucketIndex, ref int nextIndex, out int index) { if (nextIndex != -1) { index = nextIndex; nextIndex = Next[nextIndex]; return true; } return MoveNextSearch(ref bucketIndex, ref nextIndex, out index); } internal NativeArray GetKeyArray(AllocatorManager.AllocatorHandle allocator) { var result = CollectionHelper.CreateNativeArray(Count, allocator, NativeArrayOptions.UninitializedMemory); for (int i = 0, count = 0, max = result.Length, capacity = BucketCapacity ; i < capacity && count < max ; ++i ) { int bucket = Buckets[i]; while (bucket != -1) { result[count++] = UnsafeUtility.ReadArrayElement(Keys, bucket); bucket = Next[bucket]; } } return result; } [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int) })] internal NativeArray GetValueArray(AllocatorManager.AllocatorHandle allocator) where TValue : unmanaged { var result = CollectionHelper.CreateNativeArray(Count, allocator, NativeArrayOptions.UninitializedMemory); for (int i = 0, count = 0, max = result.Length, capacity = BucketCapacity ; i < capacity && count < max ; ++i ) { int bucket = Buckets[i]; while (bucket != -1) { result[count++] = UnsafeUtility.ReadArrayElement(Ptr, bucket); bucket = Next[bucket]; } } return result; } [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int) })] internal NativeKeyValueArrays GetKeyValueArrays(AllocatorManager.AllocatorHandle allocator) where TValue : unmanaged { var result = new NativeKeyValueArrays(Count, allocator, NativeArrayOptions.UninitializedMemory); for (int i = 0, count = 0, max = result.Length, capacity = BucketCapacity ; i < capacity && count < max ; ++i ) { int bucket = Buckets[i]; while (bucket != -1) { result.Keys[count] = UnsafeUtility.ReadArrayElement(Keys, bucket); result.Values[count] = UnsafeUtility.ReadArrayElement(Ptr, bucket); count++; bucket = Next[bucket]; } } return result; } internal unsafe struct Enumerator { [NativeDisableUnsafePtrRestriction] internal HashMapHelper* m_Data; internal int m_Index; internal int m_BucketIndex; internal int m_NextIndex; internal unsafe Enumerator(HashMapHelper* data) { m_Data = data; m_Index = -1; m_BucketIndex = 0; m_NextIndex = -1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool MoveNext() { return m_Data->MoveNext(ref m_BucketIndex, ref m_NextIndex, out m_Index); } internal void Reset() { m_Index = -1; m_BucketIndex = 0; m_NextIndex = -1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal KVPair GetCurrent() where TValue : unmanaged { return new KVPair { m_Data = m_Data, m_Index = m_Index }; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal TKey GetCurrentKey() { if (m_Index != -1) { return m_Data->Keys[m_Index]; } return default; } } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")] [MethodImpl(MethodImplOptions.AggressiveInlining)] void CheckIndexOutOfBounds(int idx) { if ((uint)idx >= (uint)Capacity) { throw new InvalidOperationException($"Internal HashMap error. idx {idx}"); } } } /// /// An unordered, expandable associative array. /// /// The type of the keys. /// The type of the values. [StructLayout(LayoutKind.Sequential)] [DebuggerTypeProxy(typeof(UnsafeHashMapDebuggerTypeProxy<,>))] [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int), typeof(int) })] public unsafe struct UnsafeHashMap : INativeDisposable , IEnumerable> // Used by collection initializers. where TKey : unmanaged, IEquatable where TValue : unmanaged { [NativeDisableUnsafePtrRestriction] internal HashMapHelper m_Data; /// /// Initializes and returns an instance of UnsafeHashMap. /// /// The number of key-value pairs that should fit in the initial allocation. /// The allocator to use. public UnsafeHashMap(int initialCapacity, AllocatorManager.AllocatorHandle allocator) { m_Data = default; m_Data.Init(initialCapacity, sizeof(TValue), HashMapHelper.kMinimumCapacity, allocator); } /// /// Releases all resources (memory). /// public void Dispose() { if (!IsCreated) { return; } m_Data.Dispose(); } /// /// Creates and schedules a job that will dispose this hash map. /// /// A job handle. The newly scheduled job will depend upon this handle. /// The handle of a new job that will dispose this hash map. public JobHandle Dispose(JobHandle inputDeps) { if (!IsCreated) { return inputDeps; } var jobHandle = new UnsafeDisposeJob { Ptr = m_Data.Ptr, Allocator = m_Data.Allocator }.Schedule(inputDeps); m_Data = default; return jobHandle; } /// /// Whether this hash map has been allocated (and not yet deallocated). /// /// True if this hash map has been allocated (and not yet deallocated). public readonly bool IsCreated { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Data.IsCreated; } /// /// Whether this hash map is empty. /// /// True if this hash map is empty or if the map has not been constructed. public readonly bool IsEmpty { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Data.IsEmpty; } /// /// The current number of key-value pairs in this hash map. /// /// The current number of key-value pairs in this hash map. public readonly int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Data.Count; } /// /// The number of key-value pairs that fit in the current allocation. /// /// The number of key-value pairs that fit in the current allocation. /// A new capacity. Must be larger than the current capacity. public int Capacity { [MethodImpl(MethodImplOptions.AggressiveInlining)] readonly get => m_Data.Capacity; set => m_Data.Resize(value); } /// /// Removes all key-value pairs. /// /// Does not change the capacity. public void Clear() { m_Data.Clear(); } /// /// Adds a new key-value pair. /// /// If the key is already present, this method returns false without modifying the hash map. /// The key to add. /// The value to add. /// True if the key-value pair was added. public bool TryAdd(TKey key, TValue item) { var idx = m_Data.TryAdd(key); if (-1 != idx) { UnsafeUtility.WriteArrayElement(m_Data.Ptr, idx, item); return true; } return false; } /// /// Adds a new key-value pair. /// /// If the key is already present, this method throws without modifying the hash map. /// The key to add. /// The value to add. /// Thrown if the key was already present. public void Add(TKey key, TValue item) { var result = TryAdd(key, item); if (!result) { ThrowKeyAlreadyAdded(key); } } /// /// Removes a key-value pair. /// /// The key to remove. /// True if a key-value pair was removed. public bool Remove(TKey key) { return -1 != m_Data.TryRemove(key); } /// /// Returns the value associated with a key. /// /// The key to look up. /// Outputs the value associated with the key. Outputs default if the key was not present. /// True if the key was present. public bool TryGetValue(TKey key, out TValue item) { return m_Data.TryGetValue(key, out item); } /// /// Returns true if a given key is present in this hash map. /// /// The key to look up. /// True if the key was present. public bool ContainsKey(TKey key) { return -1 != m_Data.Find(key); } /// /// Sets the capacity to match what it would be if it had been originally initialized with all its entries. /// public void TrimExcess() => m_Data.TrimExcess(); /// /// Gets and sets values by key. /// /// Getting a key that is not present will throw. Setting a key that is not already present will add the key. /// The key to look up. /// The value associated with the key. /// For getting, thrown if the key was not present. public TValue this[TKey key] { get { TValue result; if (!m_Data.TryGetValue(key, out result)) { ThrowKeyNotPresent(key); } return result; } set { var idx = m_Data.Find(key); if (-1 != idx) { UnsafeUtility.WriteArrayElement(m_Data.Ptr, idx, value); return; } TryAdd(key, value); } } /// /// Returns an array with a copy of all this hash map's keys (in no particular order). /// /// The allocator to use. /// An array with a copy of all this hash map's keys (in no particular order). public NativeArray GetKeyArray(AllocatorManager.AllocatorHandle allocator) => m_Data.GetKeyArray(allocator); /// /// Returns an array with a copy of all this hash map's values (in no particular order). /// /// The allocator to use. /// An array with a copy of all this hash map's values (in no particular order). public NativeArray GetValueArray(AllocatorManager.AllocatorHandle allocator) => m_Data.GetValueArray(allocator); /// /// Returns a NativeKeyValueArrays with a copy of all this hash map's keys and values. /// /// The key-value pairs are copied in no particular order. For all `i`, `Values[i]` will be the value associated with `Keys[i]`. /// The allocator to use. /// A NativeKeyValueArrays with a copy of all this hash map's keys and values. public NativeKeyValueArrays GetKeyValueArrays(AllocatorManager.AllocatorHandle allocator) => m_Data.GetKeyValueArrays(allocator); /// /// Returns an enumerator over the key-value pairs of this hash map. /// /// An enumerator over the key-value pairs of this hash map. public Enumerator GetEnumerator() { // return new Enumerator { Data = m_Data, Index = -1 }; fixed (HashMapHelper* data = &m_Data) { return new Enumerator { m_Enumerator = new HashMapHelper.Enumerator(data) }; } } /// /// This method is not implemented. Use instead. /// /// Throws NotImplementedException. /// Method is not implemented. IEnumerator> IEnumerable>.GetEnumerator() { throw new NotImplementedException(); } /// /// This method is not implemented. Use instead. /// /// Throws NotImplementedException. /// Method is not implemented. IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } /// /// An enumerator over the key-value pairs of a container. /// /// /// In an enumerator's initial state, is not valid to read. /// From this state, the first call advances the enumerator to the first key-value pair. /// public struct Enumerator : IEnumerator> { internal HashMapHelper.Enumerator m_Enumerator; /// /// Does nothing. /// public void Dispose() { } /// /// Advances the enumerator to the next key-value pair. /// /// True if is valid to read after the call. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() => m_Enumerator.MoveNext(); /// /// Resets the enumerator to its initial state. /// public void Reset() => m_Enumerator.Reset(); /// /// The current key-value pair. /// /// The current key-value pair. public KVPair Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Enumerator.GetCurrent(); } /// /// Gets the element at the current position of the enumerator in the container. /// object IEnumerator.Current => Current; } /// /// Returns a readonly version of this UnsafeHashMap instance. /// /// ReadOnly containers point to the same underlying data as the UnsafeHashMap it is made from. /// ReadOnly instance for this. public ReadOnly AsReadOnly() { return new ReadOnly(ref m_Data); } /// /// A read-only alias for the value of a UnsafeHashMap. Does not have its own allocated storage. /// [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int), typeof(int) })] public struct ReadOnly : IEnumerable> { [NativeDisableUnsafePtrRestriction] internal HashMapHelper m_Data; internal ReadOnly(ref HashMapHelper data) { m_Data = data; } /// /// Whether this hash map has been allocated (and not yet deallocated). /// /// True if this hash map has been allocated (and not yet deallocated). public readonly bool IsCreated { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Data.IsCreated; } /// /// Whether this hash map is empty. /// /// True if this hash map is empty or if the map has not been constructed. public readonly bool IsEmpty { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Data.IsEmpty; } /// /// The current number of key-value pairs in this hash map. /// /// The current number of key-value pairs in this hash map. public readonly int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Data.Count; } /// /// The number of key-value pairs that fit in the current allocation. /// /// The number of key-value pairs that fit in the current allocation. public readonly int Capacity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Data.Capacity; } /// /// Returns the value associated with a key. /// /// The key to look up. /// Outputs the value associated with the key. Outputs default if the key was not present. /// True if the key was present. public readonly bool TryGetValue(TKey key, out TValue item) => m_Data.TryGetValue(key, out item); /// /// Returns true if a given key is present in this hash map. /// /// The key to look up. /// True if the key was present. public readonly bool ContainsKey(TKey key) { return -1 != m_Data.Find(key); } /// /// Gets values by key. /// /// Getting a key that is not present will throw. /// The key to look up. /// The value associated with the key. /// For getting, thrown if the key was not present. public readonly TValue this[TKey key] { get { TValue result; m_Data.TryGetValue(key, out result); return result; } } /// /// Returns an array with a copy of all this hash map's keys (in no particular order). /// /// The allocator to use. /// An array with a copy of all this hash map's keys (in no particular order). public readonly NativeArray GetKeyArray(AllocatorManager.AllocatorHandle allocator) => m_Data.GetKeyArray(allocator); /// /// Returns an array with a copy of all this hash map's values (in no particular order). /// /// The allocator to use. /// An array with a copy of all this hash map's values (in no particular order). public readonly NativeArray GetValueArray(AllocatorManager.AllocatorHandle allocator) => m_Data.GetValueArray(allocator); /// /// Returns a NativeKeyValueArrays with a copy of all this hash map's keys and values. /// /// The key-value pairs are copied in no particular order. For all `i`, `Values[i]` will be the value associated with `Keys[i]`. /// The allocator to use. /// A NativeKeyValueArrays with a copy of all this hash map's keys and values. public readonly NativeKeyValueArrays GetKeyValueArrays(AllocatorManager.AllocatorHandle allocator) => m_Data.GetKeyValueArrays(allocator); /// /// Returns an enumerator over the key-value pairs of this hash map. /// /// An enumerator over the key-value pairs of this hash map. public readonly Enumerator GetEnumerator() { fixed (HashMapHelper* data = &m_Data) { return new Enumerator { m_Enumerator = new HashMapHelper.Enumerator(data) }; } } /// /// This method is not implemented. Use instead. /// /// Throws NotImplementedException. /// Method is not implemented. IEnumerator> IEnumerable>.GetEnumerator() { throw new NotImplementedException(); } /// /// This method is not implemented. Use instead. /// /// Throws NotImplementedException. /// Method is not implemented. IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")] void ThrowKeyNotPresent(TKey key) { throw new ArgumentException($"Key: {key} is not present."); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")] void ThrowKeyAlreadyAdded(TKey key) { throw new ArgumentException($"An item with the same key has already been added: {key}"); } } internal sealed class UnsafeHashMapDebuggerTypeProxy where TKey : unmanaged, IEquatable where TValue : unmanaged { HashMapHelper Data; public UnsafeHashMapDebuggerTypeProxy(UnsafeHashMap target) { Data = target.m_Data; } public UnsafeHashMapDebuggerTypeProxy(UnsafeHashMap.ReadOnly target) { Data = target.m_Data; } public List> Items { get { var result = new List>(); using (var kva = Data.GetKeyValueArrays(Allocator.Temp)) { for (var i = 0; i < kva.Length; ++i) { result.Add(new Pair(kva.Keys[i], kva.Values[i])); } } return result; } } } }