Refactor serialization reference tracking and contexts
- Rework SerializationReferenceTracker to use a unified Bloom filter + HashSet for both IId and reference-based tracking, improving efficiency and reducing allocations. - Introduce AcSerializeBase as a common base class for serialization contexts; update Binary, JSON, and Toon contexts to inherit from it. - Move AcBinaryDeserializationException, AcJsonDeserializationException, and TypeConversionInfo to separate files for better organization. - Remove obsolete code and update documentation to reflect new reference tracking logic.
This commit is contained in:
parent
858d43b881
commit
2ab640b375
|
|
@ -0,0 +1,30 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AyCode.Core.Serializers;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for serialization operations.
|
||||
/// Contains serialize-specific logic that may need override capability.
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Reference scanning (ScanReferences)
|
||||
/// - Reference tracking during write (TrackForScanning, ShouldWriteId, MarkAsWritten)
|
||||
/// - IId-aware serialization logic
|
||||
///
|
||||
/// Derived classes:
|
||||
/// - BinarySerializationContext (or similar) for Binary serialization
|
||||
/// - JsonSerializationContext (or similar) for JSON serialization
|
||||
/// - ToonSerializationContext for Toon serialization
|
||||
///
|
||||
/// Note: Currently SerializationReferenceTracker remains in AcSerializerCommon.
|
||||
/// As patterns emerge, serialize-specific methods can be moved here.
|
||||
/// </summary>
|
||||
public abstract class AcSerializeBase
|
||||
{
|
||||
// Future: Move serialize-specific logic here
|
||||
// - SerializationReferenceTracker (or make it a protected property)
|
||||
// - Virtual ComputeHash method (for IId vs Reference distinction)
|
||||
// - Virtual TrackForScanning method
|
||||
// - Virtual ShouldWriteRef method
|
||||
// - etc.
|
||||
}
|
||||
|
|
@ -878,112 +878,144 @@ public static class AcSerializerCommon
|
|||
|
||||
/// <summary>
|
||||
/// Common reference tracking for serialization.
|
||||
/// Used by both JSON and Binary serializers to track multi-referenced objects.
|
||||
/// Supports both ReferenceEquals-based tracking and IId-based tracking.
|
||||
/// Uses int IDs for efficiency (no string allocation).
|
||||
/// Uses unified Bloom filter + HashSet for both IId and Reference tracking.
|
||||
/// - IId objects: hash = Id (positive, DB guarantees uniqueness per entity type)
|
||||
/// - Non-IId objects: hash = RuntimeHelpers.GetHashCode | 0x80000000 (negative)
|
||||
/// </summary>
|
||||
public sealed class SerializationReferenceTracker
|
||||
{
|
||||
private const int InitialReferenceCapacity = 64;
|
||||
private const int InitialMultiRefCapacity = 32;
|
||||
private const int InitialCapacity = 128;
|
||||
|
||||
// Unified Bloom filter (256 bits = 4 x 64-bit)
|
||||
private ulong _bloom0, _bloom1, _bloom2, _bloom3;
|
||||
|
||||
// Unified HashSet for seen hashes (both IId and Reference)
|
||||
private HashSet<int>? _seenHashes;
|
||||
|
||||
// Multi-referenced hashes
|
||||
private HashSet<int>? _multiRefHashes;
|
||||
|
||||
// Written refs: hash → refId
|
||||
private Dictionary<int, int>? _writtenRefs;
|
||||
|
||||
private Dictionary<object, int>? _scanOccurrences;
|
||||
private Dictionary<object, int>? _writtenRefs;
|
||||
private Dictionary<(Type, object), int>? _iidWrittenRefs; // IId-based written refs
|
||||
private HashSet<object>? _multiReferenced;
|
||||
private int _nextRefId = 1;
|
||||
|
||||
// IId tracker for same-IId-different-instance deduplication
|
||||
private IIdReferenceTracker? _iidTracker;
|
||||
|
||||
/// <summary>
|
||||
/// Resets the tracker for reuse.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_nextRefId = 1;
|
||||
_scanOccurrences?.Clear();
|
||||
_bloom0 = _bloom1 = _bloom2 = _bloom3 = 0;
|
||||
_seenHashes?.Clear();
|
||||
_multiRefHashes?.Clear();
|
||||
_writtenRefs?.Clear();
|
||||
_iidWrittenRefs?.Clear();
|
||||
_multiReferenced?.Clear();
|
||||
_iidTracker?.Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures internal collections are initialized.
|
||||
/// Call once before scanning when reference handling is enabled.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void EnsureInitialized()
|
||||
{
|
||||
_scanOccurrences ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
|
||||
_writtenRefs ??= new Dictionary<object, int>(InitialReferenceCapacity, ReferenceEqualityComparer.Instance);
|
||||
_iidWrittenRefs ??= new Dictionary<(Type, object), int>(InitialMultiRefCapacity);
|
||||
_multiReferenced ??= new HashSet<object>(InitialMultiRefCapacity, ReferenceEqualityComparer.Instance);
|
||||
_seenHashes ??= new HashSet<int>(InitialCapacity);
|
||||
_multiRefHashes ??= new HashSet<int>();
|
||||
_writtenRefs ??= new Dictionary<int, int>(InitialCapacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks an object during reference scanning phase.
|
||||
/// Returns true if this is the first occurrence (continue scanning).
|
||||
/// Returns false if already seen (object is multi-referenced, stop scanning this branch).
|
||||
/// Computes the tracking hash for an object.
|
||||
/// IId objects: positive hash from Id
|
||||
/// Non-IId objects: negative hash from RuntimeHelpers.GetHashCode
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int ComputeHash<TMetadata>(object obj, TMetadata metadata) where TMetadata : TypeMetadataBase
|
||||
{
|
||||
if (metadata.IsIId)
|
||||
{
|
||||
return metadata.IdAccessorType switch
|
||||
{
|
||||
IdAccessorType.Int32 => metadata.GetIdInt32(obj),
|
||||
IdAccessorType.Int64 => (int)(metadata.GetIdInt64(obj) ^ (metadata.GetIdInt64(obj) >> 32)),
|
||||
IdAccessorType.Guid => metadata.GetIdGuid(obj).GetHashCode() & 0x7FFFFFFF,
|
||||
_ => metadata.IdGetter?.Invoke(obj)?.GetHashCode() ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
// Non-IId: use RuntimeHelpers identity hash with high bit set (negative)
|
||||
return RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks an object during reference scanning phase (non-IId version).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrackForScanning(object obj)
|
||||
{
|
||||
if (_scanOccurrences == null) return true;
|
||||
|
||||
|
||||
ref var count = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
|
||||
if (exists)
|
||||
{
|
||||
count++;
|
||||
_multiReferenced!.Add(obj);
|
||||
return false;
|
||||
}
|
||||
count = 1;
|
||||
return true;
|
||||
var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
|
||||
return TrackHash(hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extended tracking with IId support.
|
||||
/// First checks IId match (different instance, same Id), then falls back to ReferenceEquals.
|
||||
/// Returns true if this is the first occurrence (continue scanning children).
|
||||
/// Returns false if already seen (stop scanning this branch).
|
||||
/// Tracks an object during reference scanning phase with IId support.
|
||||
/// Returns true if first occurrence (continue scanning).
|
||||
/// Returns false if already seen (multi-referenced, stop scanning).
|
||||
/// </summary>
|
||||
/// <param name="obj">Object to track</param>
|
||||
/// <param name="metadata">Type metadata with IId info</param>
|
||||
/// <param name="existingRefId">If returning false, contains the refId of the existing object (unused, kept for API compatibility)</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TrackForScanningWithIId<TMetadata>(object obj, TMetadata metadata, out int existingRefId)
|
||||
where TMetadata : TypeMetadataBase
|
||||
{
|
||||
existingRefId = 0;
|
||||
if (_scanOccurrences == null) return true;
|
||||
var hash = ComputeHash(obj, metadata);
|
||||
|
||||
// 1. IId check first (different instance, same Id)
|
||||
if (metadata.IsIId && _iidTracker != null && _iidTracker.TryGetOriginalObject(obj, metadata, out var originalObject))
|
||||
{
|
||||
// Same IId already seen → mark BOTH original and current as multi-referenced
|
||||
_multiReferenced!.Add(originalObject!); // Original object
|
||||
_multiReferenced.Add(obj); // Current object (duplicate)
|
||||
return false;
|
||||
// Skip default IId values (Id = 0)
|
||||
if (metadata.IsIId && hash == 0) return true;
|
||||
|
||||
return TrackHash(hash);
|
||||
}
|
||||
|
||||
// 2. ReferenceEquals check (same instance)
|
||||
ref var count = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(_scanOccurrences, obj, out var exists);
|
||||
if (exists)
|
||||
/// <summary>
|
||||
/// Core tracking logic using Bloom filter + HashSet.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool TrackHash(int hash)
|
||||
{
|
||||
count++;
|
||||
_multiReferenced!.Add(obj);
|
||||
return false;
|
||||
}
|
||||
count = 1;
|
||||
// Bloom filter check - fast "definitely new" detection
|
||||
var segment = (hash >> 6) & 3;
|
||||
var bit = hash & 63;
|
||||
var mask = 1UL << bit;
|
||||
|
||||
// 3. Register IId for future lookups
|
||||
if (metadata.IsIId)
|
||||
var bloomHit = segment switch
|
||||
{
|
||||
_iidTracker ??= new IIdReferenceTracker();
|
||||
_iidTracker.Register(obj, metadata);
|
||||
0 => (_bloom0 & mask) != 0,
|
||||
1 => (_bloom1 & mask) != 0,
|
||||
2 => (_bloom2 & mask) != 0,
|
||||
_ => (_bloom3 & mask) != 0
|
||||
};
|
||||
|
||||
if (!bloomHit)
|
||||
{
|
||||
// Definitely new - add to bloom and set
|
||||
switch (segment)
|
||||
{
|
||||
case 0: _bloom0 |= mask; break;
|
||||
case 1: _bloom1 |= mask; break;
|
||||
case 2: _bloom2 |= mask; break;
|
||||
default: _bloom3 |= mask; break;
|
||||
}
|
||||
_seenHashes ??= new HashSet<int>(InitialCapacity);
|
||||
_seenHashes.Add(hash);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Possible duplicate - check HashSet
|
||||
_seenHashes ??= new HashSet<int>(InitialCapacity);
|
||||
if (!_seenHashes.Add(hash))
|
||||
{
|
||||
// Already seen - multi-referenced!
|
||||
_multiRefHashes ??= new HashSet<int>();
|
||||
_multiRefHashes.Add(hash);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -991,16 +1023,37 @@ public static class AcSerializerCommon
|
|||
|
||||
/// <summary>
|
||||
/// Checks if object needs a reference ID during serialization.
|
||||
/// Returns true if object is multi-referenced and hasn't been written yet.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool ShouldWriteId(object obj, out int refId)
|
||||
{
|
||||
if (_multiReferenced != null && _multiReferenced.Contains(obj) && !_writtenRefs!.ContainsKey(obj))
|
||||
var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
|
||||
return ShouldWriteIdForHash(hash, out refId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if object needs a reference ID (IId-aware version).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool ShouldWriteIdForIId<TMetadata>(object obj, TMetadata metadata, out int refId)
|
||||
where TMetadata : TypeMetadataBase
|
||||
{
|
||||
var hash = ComputeHash(obj, metadata);
|
||||
return ShouldWriteIdForHash(hash, out refId);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool ShouldWriteIdForHash(int hash, out int refId)
|
||||
{
|
||||
if (_multiRefHashes != null && _multiRefHashes.Contains(hash))
|
||||
{
|
||||
_writtenRefs ??= new Dictionary<int, int>(InitialCapacity);
|
||||
if (!_writtenRefs.ContainsKey(hash))
|
||||
{
|
||||
refId = _nextRefId++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
refId = 0;
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1011,22 +1064,20 @@ public static class AcSerializerCommon
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void MarkAsWritten(object obj, int refId)
|
||||
{
|
||||
var type = obj.GetType();
|
||||
var idInfo = JsonUtilities.GetIdInfo(type);
|
||||
|
||||
// IId típus → IId alapján tároljuk
|
||||
if (idInfo.IsId && idInfo.IdType != null)
|
||||
{
|
||||
var key = GetIIdKey(obj, type, idInfo.IdType);
|
||||
if (key.HasValue)
|
||||
{
|
||||
_iidWrittenRefs![key.Value] = refId;
|
||||
return;
|
||||
}
|
||||
var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
|
||||
_writtenRefs![hash] = refId;
|
||||
}
|
||||
|
||||
// Nem IId → ReferenceEquals alapján
|
||||
_writtenRefs![obj] = refId;
|
||||
/// <summary>
|
||||
/// Marks object as written (IId-aware version).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void MarkAsWrittenForIId<TMetadata>(object obj, TMetadata metadata, int refId)
|
||||
where TMetadata : TypeMetadataBase
|
||||
{
|
||||
var hash = ComputeHash(obj, metadata);
|
||||
_writtenRefs ??= new Dictionary<int, int>(InitialCapacity);
|
||||
_writtenRefs[hash] = refId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1035,42 +1086,30 @@ public static class AcSerializerCommon
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetExistingRef(object obj, out int refId)
|
||||
{
|
||||
var type = obj.GetType();
|
||||
var idInfo = JsonUtilities.GetIdInfo(type);
|
||||
|
||||
// IId típus → IId alapján keresünk
|
||||
if (idInfo.IsId && idInfo.IdType != null && _iidWrittenRefs != null)
|
||||
{
|
||||
var key = GetIIdKey(obj, type, idInfo.IdType);
|
||||
if (key.HasValue && _iidWrittenRefs.TryGetValue(key.Value, out refId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Nem IId → ReferenceEquals alapján
|
||||
if (_writtenRefs != null && _writtenRefs.TryGetValue(obj, out refId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
refId = 0;
|
||||
return false;
|
||||
var hash = RuntimeHelpers.GetHashCode(obj) | unchecked((int)0x80000000);
|
||||
return TryGetExistingRefForHash(hash, out refId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the (Type, Id) key for IId-based lookup.
|
||||
/// Tries to get existing reference ID (IId-aware version).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static (Type, object)? GetIIdKey(object obj, Type type, Type idType)
|
||||
public bool TryGetExistingRefForIId<TMetadata>(object obj, TMetadata metadata, out int refId)
|
||||
where TMetadata : TypeMetadataBase
|
||||
{
|
||||
var idProp = type.GetProperty("Id");
|
||||
if (idProp == null) return null;
|
||||
var hash = ComputeHash(obj, metadata);
|
||||
return TryGetExistingRefForHash(hash, out refId);
|
||||
}
|
||||
|
||||
var id = idProp.GetValue(obj);
|
||||
if (id == null || JsonUtilities.IsDefaultValue(id, idType)) return null;
|
||||
|
||||
return (type, id);
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool TryGetExistingRefForHash(int hash, out int refId)
|
||||
{
|
||||
if (_writtenRefs != null && _writtenRefs.TryGetValue(hash, out refId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
refId = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when binary deserialization fails.
|
||||
/// </summary>
|
||||
public class AcBinaryDeserializationException : Exception
|
||||
{
|
||||
public int Position { get; }
|
||||
public Type? TargetType { get; }
|
||||
|
||||
public AcBinaryDeserializationException(string message, int position = 0, Type? targetType = null, Exception? innerException = null)
|
||||
: base(message, innerException)
|
||||
{
|
||||
Position = position;
|
||||
TargetType = targetType;
|
||||
}
|
||||
}
|
||||
|
|
@ -12,22 +12,6 @@ using static AyCode.Core.Helpers.JsonUtilities;
|
|||
|
||||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when binary deserialization fails.
|
||||
/// </summary>
|
||||
public class AcBinaryDeserializationException : Exception
|
||||
{
|
||||
public int Position { get; }
|
||||
public Type? TargetType { get; }
|
||||
|
||||
public AcBinaryDeserializationException(string message, int position = 0, Type? targetType = null, Exception? innerException = null)
|
||||
: base(message, innerException)
|
||||
{
|
||||
Position = position;
|
||||
TargetType = targetType;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// High-performance binary deserializer matching AcBinarySerializer.
|
||||
/// Features:
|
||||
|
|
@ -1412,21 +1396,4 @@ public static partial class AcBinaryDeserializer
|
|||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cached type conversion info. Using readonly struct to avoid heap allocation.
|
||||
/// </summary>
|
||||
readonly struct TypeConversionInfo
|
||||
{
|
||||
public readonly Type UnderlyingType;
|
||||
public readonly TypeCode TypeCode;
|
||||
public readonly bool IsEnum;
|
||||
|
||||
public TypeConversionInfo(Type underlyingType, TypeCode typeCode, bool isEnum)
|
||||
{
|
||||
UnderlyingType = underlyingType;
|
||||
TypeCode = typeCode;
|
||||
IsEnum = isEnum;
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation moved to AcBinaryDeserializer.TypeConversionInfo.cs
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ public static partial class AcBinarySerializer
|
|||
/// <summary>
|
||||
/// Binary serialization context. Public for generated serializers.
|
||||
/// </summary>
|
||||
internal sealed class BinarySerializationContext : IDisposable
|
||||
internal sealed class BinarySerializationContext : AcSerializeBase, IDisposable
|
||||
{
|
||||
private const int MinBufferSize = 256;
|
||||
private const int PropertyIndexBufferMaxCache = 512;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
namespace AyCode.Core.Serializers.Binaries;
|
||||
|
||||
/// <summary>
|
||||
/// Cached type conversion info. Using readonly struct to avoid heap allocation.
|
||||
/// </summary>
|
||||
readonly struct TypeConversionInfo
|
||||
{
|
||||
public readonly Type UnderlyingType;
|
||||
public readonly TypeCode TypeCode;
|
||||
public readonly bool IsEnum;
|
||||
|
||||
public TypeConversionInfo(Type underlyingType, TypeCode typeCode, bool isEnum)
|
||||
{
|
||||
UnderlyingType = underlyingType;
|
||||
TypeCode = typeCode;
|
||||
IsEnum = isEnum;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
namespace AyCode.Core.Serializers.Jsons;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when JSON deserialization fails.
|
||||
/// </summary>
|
||||
public class AcJsonDeserializationException : Exception
|
||||
{
|
||||
public string? Json { get; }
|
||||
public Type? TargetType { get; }
|
||||
|
||||
public AcJsonDeserializationException(string message, string? json = null, Type? targetType = null, Exception? innerException = null)
|
||||
: base(message, innerException)
|
||||
{
|
||||
Json = json?.Length > 500 ? json[..500] + "..." : json;
|
||||
TargetType = targetType;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,22 +9,6 @@ using static AyCode.Core.Helpers.JsonUtilities;
|
|||
|
||||
namespace AyCode.Core.Serializers.Jsons;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when JSON deserialization fails.
|
||||
/// </summary>
|
||||
public class AcJsonDeserializationException : Exception
|
||||
{
|
||||
public string? Json { get; }
|
||||
public Type? TargetType { get; }
|
||||
|
||||
public AcJsonDeserializationException(string message, string? json = null, Type? targetType = null, Exception? innerException = null)
|
||||
: base(message, innerException)
|
||||
{
|
||||
Json = json?.Length > 500 ? json[..500] + "..." : json;
|
||||
TargetType = targetType;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// High-performance custom JSON deserializer optimized for IId<T> reference handling.
|
||||
/// Uses Utf8JsonReader for streaming deserialization when possible (STJ approach).
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ public static partial class AcJsonSerializer
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class JsonSerializationContext : IDisposable
|
||||
private sealed class JsonSerializationContext : AcSerializeBase, IDisposable
|
||||
{
|
||||
private readonly ArrayBufferWriter<byte> _buffer;
|
||||
public Utf8JsonWriter Writer { get; private set; }
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ public static partial class AcToonSerializer
|
|||
/// Pooled context for Toon serialization.
|
||||
/// Handles output building, indentation, and reference tracking.
|
||||
/// </summary>
|
||||
private sealed class ToonSerializationContext
|
||||
private sealed class ToonSerializationContext : AcSerializeBase
|
||||
{
|
||||
private readonly StringBuilder _builder;
|
||||
private Dictionary<object, int>? _scanOccurrences;
|
||||
|
|
|
|||
Loading…
Reference in New Issue