From 6dbe4d76c1b295317f12d8b37bdbf695da80f530 Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 20 Jan 2026 07:23:02 +0100 Subject: [PATCH] Refactor reference tracking to use per-type identity maps Replaces flat object reference dictionary with per-type identity maps in deserializer, improving type safety and efficiency for IId types. TypeMetadataWrapper now uses cached typed delegates for reference ID access. Centralizes complex type detection and exposes IsComplexType and TypedIdGetter in metadata. Updates all registration and lookup logic to use wrappers, removes obsolete metadata cache, and ensures thread-safe, type-aware reference handling throughout. Comments out legacy code for easier review and rollback. --- AyCode.Core/Serializers/AcSerializerCommon.cs | 6 +- .../Serializers/AcSerializerContextBase.cs | 49 ++++++- ...serializer.BinaryDeserializationContext.cs | 62 ++++----- .../AcBinaryDeserializer.CrossType.cs | 25 ++-- .../Binaries/AcBinaryDeserializer.Populate.cs | 33 +++-- .../Binaries/AcBinaryDeserializer.cs | 120 ++++++++++-------- AyCode.Core/Serializers/TypeMetadataBase.cs | 39 ++++-- .../Serializers/TypeMetadataWrapper.cs | 37 ++---- 8 files changed, 224 insertions(+), 147 deletions(-) diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs index 1e50ad2..2351b6a 100644 --- a/AyCode.Core/Serializers/AcSerializerCommon.cs +++ b/AyCode.Core/Serializers/AcSerializerCommon.cs @@ -643,7 +643,11 @@ public static class AcSerializerCommon { 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). @@ -669,7 +673,7 @@ public static class AcSerializerCommon slot = newValue; return newValue; } - + /// /// Tries to get the value for a key (ObjectRef lookup). /// Returns true if found, false if not. diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs index f84ea3f..b464d9f 100644 --- a/AyCode.Core/Serializers/AcSerializerContextBase.cs +++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs @@ -53,7 +53,7 @@ public abstract class AcSerializerContextBase where TMetadata : TypeM { // Get metadata from global cache (thread-safe) var metadata = GlobalMetadataCache.GetOrAdd(type, MetadataFactory); - + // Create wrapper with metadata + tracking state (per-context) var wrapper = new TypeMetadataWrapper(metadata); _wrappers[type] = wrapper; @@ -64,6 +64,45 @@ public abstract class AcSerializerContextBase where TMetadata : TypeM #region Tracking API - int + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(TypeMetadataWrapper wrapper, int refId, out object? instance) + { + var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap; + if (map == null) + { + map = new AcSerializerCommon.IdentityMap(); + wrapper.IdentityMap = map; + } + + return map.TryGetValue(refId, out instance); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(TypeMetadataWrapper wrapper, long refId, out object? instance) + { + var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap; + if (map == null) + { + map = new AcSerializerCommon.IdentityMap(); + wrapper.IdentityMap = map; + } + + return map.TryGetValue(refId, out instance); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(TypeMetadataWrapper wrapper, Guid refId, out object? instance) + { + var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap; + if (map == null) + { + map = new AcSerializerCommon.IdentityMap(); + wrapper.IdentityMap = map; + } + + return map.TryGetValue(refId, out instance); + } + /// /// Tries to track an object with int RefId. /// Use when wrapper.Metadata.IdAccessorType == Int32. @@ -78,10 +117,10 @@ public abstract class AcSerializerContextBase where TMetadata : TypeM refId = getter(obj); // BitArray fast path for small positive IDs - if (refId >= 0 && refId < MaxSmallId) - { - return TryTrackSmallId(wrapper, refId); - } + //if (refId >= 0 && refId < MaxSmallId) + //{ + // return TryTrackSmallId(wrapper, refId); + //} // IdentityMap for large/negative IDs var map = wrapper.IdentityMap as AcSerializerCommon.IdentityMap; diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index e47603c..3c09da7 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -19,7 +19,7 @@ public static partial class AcBinaryDeserializer private int _position; private List? _internedStrings; private List? _propertyNames; - private Dictionary? _objectReferences; + //private Dictionary? _objectReferences; private Dictionary? _stringCache; private readonly byte _minStringInternLength; private readonly bool _useStringCaching; @@ -66,7 +66,7 @@ public static partial class AcBinaryDeserializer _position = 0; _internedStrings = null; _propertyNames = null; - _objectReferences = null; + //_objectReferences = null; _stringCache = null; HasMetadata = false; HasReferenceHandling = false; @@ -522,33 +522,35 @@ public static partial class AcBinaryDeserializer } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RegisterObject(int refId, object instance) + public void RegisterObject(TypeMetadataWrapper wrapper, int refId, object instance) { - if (refId <= 0) - { - return; - } + if (refId == 0) throw new Exception("refId == 0"); + ContextClass.TryGetOrStoreInt32(wrapper, instance, refId); + //if (refId <= 0) + //{ + // return; + //} - _objectReferences ??= new Dictionary(16); - _objectReferences[refId] = instance; + //_objectReferences ??= new Dictionary(16); + //_objectReferences[refId] = instance; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object? GetReferencedObject(int refId) - { - if (refId <= 0) - { - return null; - } + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + //public object? GetReferencedObject(TypeMetadataWrapper wrapper, int refId) + //{ + // //if (refId <= 0) + // //{ + // // return null; + // //} - if (_objectReferences == null || !_objectReferences.TryGetValue(refId, out var value)) - { - throw new AcBinaryDeserializationException($"Unknown object reference id '{refId}'.", _position); - } + // //if (_objectReferences == null || !_objectReferences.TryGetValue(refId, out var value)) + // //{ + // // throw new AcBinaryDeserializationException($"Unknown object reference id '{refId}'.", _position); + // //} - return value; - } + // //return value; + //} private void EnsureAvailable(int length) { @@ -563,20 +565,20 @@ public static partial class AcBinaryDeserializer var byteLength = (int)ReadVarUInt(); return ReadStringUtf8(byteLength); } - + #region IId Reference Cache - Delegates to ContextClass - + /// /// After PopulateObject, checks if we should reuse an existing IId object. /// Delegates to ContextClass which uses AcSerializerContextBase infrastructure. /// Returns the object to use (either the new one or an existing cached one). /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object GetOrRegisterIIdObject(object newObj, BinaryDeserializeTypeMetadata metadata) - { - return ContextClass.GetOrRegisterIIdObject(newObj, metadata); - } - + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + //public object GetOrRegisterIIdObject(object newObj, TypeMetadataWrapper wrapper) + //{ + // return ContextClass.GetOrRegisterIIdObject(newObj, wrapper); + //} + #endregion } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs index 3069eec..4af38f9 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs @@ -215,23 +215,20 @@ public static partial class AcBinaryDeserializer /// private static object? ReadObjectWithMapping(ref BinaryDeserializationContext context, Type destType, int[] indexMapping, int depth) { - var metadata = GetTypeMetadata(destType); + var wrapper = context.ContextClass.GetWrapper(destType); + var metadata = wrapper.Metadata; // Handle reference ID if present if (context.HasReferenceHandling) { var refId = context.ReadVarInt(); - if (refId < 0) - { - // Object reference - get existing object - return context.GetReferencedObject(-refId); - } - + if (context.ContextClass.TryGetValue(wrapper, refId, out var instance)) return instance; + // New object with ID - will be registered below - var instance = CreateInstance(destType, metadata); + instance = CreateInstance(destType, metadata); if (instance != null && refId > 0) { - context.RegisterObject(refId, instance); + context.RegisterObject(wrapper, refId, instance); } PopulateObjectWithMapping(ref context, instance!, destType, indexMapping, depth); @@ -258,7 +255,8 @@ public static partial class AcBinaryDeserializer int[] indexMapping, int depth) { - var metadata = GetTypeMetadata(destType); + var wrapper = context.ContextClass.GetWrapper(destType); + var metadata = wrapper.Metadata; var propertyCount = (int)context.ReadVarUInt(); var nextDepth = depth + 1; @@ -286,7 +284,7 @@ public static partial class AcBinaryDeserializer } // Reuse common populate logic - PopulatePropertyValue(ref context, target, propInfo, nextDepth, sourcePropIndex, destPropIndex, i, propertyCount, depth); + PopulatePropertyValue(ref context, target, propInfo, wrapper, nextDepth, sourcePropIndex, destPropIndex, i, propertyCount, depth); } } @@ -311,6 +309,7 @@ public static partial class AcBinaryDeserializer ref BinaryDeserializationContext context, object target, BinaryPropertySetterInfo propInfo, + TypeMetadataWrapper wrapper, int nextDepth, int sourcePropIndex, int destPropIndex, @@ -342,11 +341,11 @@ public static partial class AcBinaryDeserializer var refId = context.ReadVarInt(); if (refId > 0) { - context.RegisterObject(refId, existingObj); + context.RegisterObject(wrapper, refId, existingObj); } } - PopulateObjectCore(ref context, existingObj, GetTypeMetadata(propInfo.PropertyType), nextDepth, skipDefaultWrite: false); + PopulateObjectCore(ref context, existingObj, wrapper.Metadata, nextDepth, skipDefaultWrite: false); return; } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index 3846ef5..f76f7f3 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -12,13 +12,18 @@ public static partial class AcBinaryDeserializer { #region Populate Object Methods + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type) + => null;//MetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t)); + /// /// Populate object with automatic mode detection from context. /// Uses IsMergeMode to determine merge behavior for IId collections. /// private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth) { - var metadata = GetTypeMetadata(targetType); + var wrapper = context.ContextClass.GetWrapper(targetType); + var metadata = wrapper.Metadata; // Handle ref ID if present if (context.HasReferenceHandling) @@ -26,7 +31,7 @@ public static partial class AcBinaryDeserializer var refId = context.ReadVarInt(); if (refId > 0) { - context.RegisterObject(refId, target); + context.RegisterObject(wrapper, refId, target); } } @@ -105,6 +110,8 @@ public static partial class AcBinaryDeserializer var existingObj = propInfo.GetValue(target); if (existingObj != null) { + var wrapper = context.ContextClass.GetWrapper(propInfo.PropertyType); + context.ReadByte(); // consume Object marker // Handle ref ID if present @@ -113,12 +120,12 @@ public static partial class AcBinaryDeserializer var refId = context.ReadVarInt(); if (refId > 0) { - context.RegisterObject(refId, existingObj); + context.RegisterObject(wrapper, refId, existingObj); } } // Recursively populate - existing object, don't skip defaults - PopulateObjectCore(ref context, existingObj, GetTypeMetadata(propInfo.PropertyType), nextDepth, skipDefaultWrite: false); + PopulateObjectCore(ref context, existingObj, wrapper.Metadata, nextDepth, skipDefaultWrite: false); continue; } } @@ -207,8 +214,10 @@ public static partial class AcBinaryDeserializer try { + var wrapper = context.ContextClass.GetWrapper(elementType); var existingCount = existingList.Count; - var elementMetadata = IsComplexType(elementType) ? GetTypeMetadata(elementType) : null; + + var elementMetadata = wrapper.Metadata.IsComplexType ? wrapper.Metadata : null; for (int i = 0; i < count; i++) { @@ -228,7 +237,7 @@ public static partial class AcBinaryDeserializer var refId = context.ReadVarInt(); if (refId > 0) { - context.RegisterObject(refId, existingItem); + context.RegisterObject(wrapper, refId, existingItem); } } @@ -302,7 +311,8 @@ public static partial class AcBinaryDeserializer var arrayCount = (int)context.ReadVarUInt(); var nextDepth = depth + 1; - var elementMetadata = GetTypeMetadata(elementType); + var wrapper = context.ContextClass.GetWrapper(elementType); + var elementMetadata = wrapper.Metadata; // Track which IDs we see in source (for orphan removal) HashSet? sourceIds = context.RemoveOrphanedItems && existingById != null @@ -329,7 +339,7 @@ public static partial class AcBinaryDeserializer { var refId = context.ReadVarInt(); if (refId > 0) - context.RegisterObject(refId, newItem); + context.RegisterObject(wrapper, refId, newItem); } // Deserialize mode: new item just created, skip writing defaults @@ -383,9 +393,10 @@ public static partial class AcBinaryDeserializer ref BinaryDeserializationContext context, IList existingList, Type elementType, - BinaryDeserializeTypeMetadata elementMetadata, + TypeMetadataWrapper wrapper, int depth) { + var elementMetadata = wrapper.Metadata; var idGetter = elementMetadata.IdGetter!; var idType = elementMetadata.IdType!; @@ -440,7 +451,7 @@ public static partial class AcBinaryDeserializer { var refId = context.ReadVarInt(); if (refId > 0) - context.RegisterObject(refId, newItem); + context.RegisterObject(wrapper, refId, newItem); } // Deserialize mode: new item just created, skip writing defaults @@ -546,7 +557,7 @@ public static partial class AcBinaryDeserializer /// Determines if a type is a complex type (not primitive, string, or simple value type). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsComplexType(Type type) + private static bool IsComplexType5(Type type) { if (type.IsPrimitive) return false; if (ReferenceEquals(type, StringType)) return false; diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 74149c6..95b56d5 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -325,15 +325,18 @@ public static partial class AcBinaryDeserializer context.ReadByte(); // For top-level list merge, check if it's an IId collection var elementType = GetCollectionElementType(targetType); - if (elementType != null && IsComplexType(elementType)) + if (elementType != null) { - var elementMetadata = GetTypeMetadata(elementType); - if (elementMetadata.IsIId && elementMetadata.IdGetter != null) + var wrapper = context.ContextClass.GetWrapper(elementType); + var elementMetadata = wrapper.Metadata; + + if (elementMetadata.IsComplexType && elementMetadata.IsIId && elementMetadata.IdGetter != null) { - MergeIIdCollectionWithMetadata(ref context, targetList, elementType, elementMetadata, 0); + MergeIIdCollectionWithMetadata(ref context, targetList, elementType, wrapper, 0); return; } } + // Non-IId collection, just populate PopulateList(ref context, targetList, targetType, 0); } @@ -909,11 +912,11 @@ public static partial class AcBinaryDeserializer /// private static object? ReadObjectRef(ref BinaryDeserializationContext context, Type targetType, int depth) { - var metadata = GetTypeMetadata(targetType); + var wrapper = context.ContextClass.GetWrapper(targetType); + var metadata = wrapper.Metadata; - if (metadata.IsIId) + if (metadata.IdAccessorType != AcSerializerCommon.IdAccessorType.None) { - var wrapper = context.ContextClass.GetWrapper(targetType); var map = wrapper.IdentityMap; // Read ID based on type and lookup in IdentityMap @@ -922,12 +925,11 @@ public static partial class AcBinaryDeserializer AcSerializerCommon.IdAccessorType.Int32 => ReadObjectRefInt32(ref context, map), AcSerializerCommon.IdAccessorType.Int64 => ReadObjectRefInt64(ref context, map), AcSerializerCommon.IdAccessorType.Guid => ReadObjectRefGuid(ref context, map), - _ => context.GetReferencedObject(context.ReadVarInt()) + _ => throw new Exception("metadata.IdAccessorType not valid") }; } - // Non-IId: sequential int refId - return context.GetReferencedObject(context.ReadVarInt()); + return null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -936,7 +938,7 @@ public static partial class AcBinaryDeserializer var id = context.ReadVarInt(); if (map is AcSerializerCommon.IdentityMap intMap && intMap.TryGetValue(id, out var obj)) return obj; - return context.GetReferencedObject(id); + return null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -959,36 +961,53 @@ public static partial class AcBinaryDeserializer private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth) { - var metadata = GetTypeMetadata(targetType); - - // Read reference ID based on IdAccessorType (different wire format for each type) - object? readId = null; - if (context.HasReferenceHandling) - { - readId = metadata.IdAccessorType switch - { - AcSerializerCommon.IdAccessorType.Int32 => context.ReadVarInt(), - AcSerializerCommon.IdAccessorType.Int64 => context.ReadVarLong(), - AcSerializerCommon.IdAccessorType.Guid => context.ReadGuidUnsafe(), - _ => throw new Exception($"metadata.IdAccessorType not valid: {metadata.IdAccessorType}") - }; - } - // Handle dictionary types if (IsDictionaryType(targetType, out var keyType, out var valueType)) { return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth); } + + var wrapper = context.ContextClass.GetWrapper(targetType); + var metadata = wrapper.Metadata; - // Create instance - var instance = CreateInstance(targetType, metadata); - if (instance == null) return null; + object? instance = null; - // Register reference for flat lookup (int refId only) - if (readId is int intRefId) + if (context.HasReferenceHandling && metadata.IdAccessorType != AcSerializerCommon.IdAccessorType.None) { - if (intRefId == 0) throw new Exception("intRefId == 0"); - context.RegisterObject(intRefId, instance); + switch (metadata.IdAccessorType) + { + case AcSerializerCommon.IdAccessorType.Int32: + var intId = context.ReadVarInt(); + if (context.ContextClass.TryGetValue(wrapper, intId, out instance)) return instance; + + instance = CreateInstance(targetType, metadata); + if (instance == null) return null; + context.ContextClass.TryGetOrStoreInt32(wrapper, instance, intId); + break; + case AcSerializerCommon.IdAccessorType.Int64: + var longId = context.ReadVarLong(); + if (context.ContextClass.TryGetValue(wrapper, longId, out instance)) return instance; + + instance = CreateInstance(targetType, metadata); + if (instance == null) return null; + context.ContextClass.TryGetOrStoreLong(wrapper, instance, longId); + break; + case AcSerializerCommon.IdAccessorType.Guid: + var guidId = context.ReadGuidUnsafe(); + if (context.ContextClass.TryGetValue(wrapper, guidId, out instance)) return instance; + + instance = CreateInstance(targetType, metadata); + if (instance == null) return null; + context.ContextClass.TryGetOrStoreGuid(wrapper, instance, guidId); + break; + default: + throw new Exception($"metadata.IdAccessorType not valid: {metadata.IdAccessorType}"); + } + } + else + { + instance = CreateInstance(targetType, metadata); + if (instance == null) return null; } // Deserialize mode: object just created, skip writing defaults (already at default) @@ -998,14 +1017,23 @@ public static partial class AcBinaryDeserializer // Note: For ChainMode, we need IdGetter OR typed getters (IdAccessorType != None) if (context.IsChainMode && metadata.IsIId && metadata.IdType != null) { - object? id = metadata.IdAccessorType switch + object? id; + switch (metadata.IdAccessorType) { - AcSerializerCommon.IdAccessorType.Int32 => metadata.GetIdInt32(instance), - AcSerializerCommon.IdAccessorType.Int64 => metadata.GetIdInt64(instance), - AcSerializerCommon.IdAccessorType.Guid => metadata.GetIdGuid(instance), - _ => null - }; - + case AcSerializerCommon.IdAccessorType.Int32: + id = metadata.GetIdInt32(instance); + break; + case AcSerializerCommon.IdAccessorType.Int64: + id = metadata.GetIdInt64(instance); + break; + case AcSerializerCommon.IdAccessorType.Guid: + id = metadata.GetIdGuid(instance); + break; + default: + id = null; + break; + } + if (id != null && !IsDefaultValue(id, metadata.IdType)) { // Check if we already have this object @@ -1020,11 +1048,6 @@ public static partial class AcBinaryDeserializer context.ChainTracker.TryRegisterIIdObject(instance); } } - // Normal IId cache for non-chain deserialization - else if (context.HasReferenceHandling && metadata.IsIId) - { - instance = context.GetOrRegisterIIdObject(instance, metadata); - } return instance; } @@ -1424,13 +1447,6 @@ public static partial class AcBinaryDeserializer #region Type Metadata - // Temporary: own cache until ref struct is removed - private static readonly ConcurrentDictionary MetadataCache = new(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type) - => MetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t)); - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata) { diff --git a/AyCode.Core/Serializers/TypeMetadataBase.cs b/AyCode.Core/Serializers/TypeMetadataBase.cs index 31b6ac1..a44d35b 100644 --- a/AyCode.Core/Serializers/TypeMetadataBase.cs +++ b/AyCode.Core/Serializers/TypeMetadataBase.cs @@ -102,8 +102,9 @@ public abstract class TypeMetadataBase /// /// Typed getter delegate for IId.Id property. /// Type depends on IdAccessorType (Func<object, int>, Func<object, long>, or Func<object, Guid>). + /// Cached here to avoid creating new delegates per wrapper. /// - private readonly Delegate? _typedIdGetter; + public Delegate? TypedIdGetter { get; } #region Scan Optimization Flags @@ -112,7 +113,9 @@ public abstract class TypeMetadataBase /// If false, ScanReferences can skip child property scanning entirely. /// public bool HasComplexProperties { get; protected set; } - + + public bool IsComplexType { get; init; } + /// /// True if this type or any of its properties could potentially be shared (multi-referenced). /// False for sealed value-like types with only primitives - these never need reference tracking. @@ -125,19 +128,19 @@ public abstract class TypeMetadataBase /// Gets the Id as int without boxing. Only valid when IdAccessorType == Int32. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetIdInt32(object obj) => ((Func)_typedIdGetter!)(obj); + public int GetIdInt32(object obj) => ((Func)TypedIdGetter!)(obj); /// /// Gets the Id as long without boxing. Only valid when IdAccessorType == Int64. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public long GetIdInt64(object obj) => ((Func)_typedIdGetter!)(obj); + public long GetIdInt64(object obj) => ((Func)TypedIdGetter!)(obj); /// /// Gets the Id as Guid without boxing. Only valid when IdAccessorType == Guid. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Guid GetIdGuid(object obj) => ((Func)_typedIdGetter!)(obj); + public Guid GetIdGuid(object obj) => ((Func)TypedIdGetter!)(obj); protected TypeMetadataBase(Type type, Func ignorePropertyFilter) { @@ -153,11 +156,14 @@ public abstract class TypeMetadataBase ReadableProperties = allReadable; WritableProperties = allReadable.Where(p => p.CanWrite).ToArray(); + IsComplexType = IsComplexType2(type); + // Cache IId info at construction time - no runtime reflection needed later! var idInfo = GetIdInfo(type); IsIId = idInfo.IsId; IdType = idInfo.IdType; + if (IsIId) { var idProp = type.GetProperty("Id"); @@ -167,17 +173,17 @@ public abstract class TypeMetadataBase if (ReferenceEquals(IdType, IntType)) { IdAccessorType = AcSerializerCommon.IdAccessorType.Int32; - _typedIdGetter = AcSerializerCommon.CreateTypedGetter(type, idProp); + TypedIdGetter = AcSerializerCommon.CreateTypedGetter(type, idProp); } else if (ReferenceEquals(IdType, LongType)) { IdAccessorType = AcSerializerCommon.IdAccessorType.Int64; - _typedIdGetter = AcSerializerCommon.CreateTypedGetter(type, idProp); + TypedIdGetter = AcSerializerCommon.CreateTypedGetter(type, idProp); } else if (ReferenceEquals(IdType, GuidType)) { IdAccessorType = AcSerializerCommon.IdAccessorType.Guid; - _typedIdGetter = AcSerializerCommon.CreateTypedGetter(type, idProp); + TypedIdGetter = AcSerializerCommon.CreateTypedGetter(type, idProp); } else { @@ -189,7 +195,7 @@ public abstract class TypeMetadataBase // Non-IId types: use RuntimeHelpers.GetHashCode (int) // RefIdGetter is created in TypeMetadataWrapper.CreateRefIdGetter() IdAccessorType = AcSerializerCommon.IdAccessorType.Int32; - // _typedIdGetter remains null - wrapper uses GetHashCode directly + // TypedIdGetter remains null - wrapper uses GetHashCode directly } } @@ -241,4 +247,19 @@ public abstract class TypeMetadataBase return allProperties; }); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsComplexType2(Type type) + { + if (type.IsPrimitive) return false; + if (ReferenceEquals(type, StringType)) return false; + if (type.IsEnum) return false; + if (ReferenceEquals(type, GuidType)) return false; + if (ReferenceEquals(type, DateTimeType)) return false; + if (ReferenceEquals(type, DecimalType)) return false; + if (ReferenceEquals(type, TimeSpanType)) return false; + if (ReferenceEquals(type, DateTimeOffsetType)) return false; + if (Nullable.GetUnderlyingType(type) != null) return false; + return true; + } } diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index 31d8e36..6e5b1ee 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -20,6 +20,8 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// /// Typed getter for reference ID. Runtime type is Func<object, int/long/Guid>. /// Use IdAccessorType to determine the actual type. + /// For IId types: uses metadata.TypedIdGetter (cached in metadata). + /// For non-IId types: uses RuntimeHelpers.GetHashCode. /// internal readonly Delegate RefIdGetter; @@ -36,39 +38,22 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat private const int BitArraySize = 1024; // 1024 * 64 = 65,536 IDs + /// + /// Static fallback delegate for non-IId types. + /// Shared across all wrappers to avoid allocation. + /// + private static readonly Func HashCodeGetter = RuntimeHelpers.GetHashCode; + /// /// Creates a new wrapper for the given metadata. - /// Initializes RefIdGetter based on IdAccessorType. + /// Uses metadata.TypedIdGetter for IId types (cached, no allocation). /// public TypeMetadataWrapper(TMetadata metadata) { Metadata = metadata; - // Create typed RefIdGetter based on IdAccessorType - RefIdGetter = CreateRefIdGetter(metadata); - } - - private static Delegate CreateRefIdGetter(TMetadata metadata) - { - if (metadata.IsIId && metadata.IdPropertyInfo != null) - { - // IId type - create typed getter from Id property - return metadata.IdAccessorType switch - { - AcSerializerCommon.IdAccessorType.Int32 => - AcSerializerCommon.CreateTypedGetter(metadata.MetadataType, metadata.IdPropertyInfo), - AcSerializerCommon.IdAccessorType.Int64 => - AcSerializerCommon.CreateTypedGetter(metadata.MetadataType, metadata.IdPropertyInfo), - AcSerializerCommon.IdAccessorType.Guid => - AcSerializerCommon.CreateTypedGetter(metadata.MetadataType, metadata.IdPropertyInfo), - _ => throw new NotSupportedException($"Unsupported IdAccessorType: {metadata.IdAccessorType}") - }; - } - else - { - // Non-IId type - use RuntimeHelpers.GetHashCode - return new Func(RuntimeHelpers.GetHashCode); - } + // Use cached delegate from metadata for IId types, static fallback for non-IId + RefIdGetter = metadata.TypedIdGetter ?? HashCodeGetter; } ///