Skip to content

Commit 4ca385b

Browse files
authored
Merge pull request #10 from lookbusy1344/claude/issue-9-20250920-1359
Add documentation and tests for PooledString copy/disposal behavior
2 parents 2d9158b + 40190d9 commit 4ca385b

File tree

3 files changed

+529
-3
lines changed

3 files changed

+529
-3
lines changed

PooledString.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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}")]
2872
public 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

Comments
 (0)