@@ -24,6 +24,50 @@ PooledString implements IDisposable to allow freeing its memory back to the pool
2424/// <summary>
2525/// Value type representing a string allocated from an unmanaged pool. Just a reference and an allocation ID, 12 bytes total.
2626/// </summary>
27+ /// <remarks>
28+ /// <para><b>Copy Behavior and Disposal:</b></para>
29+ /// <para>
30+ /// PooledString is a value type (struct) with reference semantics for the underlying memory.
31+ /// When you copy a PooledString (e.g., via assignment), both the original and the copy share
32+ /// the same allocation ID and point to the same memory in the pool.
33+ /// </para>
34+ /// <para>
35+ /// <b>Important:</b> Disposing any copy invalidates ALL copies of that PooledString.
36+ /// This is because disposal removes the allocation from the pool's internal tracking,
37+ /// making the allocation ID invalid for all structs that reference it.
38+ /// </para>
39+ /// <example>
40+ /// <code>
41+ /// var original = pool.Allocate("Hello");
42+ /// var copy = original; // Both share the same allocation
43+ ///
44+ /// original.Dispose(); // Frees the allocation
45+ /// // Now BOTH original and copy are invalid:
46+ /// copy.AsSpan(); // Throws ArgumentException
47+ /// original.AsSpan(); // Also throws ArgumentException
48+ /// </code>
49+ /// </example>
50+ /// This behavior mirrors unmanaged memory semantics where freeing memory invalidates
51+ /// all pointers to it. Multiple disposals are safe (idempotent) - calling Dispose()
52+ /// on an already-freed PooledString has no effect.
53+ /// </para>
54+ /// <para>
55+ /// <b>Warning - Memory Leaks:</b> Reassigning a PooledString variable without first
56+ /// calling Dispose() will leak the original allocation. The original memory remains
57+ /// allocated in the pool but becomes unreferenced and inaccessible.
58+ /// </para>
59+ /// <example>
60+ /// <code>
61+ /// var str = pool.Allocate("Original");
62+ /// str = pool.Allocate("New"); // LEAK: "Original" is now unreferenced but still allocated
63+ ///
64+ /// // Correct approach:
65+ /// var str = pool.Allocate("Original");
66+ /// str.Dispose(); // Free the original allocation first
67+ /// str = pool.Allocate("New"); // Now safe to reassign
68+ /// </code>
69+ /// </example>
70+ /// </remarks>
2771[ System . Diagnostics . DebuggerDisplay ( "{ToString(),nq}" ) ]
2872public readonly record struct PooledString ( UnmanagedStringPool Pool , uint AllocationId ) : IDisposable
2973{
@@ -52,8 +96,37 @@ public readonly ReadOnlySpan<char> AsSpan()
5296 /// <summary>
5397 /// Free this string's memory back to the pool. This doesn't mutate the actual PooledString fields, it just updates the underlying pool
5498 /// </summary>
99+ /// <remarks>
100+ /// <b>Warning:</b> This invalidates ALL copies of this PooledString, not just this instance.
101+ /// Since PooledString is a value type, copies share the same allocation ID. Freeing any copy
102+ /// removes the allocation from the pool, making all copies invalid.
103+ /// Multiple calls to Free() on the same or different copies are safe (idempotent).
104+ /// </remarks>
55105 public readonly void Free ( ) => Pool ? . FreeString ( AllocationId ) ;
56106
107+ /// <summary>
108+ /// Creates a deep copy of this PooledString with a new allocation ID.
109+ /// </summary>
110+ /// <returns>A new PooledString with the same content but a different allocation ID</returns>
111+ /// <remarks>
112+ /// Unlike simple assignment which creates copies that share the same allocation,
113+ /// Duplicate() allocates new memory in the pool for an independent copy.
114+ /// The duplicated string will not be affected if the original is disposed, and vice versa.
115+ /// </remarks>
116+ public readonly PooledString Duplicate ( )
117+ {
118+ if ( AllocationId == UnmanagedStringPool . EmptyStringAllocationId ) {
119+ // Empty strings don't need actual cloning, just return the same empty reference
120+ return this ;
121+ }
122+
123+ CheckDisposed ( ) ;
124+
125+ // Allocate new memory in the pool with the same content
126+ var span = AsSpan ( ) ;
127+ return Pool . Allocate ( span ) ;
128+ }
129+
57130 /// <summary>
58131 /// Allocate a new PooledString with the given value at the specified position. Old PooledString is unchanged.
59132 /// </summary>
@@ -395,5 +468,9 @@ private readonly void SetAtPosition(int start, ReadOnlySpan<char> value)
395468 /// <summary>
396469 /// Free the string back to the pool, if it is not empty
397470 /// </summary>
471+ /// <remarks>
472+ /// <b>Warning:</b> This invalidates ALL copies of this PooledString, not just this instance.
473+ /// See <see cref="Free"/> for details about copy behavior.
474+ /// </remarks>
398475 public void Dispose ( ) => Free ( ) ;
399476}
0 commit comments