Skip to content

Commit 6dee27f

Browse files
Add zero-allocation Join enumerator with ZLinq ValueEnumerable
- Add StringSegmentJoinEnumerator implementing IValueEnumerator<StringSegment> - Update Join() to return ValueEnumerable - Update Replace/ReplaceAsSegments to return ValueEnumerable - Fix ReplaceEach to use .ToArray() for yield boundary compatibility with .NET 10 - All 955 tests pass
1 parent 168c68f commit 6dee27f

5 files changed

Lines changed: 127 additions & 99 deletions

BenchmarkDotNet.Artifacts/results/Open.Text.Benchmarks.SplitAsSegmentsBaselineBenchmark-report-github.md

Lines changed: 0 additions & 24 deletions
This file was deleted.

BenchmarkDotNet.Artifacts/results/Open.Text.Benchmarks.SplitAsSegmentsBaselineBenchmark-report.csv

Lines changed: 0 additions & 12 deletions
This file was deleted.

BenchmarkDotNet.Artifacts/results/Open.Text.Benchmarks.SplitAsSegmentsBaselineBenchmark-report.html

Lines changed: 0 additions & 41 deletions
This file was deleted.

Source/Extensions.StringSegment.cs

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -205,28 +205,15 @@ public static ValueEnumerable<StringSegmentSequenceSplitEnumerator, StringSegmen
205205
/// </summary>
206206
/// <param name="source">The segments to join.</param>
207207
/// <param name="between">The segment to place between each segment.</param>
208-
/// <returns>An IEnumerable&lt;StringSegment&gt; of the segments.</returns>
208+
/// <returns>A ValueEnumerable of the joined segments (zero-allocation when used with foreach or ZLinq).</returns>
209209
/// <exception cref="System.ArgumentNullException">The source is null.</exception>
210-
public static IEnumerable<StringSegment> Join(this IEnumerable<StringSegment> source, StringSegment between)
210+
[CLSCompliant(false)]
211+
public static ValueEnumerable<StringSegmentJoinEnumerator, StringSegment> Join(this IEnumerable<StringSegment> source, StringSegment between)
211212
{
212213
return source is null
213214
? throw new ArgumentNullException(nameof(source))
214-
: JoinCore(source, between);
215-
216-
static IEnumerable<StringSegment> JoinCore(IEnumerable<StringSegment> source, StringSegment between)
217-
{
218-
using IEnumerator<StringSegment> e = source.GetEnumerator();
219-
bool ok = e.MoveNext();
220-
Debug.Assert(ok);
221-
StringSegment c = e.Current;
222-
if (c.Length != 0) yield return c;
223-
while (e.MoveNext())
224-
{
225-
if (between.HasValue) yield return between;
226-
c = e.Current;
227-
if (c.Length != 0) yield return c;
228-
}
229-
}
215+
: new ValueEnumerable<StringSegmentJoinEnumerator, StringSegment>(
216+
new StringSegmentJoinEnumerator(source, between));
230217
}
231218

232219
/// <summary>
@@ -250,8 +237,10 @@ public static string JoinToString(this IEnumerable<StringSegment> source, String
250237
/// <summary>
251238
/// Splits a sequence and replaces the removed sequences with the replacement sequence.
252239
/// </summary>
240+
/// <returns>A ValueEnumerable of the segments (zero-allocation when used with foreach or ZLinq).</returns>
253241
/// <inheritdoc cref="SplitAsSegments(string, string, StringSplitOptions, StringComparison)"/>
254-
public static IEnumerable<StringSegment> Replace(
242+
[CLSCompliant(false)]
243+
public static ValueEnumerable<StringSegmentJoinEnumerator, StringSegment> Replace(
255244
this StringSegment source,
256245
StringSegment splitSequence,
257246
StringSegment replacement,
@@ -267,14 +256,16 @@ public static string ReplaceToString(this StringSegment source,
267256
=> JoinToString(SplitAsSegments(source, splitSequence, comparisonType: comparisonType).ToArray(), replacement);
268257

269258
/// <inheritdoc cref="Replace(StringSegment, StringSegment, StringSegment, StringComparison)"/>
270-
public static IEnumerable<StringSegment> ReplaceAsSegments(
259+
[CLSCompliant(false)]
260+
public static ValueEnumerable<StringSegmentJoinEnumerator, StringSegment> ReplaceAsSegments(
271261
this string source,
272262
Regex splitSequence,
273263
StringSegment replacement)
274264
=> Join(SplitAsSegments(source, splitSequence), replacement);
275265

276266
/// <inheritdoc cref="Replace(StringSegment, StringSegment, StringSegment, StringComparison)"/>
277-
public static IEnumerable<StringSegment> ReplaceAsSegments(
267+
[CLSCompliant(false)]
268+
public static ValueEnumerable<StringSegmentJoinEnumerator, StringSegment> ReplaceAsSegments(
278269
this string source,
279270
StringSegment splitSequence,
280271
StringSegment replacement,
@@ -299,9 +290,10 @@ static IEnumerable<StringSegment> ReplaceEachCore(IEnumerable<StringSegment> sou
299290
{
300291
// Instead of source.SelectMany(s => s.Replace(splitSequence, replacement, comparisonType));
301292
// we manually yield to reduce allocations.
293+
// Note: We use ToArray() to materialize the ValueEnumerable because ref structs cannot be preserved across yield boundaries.
302294
foreach (StringSegment s in source)
303295
{
304-
foreach (StringSegment e in Replace(s, splitSequence, replacement, comparisonType))
296+
foreach (StringSegment e in Replace(s, splitSequence, replacement, comparisonType).ToArray())
305297
yield return e;
306298
}
307299
}

0 commit comments

Comments
 (0)