Skip to content

Commit 4362a7d

Browse files
committed
feat: introduce ReadOnlySpan overloads for Concat and AppendJoin methods to enhance performance
1 parent fb5c69c commit 4362a7d

File tree

5 files changed

+82
-5
lines changed

5 files changed

+82
-5
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ All notable changes to **ValueStringBuilder** will be documented in this file. T
66

77
## [Unreleased]
88

9+
This is the `v3` major release. The API is almost the same as in `v2` - there is only a slight change in the `Concat` static helper method to reflect a less-boxed API.
10+
11+
### Changed
12+
- `ValueStringBuilder.Concat` uses `params ReadOnlySpan<T>` to reduce boxing and improve performance.
13+
914
## [2.4.1] - 2025-03-25
1015

1116
### Changed

src/LinkDotNet.StringBuilder/LinkDotNet.StringBuilder.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
</PropertyGroup>
4343

4444
<ItemGroup>
45-
<PackageReference Include="Meziantou.Analyzer" Version="2.0.199">
45+
<PackageReference Include="Meziantou.Analyzer" Version="2.0.200">
4646
<PrivateAssets>all</PrivateAssets>
4747
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4848
</PackageReference>

src/LinkDotNet.StringBuilder/ValueStringBuilder.AppendJoin.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@ public ref partial struct ValueStringBuilder
1414
public void AppendJoin(ReadOnlySpan<char> separator, IEnumerable<string?> values)
1515
=> AppendJoinInternalString(separator, values);
1616

17+
/// <summary>
18+
/// Concatenates and appends all values with the given separator between each entry at the end of the string.
19+
/// </summary>
20+
/// <param name="separator">String used as separator between the entries.</param>
21+
/// <param name="values">Enumerable of strings to be concatenated.</param>
22+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
23+
public void AppendJoin(ReadOnlySpan<char> separator, ReadOnlySpan<string?> values)
24+
=> AppendJoinInternalString(separator, values);
25+
26+
/// <summary>
27+
/// Concatenates and appends all values with the given separator between each entry at the end of the string.
28+
/// </summary>
29+
/// <param name="separator">Character used as separator between the entries.</param>
30+
/// <param name="values">Enumerable of strings to be concatenated.</param>
31+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
32+
public void AppendJoin(char separator, ReadOnlySpan<string?> values)
33+
=> AppendJoinInternalChar(separator, values);
34+
1735
/// <summary>
1836
/// Concatenates and appends all values with the given separator between each entry at the end of the string.
1937
/// </summary>
@@ -42,6 +60,16 @@ public void AppendJoin(Rune separator, IEnumerable<string?> values)
4260
public void AppendJoin<T>(ReadOnlySpan<char> separator, IEnumerable<T> values)
4361
=> AppendJoinInternalString(separator, values);
4462

63+
/// <summary>
64+
/// Concatenates and appends all values with the given separator between each entry at the end of the string.
65+
/// </summary>
66+
/// <param name="separator">String used as separator between the entries.</param>
67+
/// <param name="values">Enumerable to be concatenated.</param>
68+
/// <typeparam name="T">Type of the given enumerable.</typeparam>
69+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
70+
public void AppendJoin<T>(ReadOnlySpan<char> separator, ReadOnlySpan<T> values)
71+
=> AppendJoinInternalString(separator, values);
72+
4573
/// <summary>
4674
/// Concatenates and appends all values with the given separator between each entry at the end of the string.
4775
/// </summary>
@@ -52,6 +80,16 @@ public void AppendJoin<T>(ReadOnlySpan<char> separator, IEnumerable<T> values)
5280
public void AppendJoin<T>(char separator, IEnumerable<T> values)
5381
=> AppendJoinInternalChar(separator, values);
5482

83+
/// <summary>
84+
/// Concatenates and appends all values with the given separator between each entry at the end of the string.
85+
/// </summary>
86+
/// <param name="separator">Character used as separator between the entries.</param>
87+
/// <param name="values">Enumerable to be concatenated.</param>
88+
/// <typeparam name="T">Type of the given enumerable.</typeparam>
89+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
90+
public void AppendJoin<T>(char separator, ReadOnlySpan<T> values)
91+
=> AppendJoinInternalChar(separator, values);
92+
5593
/// <summary>
5694
/// Concatenates and appends all values with the given separator between each entry at the end of the string.
5795
/// </summary>
@@ -85,6 +123,23 @@ private void AppendJoinInternalString<T>(ReadOnlySpan<char> separator, IEnumerab
85123
}
86124
}
87125

126+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
127+
private void AppendJoinInternalString<T>(ReadOnlySpan<char> separator, ReadOnlySpan<T> values)
128+
{
129+
if (values.Length == 0)
130+
{
131+
return;
132+
}
133+
134+
AppendInternal(values[0]);
135+
136+
for (var i = 1; i < values.Length; i++)
137+
{
138+
Append(separator);
139+
AppendInternal(values[i]);
140+
}
141+
}
142+
88143
[MethodImpl(MethodImplOptions.AggressiveInlining)]
89144
private void AppendJoinInternalChar<T>(char separator, IEnumerable<T> values)
90145
{
@@ -108,6 +163,23 @@ private void AppendJoinInternalChar<T>(char separator, IEnumerable<T> values)
108163
}
109164
}
110165

166+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
167+
private void AppendJoinInternalChar<T>(char separator, ReadOnlySpan<T> values)
168+
{
169+
if (values.Length == 0)
170+
{
171+
return;
172+
}
173+
174+
AppendInternal(values[0]);
175+
176+
for (var i = 1; i < values.Length; i++)
177+
{
178+
Append(separator);
179+
AppendInternal(values[i]);
180+
}
181+
}
182+
111183
[MethodImpl(MethodImplOptions.AggressiveInlining)]
112184
private void AppendJoinInternalRune<T>(Rune separator, IEnumerable<T> values)
113185
{

src/LinkDotNet.StringBuilder/ValueStringBuilder.Concat.Helper.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ public ref partial struct ValueStringBuilder
1111
/// <typeparam name="T">Any given type, which can be translated to <see cref="string"/>.</typeparam>
1212
/// <returns>Concatenated string or an empty string if <paramref name="values"/> is empty.</returns>
1313
[MethodImpl(MethodImplOptions.AggressiveInlining)]
14-
public static string Concat<T>(params T[] values)
14+
public static string Concat<T>(params ReadOnlySpan<T> values)
1515
{
16-
if (values is null || values.Length == 0)
16+
if (values.Length == 0)
1717
{
1818
return string.Empty;
1919
}

tests/LinkDotNet.StringBuilder.UnitTests/ValueStringBuilderTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ public void ConcatArbitraryValues()
437437
[Fact]
438438
public void ShouldReturnEmptyStringIfEmptyArray()
439439
{
440-
var result = ValueStringBuilder.Concat(Array.Empty<string>());
440+
var result = ValueStringBuilder.Concat<string>(Array.Empty<string>());
441441

442442
result.ShouldBe(string.Empty);
443443
}
@@ -467,7 +467,7 @@ public void ConcatShouldHandleNullValues()
467467
{
468468
string[]? array = null;
469469

470-
ValueStringBuilder.Concat(array!).ShouldBe(string.Empty);
470+
ValueStringBuilder.Concat<string>(array!).ShouldBe(string.Empty);
471471
}
472472

473473
[Fact]

0 commit comments

Comments
 (0)