diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs index cd61bd8..06d72c3 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs @@ -333,7 +333,18 @@ public static partial class AcBinaryDeserializer if (existingObj != null) { context.ReadByte(); // consume Object marker - PopulateObjectNested(ref context, existingObj, propInfo.PropertyType, nextDepth); + + // Handle ref ID if present + if (context.HasReferenceHandling) + { + var refId = context.ReadVarInt(); + if (refId > 0) + { + context.RegisterObject(refId, existingObj); + } + } + + PopulateObjectCore(ref context, existingObj, GetTypeMetadata(propInfo.PropertyType), nextDepth, skipDefaultWrite: false); return; } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index 22e3bed..ab61064 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -12,11 +12,15 @@ public static partial class AcBinaryDeserializer { #region Populate Object Methods + /// + /// 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); - // Skip ref ID if present + // Handle ref ID if present if (context.HasReferenceHandling) { var refId = context.ReadVarInt(); @@ -26,30 +30,32 @@ public static partial class AcBinaryDeserializer } } - PopulateObject(ref context, target, metadata, depth); + PopulateObjectCore(ref context, target, metadata, depth, skipDefaultWrite: false); } - private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth, bool skipDefaultWrite = false) + /// + /// Core populate logic shared by all populate paths. + /// + /// Deserialization context + /// Target object to populate + /// Type metadata + /// Current depth + /// + /// true = Deserialize mode - object just created, properties already at default, skip writing defaults + /// false = Populate mode - existing object, must overwrite properties with default values + /// + private static void PopulateObjectCore(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth, bool skipDefaultWrite) { - // NEW FORMAT: Fixed property order with SKIP markers - // - No property count (iterate all properties in metadata order) - // - No property indices (use sequential PropertyIndex = i) - // - PropertySkip marker indicates default/null value - // - // skipDefaultWrite: - // true = Deserialize mode - object just created, properties already at default, skip writing - // false = Populate mode - existing object, must overwrite properties with default values - var properties = metadata.PropertiesArray; var nextDepth = depth + 1; + var isMergeMode = context.IsMergeMode; for (int i = 0; i < properties.Length; i++) { var propInfo = properties[i]; - var peekCode = context.PeekByte(); - // OPTIMIZATION: Skip marker - property has default/null value + // Skip marker - property has default/null value if (peekCode == BinaryTypeCode.PropertySkip) { context.ReadByte(); // consume Skip marker @@ -60,11 +66,10 @@ public static partial class AcBinaryDeserializer { SetPropertyToDefault(target, propInfo); } - continue; } - // OPTIMIZATION: Skip null values early - no GetValue call needed! + // Null values - always set if (peekCode == BinaryTypeCode.Null) { context.ReadByte(); // consume Null marker @@ -72,37 +77,57 @@ public static partial class AcBinaryDeserializer continue; } - // Handle nested complex objects - reuse existing if available - // ONLY call GetValue if we actually have an Object coming in - if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType) - { - var existingObj = propInfo.GetValue(target); - if (existingObj != null) - { - context.ReadByte(); // consume Object marker - PopulateObjectNested(ref context, existingObj, propInfo.PropertyType, nextDepth); - continue; - } - } - - // Handle collections - reuse existing collection and populate items - // ONLY call GetValue if we actually have an Array coming in + // Handle collections if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection) { var existingCollection = propInfo.GetValue(target); if (existingCollection is IList existingList) { context.ReadByte(); // consume Array marker - PopulateListOptimized(ref context, existingList, propInfo, nextDepth); + + // Merge mode with IId collection: use merge logic + if (isMergeMode && propInfo.IsIIdCollection) + { + MergeIIdCollection(ref context, existingList, propInfo, nextDepth); + } + else + { + // Normal populate: replace collection contents + PopulateListOptimized(ref context, existingList, propInfo, nextDepth); + } continue; } } - // Default: read value and set (for primitives, strings, null cases) + // Handle nested complex objects - reuse existing if available + if (peekCode == BinaryTypeCode.Object && propInfo.IsComplexType) + { + var existingObj = propInfo.GetValue(target); + if (existingObj != null) + { + context.ReadByte(); // consume Object marker + + // Handle ref ID if present + if (context.HasReferenceHandling) + { + var refId = context.ReadVarInt(); + if (refId > 0) + { + context.RegisterObject(refId, existingObj); + } + } + + // Recursively populate - existing object, don't skip defaults + PopulateObjectCore(ref context, existingObj, GetTypeMetadata(propInfo.PropertyType), nextDepth, skipDefaultWrite: false); + continue; + } + } + + // Default: read value and set (for primitives, strings, new objects) var positionBeforeRead = context.Position; try { - // OPTIMIZATION: Use typed setters for primitives to avoid boxing + // Use typed setters for primitives to avoid boxing if (TryReadAndSetTypedValue(ref context, target, propInfo, peekCode)) continue; @@ -111,12 +136,9 @@ public static partial class AcBinaryDeserializer } catch (InvalidCastException ex) { - // Only get type info when needed for error message (cold path) var targetType = target.GetType(); - var targetTypeName = targetType.Name; - throw new AcBinaryDeserializationException( - $"Type mismatch for property '{propInfo.Name}' (index {i}) on '{targetTypeName}'. " + + $"Type mismatch for property '{propInfo.Name}' (index {i}) on '{targetType.Name}'. " + $"Expected type: '{propInfo.PropertyType.FullName}'. " + $"PeekCode before read: {peekCode} (0x{peekCode:X2}). " + $"Position before read: {positionBeforeRead}, current: {context.Position}. " + @@ -132,91 +154,12 @@ public static partial class AcBinaryDeserializer } /// - /// Populate nested object, reusing existing object and recursively updating properties. + /// Called from ReadObject for new instances - just calls PopulateObjectCore with skipDefaultWrite=true. /// - private static void PopulateObjectNested(ref BinaryDeserializationContext context, object target, Type targetType, int depth) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void PopulateObject(ref BinaryDeserializationContext context, object target, BinaryDeserializeTypeMetadata metadata, int depth, bool skipDefaultWrite) { - var metadata = GetTypeMetadata(targetType); - - // Handle ref ID if present - if (context.HasReferenceHandling) - { - var refId = context.ReadVarInt(); - if (refId > 0) - { - context.RegisterObject(refId, target); - } - } - - PopulateObject(ref context, target, metadata, depth); - } - - private static void PopulateObjectMerge(ref BinaryDeserializationContext context, object target, Type targetType, int depth, bool skipDefaultWrite = false) - { - var metadata = GetTypeMetadata(targetType); - - // Skip ref ID if present - if (context.HasReferenceHandling) - { - var refId = context.ReadVarInt(); - if (refId > 0) - { - context.RegisterObject(refId, target); - } - } - - // NEW FORMAT: Fixed property order with SKIP markers - var properties = metadata.PropertiesArray; - var nextDepth = depth + 1; - - for (int i = 0; i < properties.Length; i++) - { - var propInfo = properties[i]; - - var peekCode = context.PeekByte(); - - // OPTIMIZATION: Skip marker - property has default/null value - if (peekCode == BinaryTypeCode.PropertySkip) - { - context.ReadByte(); // consume Skip marker - - // Populate mode: overwrite with default (same as PopulateObject) - // Deserialize mode: skip write (new object already has defaults) - if (!skipDefaultWrite) - { - SetPropertyToDefault(target, propInfo); - } - - continue; - } - - // Handle IId collection merge - if (propInfo.IsIIdCollection && peekCode == BinaryTypeCode.Array) - { - var existingCollection = propInfo.GetValue(target); - if (existingCollection is IList existingList) - { - context.ReadByte(); // consume Array marker - MergeIIdCollection(ref context, existingList, propInfo, depth); - continue; - } - } - - // Handle nested object merge - if (peekCode == BinaryTypeCode.Object && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType)) - { - var existingObj = propInfo.GetValue(target); - if (existingObj != null) - { - context.ReadByte(); // consume Object marker - PopulateObjectMerge(ref context, existingObj, propInfo.PropertyType, nextDepth); - continue; - } - } - - var value = ReadValue(ref context, propInfo.PropertyType, nextDepth); - propInfo.SetValue(target, value); - } + PopulateObjectCore(ref context, target, metadata, depth, skipDefaultWrite); } #endregion @@ -237,7 +180,7 @@ public static partial class AcBinaryDeserializer var count = (int)context.ReadVarUInt(); var nextDepth = depth + 1; - // ChainMode: Use cached IId info from element type metadata (no runtime reflection!) + // ChainMode: Use cached IId info from element type metadata BinaryDeserializeTypeMetadata? elementMetadata = null; if (context.IsChainMode && IsComplexType(elementType)) { @@ -257,7 +200,6 @@ public static partial class AcBinaryDeserializer { if (context.ChainTracker!.TryGetObject(elementType, id, out var existingObj)) { - // Use existing object instead of new one targetList.Add(existingObj); continue; } @@ -273,31 +215,6 @@ public static partial class AcBinaryDeserializer } } - private static void PopulateListMerge(ref BinaryDeserializationContext context, IList targetList, Type listType, int depth) - { - var elementType = GetCollectionElementType(listType) ?? typeof(object); - - // Use cached metadata instead of runtime reflection - if (!IsComplexType(elementType)) - { - // Not a complex type, just replace - PopulateList(ref context, targetList, listType, depth); - return; - } - - var elementMetadata = GetTypeMetadata(elementType); - - if (!elementMetadata.IsIId || elementMetadata.IdGetter == null || elementMetadata.IdType == null) - { - // No IId, just replace - PopulateList(ref context, targetList, listType, depth); - return; - } - - // IId merge logic using cached metadata - MergeIIdCollectionWithMetadata(ref context, targetList, elementType, elementMetadata, depth); - } - /// /// Optimized list populate that reuses existing items when possible. /// @@ -338,7 +255,7 @@ public static partial class AcBinaryDeserializer } // Populate mode: existing item, must overwrite with defaults - PopulateObject(ref context, existingItem, elementMetadata, nextDepth, skipDefaultWrite: false); + PopulateObjectCore(ref context, existingItem, elementMetadata, nextDepth, skipDefaultWrite: false); continue; } } @@ -348,12 +265,10 @@ public static partial class AcBinaryDeserializer if (i < existingCount) { - // Replace existing item existingList[i] = value; } else { - // Add new item existingList.Add(value); } } @@ -375,7 +290,8 @@ public static partial class AcBinaryDeserializer #region Merge Methods /// - /// IId collection merge using cached property info (for property-based merge). + /// IId collection merge using cached property info. + /// Matches items by Id, updates existing, adds new, optionally removes orphans. /// private static void MergeIIdCollection(ref BinaryDeserializationContext context, IList existingList, BinaryPropertySetterInfo propInfo, int depth) { @@ -430,7 +346,7 @@ public static partial class AcBinaryDeserializer var newItem = CreateInstance(elementType, elementMetadata); if (newItem == null) continue; - // Read ref ID if present + // Handle ref ID if present if (context.HasReferenceHandling) { var refId = context.ReadVarInt(); @@ -439,7 +355,7 @@ public static partial class AcBinaryDeserializer } // Deserialize mode: new item just created, skip writing defaults - PopulateObject(ref context, newItem, elementMetadata, nextDepth, skipDefaultWrite: true); + PopulateObjectCore(ref context, newItem, elementMetadata, nextDepth, skipDefaultWrite: true); var itemId = idGetter(newItem); if (itemId != null && !IsDefaultValue(itemId, idType)) @@ -449,7 +365,7 @@ public static partial class AcBinaryDeserializer if (existingById != null && existingById.TryGetValue(itemId, out var existingItem)) { - // Copy properties to existing item + // Copy properties to existing item (preserves reference) CopyProperties(newItem, existingItem, elementMetadata); continue; } @@ -461,7 +377,6 @@ public static partial class AcBinaryDeserializer // Remove orphaned items (items in destination but not in source) if (context.RemoveOrphanedItems && existingById != null && sourceIds != null) { - // Find items to remove (those not in sourceIds) var itemsToRemove = new List(); foreach (var kvp in existingById) { @@ -471,7 +386,6 @@ public static partial class AcBinaryDeserializer } } - // Remove orphaned items foreach (var item in itemsToRemove) { existingList.Remove(item); @@ -485,7 +399,7 @@ public static partial class AcBinaryDeserializer } /// - /// Optimized IId collection merge using cached metadata (no runtime reflection). + /// IId collection merge using type metadata (for top-level list merge). /// private static void MergeIIdCollectionWithMetadata( ref BinaryDeserializationContext context, @@ -543,7 +457,7 @@ public static partial class AcBinaryDeserializer var newItem = CreateInstance(elementType, elementMetadata); if (newItem == null) continue; - // Read ref ID if present + // Handle ref ID if present if (context.HasReferenceHandling) { var refId = context.ReadVarInt(); @@ -552,7 +466,7 @@ public static partial class AcBinaryDeserializer } // Deserialize mode: new item just created, skip writing defaults - PopulateObject(ref context, newItem, elementMetadata, nextDepth, skipDefaultWrite: true); + PopulateObjectCore(ref context, newItem, elementMetadata, nextDepth, skipDefaultWrite: true); var itemId = idGetter(newItem); if (itemId != null && !IsDefaultValue(itemId, idType)) @@ -562,7 +476,7 @@ public static partial class AcBinaryDeserializer if (existingById != null && existingById.TryGetValue(itemId, out var existingItem)) { - // Copy properties to existing item + // Copy properties to existing item (preserves reference) CopyProperties(newItem, existingItem, elementMetadata); continue; } @@ -574,7 +488,6 @@ public static partial class AcBinaryDeserializer // Remove orphaned items (items in destination but not in source) if (context.RemoveOrphanedItems && existingById != null && sourceIds != null) { - // Find items to remove (those not in sourceIds) var itemsToRemove = new List(); foreach (var kvp in existingById) { @@ -584,7 +497,6 @@ public static partial class AcBinaryDeserializer } } - // Remove orphaned items foreach (var item in itemsToRemove) { existingList.Remove(item); @@ -611,7 +523,6 @@ public static partial class AcBinaryDeserializer /// /// Sets a property to its default value using typed setters to avoid boxing. - /// For collections and complex objects, clears the collection instead of setting to null. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SetPropertyToDefault(object target, BinaryPropertySetterInfo propInfo) @@ -647,18 +558,6 @@ public static partial class AcBinaryDeserializer return; case PropertyAccessorType.Object: default: - // For collections: clear instead of setting to null - //if (propInfo.IsCollection) - //{ - // var collection = propInfo.GetValue(target); - // if (collection is IList list) - // { - // list.Clear(); - // } - // // If not IList or null, leave it as-is (readonly collection or null already) - // return; - //} - // Reference types and nullable value types: set to null propInfo.SetValue(target, null); return; diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index 46fc890..346fade 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -332,12 +332,25 @@ public static partial class AcBinaryDeserializer if (typeCode == BinaryTypeCode.Object) { context.ReadByte(); - PopulateObjectMerge(ref context, target, targetType, 0); + // Uses unified PopulateObject which checks IsMergeMode internally + PopulateObject(ref context, target, targetType, 0); } else if (typeCode == BinaryTypeCode.Array && target is IList targetList) { context.ReadByte(); - PopulateListMerge(ref context, targetList, targetType, 0); + // For top-level list merge, check if it's an IId collection + var elementType = GetCollectionElementType(targetType); + if (elementType != null && IsComplexType(elementType)) + { + var elementMetadata = GetTypeMetadata(elementType); + if (elementMetadata.IsIId && elementMetadata.IdGetter != null) + { + MergeIIdCollectionWithMetadata(ref context, targetList, elementType, elementMetadata, 0); + return; + } + } + // Non-IId collection, just populate + PopulateList(ref context, targetList, targetType, 0); } else {