148 lines
5.9 KiB
C#
148 lines
5.9 KiB
C#
using System.Collections;
|
|
using System.Runtime.CompilerServices;
|
|
using static AyCode.Core.Helpers.JsonUtilities;
|
|
|
|
namespace AyCode.Core.Serializers.Binaries;
|
|
|
|
public static partial class AcBinarySerializer
|
|
{
|
|
/// <summary>
|
|
/// First pass: scans object graph to identify duplicates (strings + objects).
|
|
/// Only traverses reference properties (complex types + strings).
|
|
/// Stops traversing children after 2nd occurrence of an IId object:
|
|
/// - Prevents infinite recursion on circular references
|
|
/// - Consistent with write pass which writes ObjectRef (no children) for 2nd occurrence
|
|
/// - Strings/objects skipped here are never written anyway (parent is ObjectRef)
|
|
/// CacheIndex is assigned immediately on 2nd occurrence (no post-processing needed).
|
|
/// </summary>
|
|
private static void ScanForDuplicates<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
if (!context.HasCaching)
|
|
return;
|
|
|
|
var wrapper = context.GetWrapper(type);
|
|
ScanValue(value, wrapper, context, 0);
|
|
}
|
|
|
|
private static void ScanValue<TOutput>(object? value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
if (value == null || depth > context.MaxDepth)
|
|
return;
|
|
|
|
var metadata = wrapper.Metadata;
|
|
|
|
// Skip primitives (pre-computed field, no Type.GetTypeCode() call)
|
|
if (metadata.IsPrimitiveType)
|
|
return;
|
|
|
|
// Collection → iterate elements using IList fast path (no IEnumerator alloc)
|
|
if (metadata.IsCollection)
|
|
{
|
|
if (metadata.ElementNeedsScan)
|
|
{
|
|
var nextDepth = depth + 1;
|
|
if (value is IList list)
|
|
{
|
|
for (var i = 0; i < list.Count; i++)
|
|
{
|
|
var item = list[i];
|
|
if (item != null)
|
|
ScanItem(item, context, nextDepth);
|
|
}
|
|
}
|
|
else if (value is IEnumerable enumerable)
|
|
{
|
|
foreach (var item in enumerable)
|
|
{
|
|
if (item != null)
|
|
ScanItem(item, context, nextDepth);
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Object → ref tracking + recursive scan
|
|
|
|
// Reference tracking for IId types (or all types when ReferenceHandling == All)
|
|
// 2nd occurrence → skip children because:
|
|
// 1. Write pass writes ObjectRef (no children) → strings/objects here are never in output
|
|
// 2. Prevents infinite recursion on circular references (A→B→A→...)
|
|
// 3. Nested objects reachable from other paths are scanned through those paths
|
|
if (context.UseTypeReferenceHandling(metadata))
|
|
{
|
|
// Direct tracking call - avoid extra indirection through context
|
|
bool isFirst;
|
|
switch (metadata.IdAccessorType)
|
|
{
|
|
case IdAccessorType.Int32:
|
|
var id32 = wrapper.RefIdGetterInt32!(value);
|
|
isFirst = wrapper.TryTrackInt32(id32, context.NextFirstIndex++, ref context.NextCacheIndexRef, out _);
|
|
break;
|
|
case IdAccessorType.Int64:
|
|
var id64 = wrapper.RefIdGetterInt64!(value);
|
|
isFirst = wrapper.TryTrackInt64(id64, context.NextFirstIndex++, ref context.NextCacheIndexRef, out _);
|
|
break;
|
|
case IdAccessorType.Guid:
|
|
var idGuid = wrapper.RefIdGetterGuid!(value);
|
|
isFirst = wrapper.TryTrackGuid(idGuid, context.NextFirstIndex++, ref context.NextCacheIndexRef, out _);
|
|
break;
|
|
default:
|
|
isFirst = true;
|
|
break;
|
|
}
|
|
|
|
if (!isFirst)
|
|
return; // 2nd occurrence → skip children (symmetric with write pass ObjectRef)
|
|
}
|
|
|
|
// Recursive scan on reference properties only
|
|
// Use typed getter for strings (much faster than reflection GetValue)
|
|
var refProperties = metadata.ReferenceProperties;
|
|
var nextDepth2 = depth + 1;
|
|
for (var i = 0; i < refProperties.Length; i++)
|
|
{
|
|
var prop = refProperties[i];
|
|
if (prop.AccessorType == PropertyAccessorType.String)
|
|
{
|
|
// Fast path: typed getter for string
|
|
var str2 = prop.GetString(value);
|
|
if (str2 != null && context.IsValidForInterningString(str2.Length))
|
|
context.ScanInternString(str2);
|
|
}
|
|
else
|
|
{
|
|
// Object property: use generic getter, get wrapper for property type
|
|
var propValue = prop.GetValue(value);
|
|
if (propValue != null)
|
|
{
|
|
var propWrapper = context.GetWrapper(prop.PropertyType);
|
|
ScanValue(propValue, propWrapper, context, nextDepth2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scans a collection item. Handles string fast path and gets wrapper for the runtime type.
|
|
/// </summary>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void ScanItem<TOutput>(object item, BinarySerializationContext<TOutput> context, int depth)
|
|
where TOutput : BinaryOutputBase
|
|
{
|
|
// String fast path — avoid GetWrapper entirely
|
|
if (item is string str)
|
|
{
|
|
if (context.UseStringInterning && context.IsValidForInterningString(str.Length))
|
|
context.ScanInternString(str);
|
|
return;
|
|
}
|
|
|
|
var itemWrapper = context.GetWrapper(item.GetType());
|
|
ScanValue(item, itemWrapper, context, depth);
|
|
}
|
|
}
|