From 1c41eba96e4b83cc89c8a75d018b61c650988d7a Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 1 Feb 2026 10:02:48 +0100 Subject: [PATCH] Refactor IdentityMap to be generic over key and value types IdentityMap is now fully generic as IdentityMap, enabling type-safe value storage and improved flexibility. All internal logic and method signatures are updated to use TKey and TValue. The small int optimization for value storage is removed, and the _useSmallInt flag is disabled by default. Legacy IIdentityMap code is deleted. TypeMetadataWrapper is updated to use the new generic IdentityMap signatures. This refactor improves type safety, eliminates boxing, and prepares the code for value-type scenarios. --- AyCode.Core/Serializers/IdentityMap.cs | 383 ++++-------------- .../Serializers/TypeMetadataWrapper.cs | 37 +- 2 files changed, 94 insertions(+), 326 deletions(-) diff --git a/AyCode.Core/Serializers/IdentityMap.cs b/AyCode.Core/Serializers/IdentityMap.cs index 68256cf..5b6eddb 100644 --- a/AyCode.Core/Serializers/IdentityMap.cs +++ b/AyCode.Core/Serializers/IdentityMap.cs @@ -43,35 +43,35 @@ public interface IIdentityMap /// - Large keys: custom hash table with chaining /// No Dictionary overhead, no per-entry allocation. /// -/// The ID type (int, long, Guid, string) -public sealed class IdentityMap : IIdentityMap where TId : notnull +/// The key type (int, long, Guid, string) +/// The value type +public sealed class IdentityMap : IIdentityMap where TKey : notnull { - private const bool _useSmallInt = true; + private const bool _useSmallInt = false; - // Small int optimization (TId = int only, 0-4095), 512 bytes (fits in L1 cache!) + // Small int optimization (TKey = int only, 0-4095), 512 bytes (fits in L1 cache!) private const int SmallBitmapSize = 64; private const int SmallSize = 4096; - // Small int optimization (TId = int only, 0-65535) + // Small int optimization (TKey = int only, 0-65535) //private const int SmallBitmapSize = 1024; //private const int SmallSize = SmallBitmapSize * 64; // Slot for hash table entries (generation needed for hash table validity) private struct HashSlot { - public object? Value; + public TValue Value; public int Next; // next slot index in chain (-1 = end) } - // Small int storage - SIMPLE: bitmap for "seen?" + direct object[] for values + // Small int storage - SIMPLE: bitmap for "seen?" + direct TValue[] for values // Bitmap protects values - no generation needed, no value array clearing on Reset! private ulong[]? _smallBitmap; // Cache-friendly "seen?" check (512 bytes, cleared on Reset) - private object?[]? _smallValues; // Direct value storage (NOT cleared on Reset - bitmap protects) // Hash table storage (for large ints and other types) private int[]? _buckets; // bucket index → first entry index private HashSlot[]? _entries; // hash table entries - private TId[]? _keys; // keys for equality check + private TKey[]? _keys; // keys for equality check private int _count; // number of entries in hash table private int _bucketsLength; // actual rented length (for modulo) private int _entriesLength; // actual capacity @@ -79,27 +79,28 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull private const int InitialHashCapacity = 16; // Type checks (JIT eliminates these at compile time) - private static readonly bool IsInt32 = typeof(TId) == typeof(int); - private static readonly bool IsInt64 = typeof(TId) == typeof(long); - private static readonly bool IsGuid = typeof(TId) == typeof(Guid); - private static readonly bool IsString = typeof(TId) == typeof(string); + private static readonly bool IsInt32 = typeof(TKey) == typeof(int); + private static readonly bool IsInt64 = typeof(TKey) == typeof(long); + private static readonly bool IsGuid = typeof(TKey) == typeof(Guid); + private static readonly bool IsString = typeof(TKey) == typeof(string); + private static readonly bool IsValueTypeValue = typeof(TValue).IsValueType; public IdentityMap() { } - + /// /// Tries to add a key to tracking (serialization). /// Returns true if first occurrence (key was added). /// Returns false if already seen. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryAddKey(TId key) + public bool TryAddKey(TKey key) { // Small int fast path if (_useSmallInt && IsInt32) { - var intKey = Unsafe.As(ref key); + var intKey = Unsafe.As(ref key); if ((uint)intKey < SmallSize) { return TryAddSmallInt(intKey); @@ -113,12 +114,11 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool TryAddSmallInt(int key) { - // Lazy init - ONLY bitmap for tracking (serializer doesn't need _smallValues) + // Lazy init - ONLY bitmap for tracking if (_smallBitmap == null) { _smallBitmap = ArrayPool.Shared.Rent(SmallBitmapSize); Array.Clear(_smallBitmap, 0, SmallBitmapSize); - // _smallValues allocated lazily in TryGetOrAddSmallIntValue when needed } var wordIdx = key >> 6; @@ -133,17 +133,17 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool KeyEquals(TId a, TId b) + private static bool KeyEquals(TKey a, TKey b) { - // JIT eliminates these branches at compile time for each TId instantiation + // JIT eliminates these branches at compile time for each TKey instantiation if (IsInt32) { - return Unsafe.As(ref a) == Unsafe.As(ref b); + return Unsafe.As(ref a) == Unsafe.As(ref b); } if (IsString) { - var strA = Unsafe.As(ref a); - var strB = Unsafe.As(ref b); + var strA = Unsafe.As(ref a); + var strB = Unsafe.As(ref b); // Fast path: reference equality (interned strings) //if (ReferenceEquals(strA, strB)) return true; @@ -153,18 +153,18 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull } if (IsInt64) { - return Unsafe.As(ref a) == Unsafe.As(ref b); + return Unsafe.As(ref a) == Unsafe.As(ref b); } if (IsGuid) { - return Unsafe.As(ref a) == Unsafe.As(ref b); + return Unsafe.As(ref a) == Unsafe.As(ref b); } // Fallback for other types - return EqualityComparer.Default.Equals(a, b); + return EqualityComparer.Default.Equals(a, b); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryAddHash(TId key, out int slotIndex) + private bool TryAddHash(TKey key, out int slotIndex) { var hash = GetHashCode(key); @@ -179,8 +179,8 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull // Search chain for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next) { - //if (EqualityComparer.Default.Equals(_keys![i], key)) - if (KeyEquals(_keys![i], key)) // Direct comparison, no virtual call + //if (EqualityComparer.Default.Equals(_keys![i], key)) + if (KeyEquals(_keys![i], key)) // Direct comparison, no virtual call { slotIndex = i; return false; // already seen @@ -203,13 +203,13 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetHashCode(TId key) + private static int GetHashCode(TKey key) { // Specialized hash for known types - JIT eliminates branches - if (IsInt32) return Unsafe.As(ref key); - if (IsInt64) return Unsafe.As(ref key).GetHashCode(); - if (IsGuid) return Unsafe.As(ref key).GetHashCode(); - if (IsString) return string.GetHashCode(Unsafe.As(ref key), StringComparison.Ordinal); + if (IsInt32) return Unsafe.As(ref key); + if (IsInt64) return Unsafe.As(ref key).GetHashCode(); + if (IsGuid) return Unsafe.As(ref key).GetHashCode(); + if (IsString) return string.GetHashCode(Unsafe.As(ref key), StringComparison.Ordinal); return key.GetHashCode(); } @@ -218,16 +218,16 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull { // Use remembered capacity if larger (from previous serialization) var actualCapacity = Math.Max(capacity, _bucketsLength); - + _buckets = ArrayPool.Shared.Rent(actualCapacity); - _bucketsLength = actualCapacity; - Array.Fill(_buckets, -1, 0, actualCapacity); + _bucketsLength = _buckets.Length; + Array.Fill(_buckets, -1, 0, _bucketsLength); _entries = ArrayPool.Shared.Rent(actualCapacity); - _entriesLength = actualCapacity; + _entriesLength = _entries.Length; //Array.Clear(_entries, 0, _entriesLength); - _keys = ArrayPool.Shared.Rent(actualCapacity); + _keys = ArrayPool.Shared.Rent(actualCapacity); //Array.Clear(_keys, 0, actualCapacity); _count = 0; @@ -239,9 +239,10 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull // Rent new arrays var newBuckets = ArrayPool.Shared.Rent(newCapacity); - Array.Fill(newBuckets, -1, 0, newCapacity); // MUST fill with -1 + var newBucketsLength = newBuckets.Length; + Array.Fill(newBuckets, -1, 0, newBucketsLength); var newEntries = ArrayPool.Shared.Rent(newCapacity); - var newKeys = ArrayPool.Shared.Rent(newCapacity); + var newKeys = ArrayPool.Shared.Rent(newCapacity); // Copy entries (no clear needed) Array.Copy(_entries!, newEntries, _count); @@ -250,7 +251,7 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull // Rebuild bucket chains for (var i = 0; i < _count; i++) { - var bucketIdx = (GetHashCode(newKeys[i]) & 0x7FFFFFFF) % newCapacity; + var bucketIdx = (GetHashCode(newKeys[i]) & 0x7FFFFFFF) % newBucketsLength; newEntries[i].Next = newBuckets[bucketIdx]; newBuckets[bucketIdx] = i; } @@ -258,22 +259,22 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull // Return old arrays to pool ArrayPool.Shared.Return(_buckets!); ArrayPool.Shared.Return(_entries!); - ArrayPool.Shared.Return(_keys!); + ArrayPool.Shared.Return(_keys!); _buckets = newBuckets; - _bucketsLength = newCapacity; + _bucketsLength = newBucketsLength; _entries = newEntries; - _entriesLength = newCapacity; + _entriesLength = newEntries.Length; _keys = newKeys; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasKey(TId key) + public bool HasKey(TKey key) { // Small int fast path - bitmap check is cache-friendly (512 bytes) if (_useSmallInt && IsInt32) { - var intKey = Unsafe.As(ref key); + var intKey = Unsafe.As(ref key); if ((uint)intKey < SmallSize && _smallBitmap != null) { var wordIdx = intKey >> 6; @@ -298,87 +299,28 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull /// /// Checks if key exists and returns existing value, or adds new key (deserialization). - /// Returns true if first occurrence (key was added, out = null). + /// Returns true if first occurrence (key was added, out = default). /// Returns false if already seen (out = existing value). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetOrAddKey(TId key, out object? existing) + public bool TryGetOrAddKey(TKey key, out TValue? existing) { - // NOTE: SmallInt path DISABLED for deserializer. - // The 512KB _smallValues sparse array causes cache misses. - // Hash table has better cache locality. - - //// Small int fast path - //if (_useSmallInt && IsInt32) - //{ - // var intKey = Unsafe.As(ref key); - // if ((uint)intKey < SmallSize) - // { - // return TryGetOrAddSmallInt(intKey, out existing); - // } - //} - - // Hash table path if (!TryAddHash(key, out var slotIndex)) { existing = _entries![slotIndex].Value; return false; } - existing = null; - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryGetOrAddSmallInt(int key, out object? existing) - { - // Lazy init bitmap only - NOT _smallValues! - // This method is for tracking (key seen?), not value storage. - if (_smallBitmap == null) - { - _smallBitmap = ArrayPool.Shared.Rent(SmallBitmapSize); - Array.Clear(_smallBitmap, 0, SmallBitmapSize); - } - - var wordIdx = key >> 6; - var bit = 1UL << (key & 63); - - ref var word = ref _smallBitmap[wordIdx]; - if ((word & bit) != 0) - { - // Bitmap says seen - return stored value if any - existing = _smallValues?[key]; - return false; // Already seen - } - - // First occurrence - mark as seen (don't touch _smallValues!) - word |= bit; - existing = null; + existing = default; return true; } /// - /// Gets existing value or adds new value for key (deserialization with object storage). + /// Gets existing value or adds new value for key (deserialization with value storage). /// Returns the existing value if key was seen before, or stores and returns newValue. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object TryGetOrAddValue(TId key, object newValue) + public TValue TryGetOrAddValue(TKey key, TValue newValue) { - // NOTE: SmallInt path DISABLED for deserializer value storage. - // The 512KB _smallValues sparse array causes cache misses. - // Hash table has better cache locality (only stores actual entries). - // Bitmap is only useful for tracking (serializer) - not for value storage. - - //// Small int fast path - //if (_useSmallInt && IsInt32) - //{ - // var intKey = Unsafe.As(ref key); - // if ((uint)intKey < SmallSize) - // { - // return TryGetOrAddSmallIntValue(intKey, newValue); - // } - //} - - // Hash table path if (!TryAddHash(key, out var slotIndex)) { var existing = _entries![slotIndex].Value; @@ -388,71 +330,16 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull return newValue; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private object TryGetOrAddSmallIntValue(int key, object newValue) - { - // Lazy init bitmap - if (_smallBitmap == null) - { - _smallBitmap = ArrayPool.Shared.Rent(SmallBitmapSize); - Array.Clear(_smallBitmap, 0, SmallBitmapSize); - } - - // Lazy init values array (only when needed for deserialization) - if (_smallValues == null) - { - _smallValues = ArrayPool.Shared.Rent(SmallSize); - Array.Clear(_smallValues, 0, SmallSize); - } - - var wordIdx = key >> 6; - var bit = 1UL << (key & 63); - - ref var word = ref _smallBitmap[wordIdx]; - if ((word & bit) != 0) - { - // Bitmap says seen - check if value exists - var existing = _smallValues![key]; - if (existing != null) - return existing; - } - - // First occurrence or no value yet - store new value - word |= bit; - _smallValues![key] = newValue; - return newValue; - } - /// - /// Tries to get the value for a key (ObjectRef lookup). + /// Tries to get the value for a key. /// Returns true if found, false if not. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetValue(TId key, out object? value) + public bool TryGetValue(TKey key, out TValue? value) { - //// Small int fast path - bitmap check first (cache-friendly, 512 bytes) - //if (_useSmallInt && IsInt32) - //{ - // var intKey = Unsafe.As(ref key); - // if ((uint)intKey < SmallSize && _smallBitmap != null) - // { - // var wordIdx = intKey >> 6; - // var bit = 1UL << (intKey & 63); - // if ((_smallBitmap[wordIdx] & bit) != 0) - // { - // // Bitmap says seen - return value if exists - // value = _smallValues?[intKey]; - // return value != null; - // } - // value = null; - // return false; - // } - //} - - // Hash table path if (_buckets == null) { - value = null; + value = default; return false; } @@ -467,10 +354,20 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull return true; } } - value = null; + value = default; return false; } + /// + /// Returns a reference to the value at the given slot index. + /// Use with slotIndex from TryAddHash for in-place value modification. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref TValue GetValueRef(int slotIndex) + { + return ref _entries![slotIndex].Value; + } + /// /// Resets the identity map for reuse. /// Small arrays (≤ InitialHashCapacity*5): keep and clear (faster than pool round-trip). @@ -479,29 +376,6 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull /// If true, pre-rent arrays at next capacity (useful for async Clear to shift work from hot path) public void Reset(bool preRentBuckets = false) { - if (_smallValues != null) - { - //Array.Clear(_smallValues, 0, SmallSize); - - // Végigmegyünk a bitmap-en és csak a set bit-ekhez tartozó értékeket töröljük - for (var wordIdx = 0; wordIdx < SmallBitmapSize; wordIdx++) - { - var word = _smallBitmap[wordIdx]; - if (word == 0) continue; - - var baseIdx = wordIdx << 6; - while (word != 0) - { - var bitPos = BitOperations.TrailingZeroCount(word); - _smallValues[baseIdx + bitPos] = null; - word &= word - 1; // Clear lowest set bit - } - } - - //ArrayPool.Shared.Return(_smallValues, clearArray: true); // Clear to release object refs - //_smallValues = null; - } - if (_smallBitmap != null) { Array.Clear(_smallBitmap, 0, SmallBitmapSize); @@ -524,7 +398,7 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull _count = 0; return; } - + // Large arrays: return to pool, remember half capacity var nextCapacity = Math.Max(_bucketsLength / 2, InitialHashCapacity * 5); @@ -538,20 +412,20 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull ArrayPool.Shared.Return(_buckets); ArrayPool.Shared.Return(_entries!,false); - ArrayPool.Shared.Return(_keys!, false); - + ArrayPool.Shared.Return(_keys!, false); + if (preRentBuckets) { // Pre-rent arrays now (async background) so Pool.Get() is faster _buckets = ArrayPool.Shared.Rent(nextCapacity); - _bucketsLength = nextCapacity; - Array.Fill(_buckets, -1, 0, nextCapacity); - + _bucketsLength = _buckets.Length; + Array.Fill(_buckets, -1, 0, _bucketsLength); + _entries = ArrayPool.Shared.Rent(nextCapacity); - _entriesLength = nextCapacity; + _entriesLength = _entries.Length; //Array.Clear(_entries, 0, _entriesLength); - - _keys = ArrayPool.Shared.Rent(nextCapacity); + + _keys = ArrayPool.Shared.Rent(nextCapacity); //Array.Clear(_keys, 0, nextCapacity); } else @@ -568,108 +442,3 @@ public sealed class IdentityMap : IIdentityMap where TId : notnull } #endregion - - -//#region IId Reference Tracking - -///// -///// Specifies the accessor type for IId.Id property to enable typed getter dispatch without boxing. -///// -//public enum IdAccessorType : byte -//{ -// None = 0, -// /// Id is int (most common). -// Int32 = 1, -// /// Id is long. -// Int64 = 2, -// /// Id is Guid. -// Guid = 3, -//} - -///// -///// Interface for identity maps used in serialization tracking. -///// Enables type-safe Reset() without knowing the generic type parameter. -///// -//public interface IIdentityMap -//{ -// /// -// /// Resets the identity map for reuse between serializations. -// /// -// void Reset(); -//} - -///// -///// Generic identity map for tracking IId values during serialization/deserialization. -///// Uses Dictionary internally for unified tracking + object storage. -///// -///// The ID type (int, long, Guid) -//public sealed class IdentityMap : IIdentityMap where TId : notnull -//{ -// private readonly Dictionary _tracked; - -// public IdentityMap() -// { -// _tracked = new Dictionary(EqualityComparer.Default); -// } - -// /// -// /// Tries to add a key to tracking (serialization). -// /// Returns true if first occurrence (key was added). -// /// Returns false if already seen. -// /// -// [MethodImpl(MethodImplOptions.AggressiveInlining)] -// public bool TryAddKey(TId key) -// { -// return _tracked.TryAdd(key, null); -// } - -// [MethodImpl(MethodImplOptions.AggressiveInlining)] -// public bool HasKey(TId key) => _tracked.ContainsKey(key); - - -// /// -// /// Checks if key exists and returns existing value, or adds new key (deserialization). -// /// Returns true if first occurrence (key was added, out = null). -// /// Returns false if already seen (out = existing value). -// /// -// [MethodImpl(MethodImplOptions.AggressiveInlining)] -// public bool TryGetOrAddKey(TId key, out object? existing) -// { -// ref var slot = ref CollectionsMarshal.GetValueRefOrAddDefault(_tracked, key, out var exists); -// existing = exists ? slot : null; -// return !exists; -// } - -// /// -// /// Gets existing value or adds new value for key (deserialization with object storage). -// /// Returns the existing value if key was seen before, or stores and returns newValue. -// /// -// [MethodImpl(MethodImplOptions.AggressiveInlining)] -// public object TryGetOrAddValue(TId key, object newValue) -// { -// ref var slot = ref CollectionsMarshal.GetValueRefOrAddDefault(_tracked, key, out var exists); -// if (exists && slot != null) return slot; -// slot = newValue; -// return newValue; -// } - -// /// -// /// Tries to get the value for a key (ObjectRef lookup). -// /// Returns true if found, false if not. -// /// -// [MethodImpl(MethodImplOptions.AggressiveInlining)] -// public bool TryGetValue(TId key, out object? value) -// { -// return _tracked.TryGetValue(key, out value); -// } - -// /// -// /// Resets the identity map for reuse. -// /// -// public void Reset() -// { -// _tracked.Clear(); -// } -//} - -//#endregion \ No newline at end of file diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index 04b714b..65153f9 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -40,19 +40,18 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// /// Typed IdentityMap for Int32 IDs. Direct access, no type check. /// - internal IdentityMap? IdentityMapInt32; + internal IdentityMap? IdentityMapInt32; /// /// Typed IdentityMap for Int64 IDs. Direct access, no type check. /// - internal IdentityMap? IdentityMapInt64; + internal IdentityMap? IdentityMapInt64; /// /// Typed IdentityMap for Guid IDs. Direct access, no type check. /// - internal IdentityMap? IdentityMapGuid; - - + internal IdentityMap? IdentityMapGuid; + #endregion /// @@ -152,7 +151,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat { if (id == 0) return newObj; // Default Id - no tracking - var map = IdentityMapInt32 ??= new IdentityMap(); + var map = IdentityMapInt32 ??= new IdentityMap(); return map.TryGetOrAddValue(id, newObj); } @@ -166,7 +165,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat var id = RefIdGetterInt32!(instance); if (id == 0) return; - var map = IdentityMapInt32 ??= new IdentityMap(); + var map = IdentityMapInt32 ??= new IdentityMap(); map.TryGetOrAddValue(id, instance); } @@ -189,7 +188,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat { if (id == 0) return newObj; - var map = IdentityMapInt64 ??= new IdentityMap(); + var map = IdentityMapInt64 ??= new IdentityMap(); return map.TryGetOrAddValue(id, newObj); } @@ -199,7 +198,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat var id = RefIdGetterInt64!(instance); if (id == 0) return; - var map = IdentityMapInt64 ??= new IdentityMap(); + var map = IdentityMapInt64 ??= new IdentityMap(); map.TryGetOrAddValue(id, instance); } @@ -222,7 +221,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat { if (id == Guid.Empty) return newObj; - var map = IdentityMapGuid ??= new IdentityMap(); + var map = IdentityMapGuid ??= new IdentityMap(); return map.TryGetOrAddValue(id, newObj); } @@ -232,7 +231,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat var id = RefIdGetterGuid!(instance); if (id == Guid.Empty) return; - var map = IdentityMapGuid ??= new IdentityMap(); + var map = IdentityMapGuid ??= new IdentityMap(); map.TryGetOrAddValue(id, instance); } @@ -245,25 +244,25 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// SLOW PATH - use typed methods (TryGetValueInt32, etc.) instead! /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IdentityMap GetOrCreateIdentityMap() where TId : notnull + public IdentityMap GetOrCreateIdentityMap() where TId : notnull { // Route to typed fields based on TId if (typeof(TId) == typeof(int)) { - var map = IdentityMapInt32 ??= new IdentityMap(); - return Unsafe.As, IdentityMap>(ref map); + var map = IdentityMapInt32 ??= new IdentityMap(); + return Unsafe.As, IdentityMap>(ref map); } if (typeof(TId) == typeof(long)) { - var map = IdentityMapInt64 ??= new IdentityMap(); - return Unsafe.As, IdentityMap>(ref map); + var map = IdentityMapInt64 ??= new IdentityMap(); + return Unsafe.As, IdentityMap>(ref map); } if (typeof(TId) == typeof(Guid)) { - var map = IdentityMapGuid ??= new IdentityMap(); - return Unsafe.As, IdentityMap>(ref map); + var map = IdentityMapGuid ??= new IdentityMap(); + return Unsafe.As, IdentityMap>(ref map); } - + throw new NotSupportedException($"Id type {typeof(TId)} is not supported"); }