Skip to content

Commit 8fe4c19

Browse files
authored
Merge pull request #16 from rameel/cleanup
Optimize ArrayView and StringView
2 parents dc67995 + 0e8525c commit 8fe4c19

8 files changed

Lines changed: 65 additions & 56 deletions

File tree

Directory.Packages.props

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,5 @@
99
<PackageVersion Include="MinVer" Version="6.0.0" />
1010
<PackageVersion Include="NUnit" Version="4.3.2" />
1111
<PackageVersion Include="NUnit3TestAdapter" Version="5.0.0" />
12-
<PackageVersion Include="System.Collections.Immutable" Version="6.0.0" />
1312
</ItemGroup>
1413
</Project>

README.md

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,6 @@ foreach (ref readonly HeavyStruct s in view)
4747
}
4848

4949
```
50-
## Changelog
51-
52-
### 1.2.2
53-
- Optimize `ArrayView.Create` method for empty collection expression
54-
55-
### 1.2.1
56-
- Add `List<T>` to `ArrayView<T>` extension for NET9.0+
57-
58-
### 1.2.0
59-
- Add `Trim` overloads to `StringView` class
6050

6151
## Supported versions
6252

Ramstack.Structures.Tests/Collections/ArrayViewExtensionsTests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
namespace Ramstack.Collections;
22

3+
#if NET9_0_OR_GREATER
4+
35
[TestFixture]
46
public class ArrayViewExtensionsTests
57
{
6-
#if NET9_0_OR_GREATER
7-
88
[Test]
99
public void List_AsView_Empty()
1010
{
@@ -25,5 +25,6 @@ public void List_AsView()
2525
Assert.That(view, Is.EquivalentTo(list));
2626
}
2727

28-
#endif
2928
}
29+
30+
#endif

Ramstack.Structures/Collections/ArrayViewExtensions.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,13 @@ public static ArrayView<T> AsView<T>(this List<T>? list)
149149
if (list is not null)
150150
{
151151
var array = ListAccessor<T>.GetArray(list);
152-
var count = Math.Min(list.Count, array.Length);
152+
_ = array.Length;
153153

154-
return new ArrayView<T>(array, 0, count);
154+
//
155+
// SCG.List<T> maintains internal invariants, so we can safely use the unchecked constructor
156+
// to bypass redundant bounds checks for better performance.
157+
//
158+
return new ArrayView<T>(array, 0, list.Count, unused: 0);
155159
}
156160

157161
return ArrayView<T>.Empty;

Ramstack.Structures/Collections/ArrayView`1.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,23 @@ public ArrayView(T[] array, int index, int length)
101101
/// Initializes a new instance of the <see cref="ArrayView{T}"/> structure that creates
102102
/// a view for the specified range of the elements in the specified array.
103103
/// </summary>
104+
/// <remarks>
105+
/// This constructor is intentionally minimal and skips all argument validations,
106+
/// as the caller is responsible for ensuring correctness (e.g.,
107+
/// <paramref name="array"/> is non-null, <paramref name="index"/> and
108+
/// <paramref name="length"/> are within bounds).
109+
/// </remarks>
104110
/// <param name="array">The array to wrap.</param>
105111
/// <param name="index">The zero-based index of the first element in the range.</param>
106112
/// <param name="length">The number of elements in the range.</param>
107-
/// <param name="dummy">The dummy parameter.</param>
113+
/// <param name="unused">Unused parameter, exists solely to disambiguate overloads.</param>
108114
[MethodImpl(MethodImplOptions.AggressiveInlining)]
109-
private ArrayView(T[] array, int index, int length, int dummy)
115+
internal ArrayView(T[] array, int index, int length, int unused)
110116
{
111117
_index = index;
112118
_count = length;
113119
_array = array;
114-
_ = dummy;
120+
_ = unused;
115121
}
116122

117123
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
@@ -132,7 +138,7 @@ public ArrayView<T> Slice(int index)
132138
if ((uint)index > (uint)_count)
133139
ThrowHelper.ThrowArgumentOutOfRangeException();
134140

135-
return new ArrayView<T>(_array!, _index + index, _count - index, dummy: 0);
141+
return new ArrayView<T>(_array!, _index + index, _count - index, unused: 0);
136142
}
137143

138144
/// <summary>
@@ -157,7 +163,7 @@ public ArrayView<T> Slice(int index, int count)
157163
ThrowHelper.ThrowArgumentOutOfRangeException();
158164
}
159165

160-
return new ArrayView<T>(_array!, _index + index, count, dummy: 0);
166+
return new ArrayView<T>(_array!, _index + index, count, unused: 0);
161167
}
162168

163169
/// <summary>
@@ -316,7 +322,7 @@ ref array.GetRawArrayData(view._index),
316322
/// A <see cref="ArrayView{T}"/> representation of the array segment.
317323
/// </returns>
318324
public static implicit operator ArrayView<T>(ArraySegment<T> segment) =>
319-
new(segment.Array!, segment.Offset, segment.Count, dummy: 0);
325+
new(segment.Array!, segment.Offset, segment.Count, unused: 0);
320326

321327
/// <summary>
322328
/// Returns a string representation of the current instance's state,

Ramstack.Structures/Internal/JitHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public static ref readonly char GetRawStringData(this string text, int index) =>
3030
public static ref T GetRawArrayData<T>(this T[] array, int index)
3131
{
3232
// It's valid for a ref to point just past the end of an array, and it'll
33-
// be properly GC-tracked. (Though dereferencing it may result in undefined behavior.)
33+
// be properly GC-tracked. (Though dereferencing it may result in undefined behavior)
3434
return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), (nint)(uint)index);
3535
}
3636
}

Ramstack.Structures/Ramstack.Structures.csproj

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,6 @@ Ramstack.Collections.ReadOnlyArray&lt;T&gt;</Description>
6464
</PackageReference>
6565
</ItemGroup>
6666

67-
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
68-
<PackageReference Include="System.Collections.Immutable" />
69-
</ItemGroup>
70-
71-
7267
<ItemGroup>
7368
<None Include="..\README.md" Link="Properties\README.md">
7469
<Pack>True</Pack>

Ramstack.Structures/Text/StringView.cs

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public char this[int index]
4545
/// </summary>
4646
/// <param name="value">The string to wrap.</param>
4747
[MethodImpl(MethodImplOptions.AggressiveInlining)]
48-
public StringView(string value) : this(value, 0, value.Length, dummy: 0)
48+
public StringView(string value) : this(value, 0, value.Length, unused: 0)
4949
{
5050
}
5151

@@ -96,17 +96,23 @@ public StringView(string value, int index, int length)
9696
/// Initializes a new instance of the <see cref="StringView"/> structure that creates
9797
/// a view for the specified range of the characters in the specified string.
9898
/// </summary>
99+
/// <remarks>
100+
/// This constructor is intentionally minimal and skips all argument validations,
101+
/// as the caller is responsible for ensuring correctness (e.g.,
102+
/// <paramref name="value"/> is non-null, <paramref name="index"/> and
103+
/// <paramref name="length"/> are within bounds).
104+
/// </remarks>
99105
/// <param name="value">The string to wrap.</param>
100106
/// <param name="index">The zero-based index of the first character in the range.</param>
101107
/// <param name="length">The number of characters in the range.</param>
102-
/// <param name="dummy">The dummy parameter.</param>
108+
/// <param name="unused">Unused parameter, exists solely to disambiguate overloads.</param>
103109
[MethodImpl(MethodImplOptions.AggressiveInlining)]
104-
private StringView(string value, int index, int length, int dummy)
110+
private StringView(string value, int index, int length, int unused)
105111
{
106112
_index = index;
107113
_length = length;
108114
_value = value;
109-
_ = dummy;
115+
_ = unused;
110116
}
111117

112118
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
@@ -156,7 +162,7 @@ public StringView Slice(int start)
156162
if ((uint)start > (uint)_length)
157163
ThrowHelper.ThrowArgumentOutOfRangeException();
158164

159-
return new StringView(_value!, _index + start, _length - start, dummy: 0);
165+
return new StringView(_value!, _index + start, _length - start, unused: 0);
160166
}
161167

162168
/// <summary>
@@ -182,7 +188,7 @@ public StringView Slice(int start, int length)
182188
ThrowHelper.ThrowArgumentOutOfRangeException();
183189
}
184190

185-
return new StringView(_value!, _index + start, length, dummy: 0);
191+
return new StringView(_value!, _index + start, length, unused: 0);
186192
}
187193

188194
/// <summary>
@@ -256,7 +262,7 @@ public StringView TrimStart()
256262
if (!char.IsWhiteSpace(value.GetRawStringData(start)))
257263
break;
258264

259-
return new StringView(value!, start, final - start, dummy: 0);
265+
return new StringView(value!, start, final - start, unused: 0);
260266
}
261267

262268
/// <summary>
@@ -277,7 +283,7 @@ public StringView TrimStart(char trimChar)
277283
if (value.GetRawStringData(start) != trimChar)
278284
break;
279285

280-
return new StringView(value!, start, final - start, dummy: 0);
286+
return new StringView(value!, start, final - start, unused: 0);
281287
}
282288

283289
/// <summary>
@@ -306,7 +312,7 @@ public StringView TrimStart(params char[]? trimChars) =>
306312
/// <returns>
307313
/// The trimmed <see cref="StringView"/>.
308314
/// </returns>
309-
public StringView TrimStart(ReadOnlySpan<char> trimChars)
315+
public StringView TrimStart(params ReadOnlySpan<char> trimChars)
310316
{
311317
if (trimChars.Length == 0)
312318
return TrimStart();
@@ -319,16 +325,18 @@ public StringView TrimStart(ReadOnlySpan<char> trimChars)
319325
{
320326
for (; start < final; start++)
321327
{
322-
for (var i = 0; i < trimChars.Length; i++)
323-
if (value.GetRawStringData(start) == trimChars[i])
328+
var ch = value.GetRawStringData(start);
329+
330+
foreach (var trimChar in trimChars)
331+
if (ch == trimChar)
324332
goto MATCHED;
325333

326334
break;
327335
MATCHED: ;
328336
}
329337
}
330338

331-
return new StringView(value!, start, final - start, dummy: 0);
339+
return new StringView(value!, start, final - start, unused: 0);
332340
}
333341

334342
/// <summary>
@@ -348,7 +356,7 @@ public StringView TrimEnd()
348356
if (!char.IsWhiteSpace(value.GetRawStringData(final)))
349357
break;
350358

351-
return new StringView(value!, start, final + 1 - start, dummy: 0);
359+
return new StringView(value!, start, final + 1 - start, unused: 0);
352360
}
353361

354362
/// <summary>
@@ -369,7 +377,7 @@ public StringView TrimEnd(char trimChar)
369377
if (value.GetRawStringData(final) != trimChar)
370378
break;
371379

372-
return new StringView(value!, start, final + 1 - start, dummy: 0);
380+
return new StringView(value!, start, final + 1 - start, unused: 0);
373381
}
374382

375383
/// <summary>
@@ -398,7 +406,7 @@ public StringView TrimEnd(params char[]? trimChars) =>
398406
/// <returns>
399407
/// The trimmed <see cref="StringView"/>.
400408
/// </returns>
401-
public StringView TrimEnd(ReadOnlySpan<char> trimChars)
409+
public StringView TrimEnd(params ReadOnlySpan<char> trimChars)
402410
{
403411
if (trimChars.Length == 0)
404412
return TrimEnd();
@@ -411,16 +419,18 @@ public StringView TrimEnd(ReadOnlySpan<char> trimChars)
411419
{
412420
for (; final >= start; final--)
413421
{
414-
for (var i = 0; i < trimChars.Length; i++)
415-
if (value.GetRawStringData(final) == trimChars[i])
422+
var ch = value.GetRawStringData(final);
423+
424+
foreach (var trimChar in trimChars)
425+
if (ch == trimChar)
416426
goto MATCHED;
417427

418428
break;
419429
MATCHED: ;
420430
}
421431
}
422432

423-
return new StringView(value!, start, final + 1 - start, dummy: 0);
433+
return new StringView(value!, start, final + 1 - start, unused: 0);
424434
}
425435

426436
/// <summary>
@@ -446,7 +456,7 @@ public StringView Trim()
446456
break;
447457
}
448458

449-
return new StringView(value!, start, final + 1 - start, dummy: 0);
459+
return new StringView(value!, start, final + 1 - start, unused: 0);
450460
}
451461

452462
/// <summary>
@@ -473,7 +483,7 @@ public StringView Trim(char trimChar)
473483
break;
474484
}
475485

476-
return new StringView(value!, start, final + 1 - start, dummy: 0);
486+
return new StringView(value!, start, final + 1 - start, unused: 0);
477487
}
478488

479489
/// <summary>
@@ -502,7 +512,7 @@ public StringView Trim(params char[]? trimChars) =>
502512
/// <returns>
503513
/// The trimmed <see cref="StringView"/>.
504514
/// </returns>
505-
public StringView Trim(ReadOnlySpan<char> trimChars)
515+
public StringView Trim(params ReadOnlySpan<char> trimChars)
506516
{
507517
if (trimChars.Length == 0)
508518
return Trim();
@@ -515,8 +525,10 @@ public StringView Trim(ReadOnlySpan<char> trimChars)
515525
{
516526
for (; start <= final; start++)
517527
{
518-
for (var i = 0; i < trimChars.Length; i++)
519-
if (value.GetRawStringData(start) == trimChars[i])
528+
var ch = value.GetRawStringData(start);
529+
530+
foreach (var trimChar in trimChars)
531+
if (ch == trimChar)
520532
goto MATCHED;
521533

522534
break;
@@ -525,16 +537,18 @@ public StringView Trim(ReadOnlySpan<char> trimChars)
525537

526538
for (; final > start; final--)
527539
{
528-
for (var i = 0; i < trimChars.Length; i++)
529-
if (value.GetRawStringData(final) == trimChars[i])
540+
var ch = value.GetRawStringData(final);
541+
542+
foreach (var trimChar in trimChars)
543+
if (ch == trimChar)
530544
goto MATCHED;
531545

532546
break;
533547
MATCHED: ;
534548
}
535549
}
536550

537-
return new StringView(value!, start, final + 1 - start, dummy: 0);
551+
return new StringView(value!, start, final + 1 - start, unused: 0);
538552
}
539553

540554
/// <summary>
@@ -723,8 +737,8 @@ public override bool Equals([NotNullWhen(true)] object? obj)
723737
if (obj is null)
724738
return _length == 0;
725739

726-
if (obj is StringView)
727-
return Equals(this, Unsafe.Unbox<StringView>(obj));
740+
if (obj is StringView view)
741+
return Equals(this, view);
728742

729743
return obj is string s && Equals(s);
730744
}

0 commit comments

Comments
 (0)