Skip to content

Commit 0cb2ea6

Browse files
author
msftbot[bot]
authored
Expose public indexer and length properties on RefEnumerable<T> and ReadOnlyRefEnumerable<T> (#4047)
## Closes #4043 ## PR Type What kind of change does this PR introduce? - Feature ## What is the current behavior? Currently, `RefEnumerable<T>` (and its read-only variant) do not expose an indexer or their length. This can make it difficult to implement certain APIs against them, such as a function to reverse their data. ## What is the new behavior? This PR exposes public length and indexer properties (for both `int` and `System.Index`) on both `RefEnumerable<T>` and `ReadOnlyRefEnumerable<T>`. ```diff public partial readonly ref struct RefEnumerable<T> { + public int Length { get; } + public ref T this[int index] { get; } + public ref T this[Index index] { get; } } public partial readonly ref struct ReadOnlyRefEnumerable<T> { + public int Length { get; } + public ref readonly T this[int index] { get; } + public ref readonly T this[Index index] { get; } } ``` ## PR Checklist Please check if your PR fulfills the following requirements: - [x] Tested code with current [supported SDKs](../readme.md#supported) - [ ] Pull Request has been submitted to the documentation repository [instructions](..\contributing.md#docs). Link: <!-- docs PR link --> - [ ] ~Sample in sample app has been added / updated (for bug fixes / features)~ - [ ] ~Icon has been created (if new sample) following the [Thumbnail Style Guide and templates](https://github.com/windows-toolkit/WindowsCommunityToolkit-design-assets)~ - [ ] ~New major technical changes in the toolkit have or will be added to the [Wiki](https://github.com/windows-toolkit/WindowsCommunityToolkit/wiki) e.g. build changes, source generators, testing infrastructure, sample creation changes, etc...~ - [x] Tests for the changes have been added (for bug fixes / features) (if applicable) - [ ] ~Header has been added to all new source files (run *build/UpdateHeaders.bat*)~ - [x] Contains **NO** breaking changes ## Other information It may be worthwhile to additionally expose the `Step` field and the newly-added `DangerousGetReference`/`DangerousGetReferenceAt` methods. This would allow for an implementation of `Reverse` that avoids bounds-checking.
2 parents 1973504 + 4d1bf10 commit 0cb2ea6

File tree

4 files changed

+271
-8
lines changed

4 files changed

+271
-8
lines changed

Microsoft.Toolkit.HighPerformance/Enumerables/ReadOnlyRefEnumerable{T}.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
#endif
1111
using Microsoft.Toolkit.HighPerformance.Helpers.Internals;
1212
using Microsoft.Toolkit.HighPerformance.Memory.Internals;
13-
1413
#if !SPAN_RUNTIME_SUPPORT
1514
using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers;
1615
#endif
@@ -121,6 +120,65 @@ internal ReadOnlyRefEnumerable(object? instance, IntPtr offset, int length, int
121120
}
122121
#endif
123122

123+
/// <summary>
124+
/// Gets the total available length for the sequence.
125+
/// </summary>
126+
public int Length
127+
{
128+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
129+
#if SPAN_RUNTIME_SUPPORT
130+
get => this.span.Length;
131+
#else
132+
get => this.length;
133+
#endif
134+
}
135+
136+
/// <summary>
137+
/// Gets the element at the specified zero-based index.
138+
/// </summary>
139+
/// <param name="index">The zero-based index of the element.</param>
140+
/// <returns>A reference to the element at the specified index.</returns>
141+
/// <exception cref="IndexOutOfRangeException">
142+
/// Thrown when <paramref name="index"/> is invalid.
143+
/// </exception>
144+
public ref readonly T this[int index]
145+
{
146+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
147+
get
148+
{
149+
if ((uint)index >= (uint)Length)
150+
{
151+
ThrowHelper.ThrowIndexOutOfRangeException();
152+
}
153+
154+
#if SPAN_RUNTIME_SUPPORT
155+
ref T r0 = ref MemoryMarshal.GetReference(this.span);
156+
#else
157+
ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference<T>(this.instance, this.offset);
158+
#endif
159+
nint offset = (nint)(uint)index * (nint)(uint)this.step;
160+
ref T ri = ref Unsafe.Add(ref r0, offset);
161+
162+
return ref ri;
163+
}
164+
}
165+
166+
#if NETSTANDARD2_1_OR_GREATER
167+
/// <summary>
168+
/// Gets the element at the specified zero-based index.
169+
/// </summary>
170+
/// <param name="index">The zero-based index of the element.</param>
171+
/// <returns>A reference to the element at the specified index.</returns>
172+
/// <exception cref="IndexOutOfRangeException">
173+
/// Thrown when <paramref name="index"/> is invalid.
174+
/// </exception>
175+
public ref readonly T this[Index index]
176+
{
177+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
178+
get => ref this[index.GetOffset(Length)];
179+
}
180+
#endif
181+
124182
/// <inheritdoc cref="System.Collections.IEnumerable.GetEnumerator"/>
125183
[Pure]
126184
[MethodImpl(MethodImplOptions.AggressiveInlining)]

Microsoft.Toolkit.HighPerformance/Enumerables/RefEnumerable{T}.cs

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
using System.Runtime.InteropServices;
1010
#endif
1111
using Microsoft.Toolkit.HighPerformance.Helpers.Internals;
12-
#if SPAN_RUNTIME_SUPPORT
1312
using Microsoft.Toolkit.HighPerformance.Memory.Internals;
14-
#else
13+
#if !SPAN_RUNTIME_SUPPORT
1514
using RuntimeHelpers = Microsoft.Toolkit.HighPerformance.Helpers.Internals.RuntimeHelpers;
1615
#endif
1716

@@ -39,11 +38,6 @@ public readonly ref struct RefEnumerable<T>
3938
/// The initial offset within <see cref="Instance"/>.
4039
/// </summary>
4140
internal readonly IntPtr Offset;
42-
43-
/// <summary>
44-
/// The total available length for the sequence.
45-
/// </summary>
46-
internal readonly int Length;
4741
#endif
4842

4943
/// <summary>
@@ -109,6 +103,65 @@ internal RefEnumerable(object? instance, IntPtr offset, int length, int step)
109103
}
110104
#endif
111105

106+
/// <summary>
107+
/// Gets the total available length for the sequence.
108+
/// </summary>
109+
public int Length
110+
{
111+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
112+
#if SPAN_RUNTIME_SUPPORT
113+
get => this.Span.Length;
114+
#else
115+
get;
116+
#endif
117+
}
118+
119+
/// <summary>
120+
/// Gets the element at the specified zero-based index.
121+
/// </summary>
122+
/// <param name="index">The zero-based index of the element.</param>
123+
/// <returns>A reference to the element at the specified index.</returns>
124+
/// <exception cref="IndexOutOfRangeException">
125+
/// Thrown when <paramref name="index"/> is invalid.
126+
/// </exception>
127+
public ref T this[int index]
128+
{
129+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
130+
get
131+
{
132+
if ((uint)index >= (uint)Length)
133+
{
134+
ThrowHelper.ThrowIndexOutOfRangeException();
135+
}
136+
137+
#if SPAN_RUNTIME_SUPPORT
138+
ref T r0 = ref MemoryMarshal.GetReference(this.Span);
139+
#else
140+
ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference<T>(this.Instance, this.Offset);
141+
#endif
142+
nint offset = (nint)(uint)index * (nint)(uint)this.Step;
143+
ref T ri = ref Unsafe.Add(ref r0, offset);
144+
145+
return ref ri;
146+
}
147+
}
148+
149+
#if NETSTANDARD2_1_OR_GREATER
150+
/// <summary>
151+
/// Gets the element at the specified zero-based index.
152+
/// </summary>
153+
/// <param name="index">The zero-based index of the element.</param>
154+
/// <returns>A reference to the element at the specified index.</returns>
155+
/// <exception cref="IndexOutOfRangeException">
156+
/// Thrown when <paramref name="index"/> is invalid.
157+
/// </exception>
158+
public ref T this[Index index]
159+
{
160+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
161+
get => ref this[index.GetOffset(Length)];
162+
}
163+
#endif
164+
112165
/// <inheritdoc cref="System.Collections.IEnumerable.GetEnumerator"/>
113166
[Pure]
114167
[MethodImpl(MethodImplOptions.AggressiveInlining)]

UnitTests/UnitTests.HighPerformance.Shared/Enumerables/Test_ReadOnlyRefEnumerable{T}.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,82 @@ public void Test_ReadOnlyRefEnumerable_DangerousCreate_BelowZero(int length, int
4747
{
4848
_ = ReadOnlyRefEnumerable<int>.DangerousCreate(in Unsafe.NullRef<int>(), length, step);
4949
}
50+
51+
[TestCategory("ReadOnlyRefEnumerable")]
52+
[TestMethod]
53+
[DataRow(1, new[] { 1 })]
54+
[DataRow(1, new[] { 1, 2, 3, 4 })]
55+
[DataRow(4, new[] { 1, 5, 9, 13 })]
56+
[DataRow(5, new[] { 1, 6, 11, 16 })]
57+
public void Test_ReadOnlyRefEnumerable_Indexer(int step, int[] values)
58+
{
59+
Span<int> data = new[]
60+
{
61+
1, 2, 3, 4,
62+
5, 6, 7, 8,
63+
9, 10, 11, 12,
64+
13, 14, 15, 16
65+
};
66+
67+
ReadOnlyRefEnumerable<int> enumerable = ReadOnlyRefEnumerable<int>.DangerousCreate(in data[0], values.Length, step);
68+
69+
for (int i = 0; i < enumerable.Length; i++)
70+
{
71+
Assert.AreEqual(enumerable[i], values[i]);
72+
}
73+
}
74+
75+
[TestCategory("ReadOnlyRefEnumerable")]
76+
[TestMethod]
77+
public void Test_ReadOnlyRefEnumerable_Indexer_ThrowsIndexOutOfRange()
78+
{
79+
int[] array = new[]
80+
{
81+
0, 0, 0, 0
82+
};
83+
84+
Assert.ThrowsException<IndexOutOfRangeException>(() => ReadOnlyRefEnumerable<int>.DangerousCreate(in array[0], array.Length, 1)[-1]);
85+
Assert.ThrowsException<IndexOutOfRangeException>(() => ReadOnlyRefEnumerable<int>.DangerousCreate(in array[0], array.Length, 1)[array.Length]);
86+
}
87+
88+
#if NETCOREAPP3_1_OR_GREATER
89+
[TestCategory("ReadOnlyRefEnumerable")]
90+
[TestMethod]
91+
[DataRow(1, new[] { 1 })]
92+
[DataRow(1, new[] { 1, 2, 3, 4 })]
93+
[DataRow(4, new[] { 1, 5, 9, 13 })]
94+
[DataRow(5, new[] { 1, 6, 11, 16 })]
95+
public void Test_ReadOnlyRefEnumerable_Index_Indexer(int step, int[] values)
96+
{
97+
Span<int> data = new[]
98+
{
99+
1, 2, 3, 4,
100+
5, 6, 7, 8,
101+
9, 10, 11, 12,
102+
13, 14, 15, 16
103+
};
104+
105+
ReadOnlyRefEnumerable<int> enumerable = ReadOnlyRefEnumerable<int>.DangerousCreate(in data[0], values.Length, step);
106+
107+
for (int i = 1; i <= enumerable.Length; i++)
108+
{
109+
Assert.AreEqual(values[^i], enumerable[^i]);
110+
}
111+
}
112+
113+
[TestCategory("ReadOnlyRefEnumerable")]
114+
[TestMethod]
115+
public void Test_ReadOnlyRefEnumerable_Index_Indexer_ThrowsIndexOutOfRange()
116+
{
117+
int[] array = new[]
118+
{
119+
0, 0, 0, 0
120+
};
121+
122+
Assert.ThrowsException<IndexOutOfRangeException>(() => ReadOnlyRefEnumerable<int>.DangerousCreate(in array[0], array.Length, 1)[new Index(array.Length)]);
123+
Assert.ThrowsException<IndexOutOfRangeException>(() => ReadOnlyRefEnumerable<int>.DangerousCreate(in array[0], array.Length, 1)[^0]);
124+
}
125+
#endif
50126
}
51127
}
52128

UnitTests/UnitTests.HighPerformance.Shared/Enumerables/Test_RefEnumerable{T}.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,82 @@ public void Test_RefEnumerable_DangerousCreate_BelowZero(int length, int step)
4747
{
4848
_ = RefEnumerable<int>.DangerousCreate(ref Unsafe.NullRef<int>(), length, step);
4949
}
50+
51+
[TestCategory("RefEnumerable")]
52+
[TestMethod]
53+
[DataRow(1, new[] { 1 })]
54+
[DataRow(1, new[] { 1, 2, 3, 4 })]
55+
[DataRow(4, new[] { 1, 5, 9, 13 })]
56+
[DataRow(5, new[] { 1, 6, 11, 16 })]
57+
public void Test_RefEnumerable_Indexer(int step, int[] values)
58+
{
59+
Span<int> data = new[]
60+
{
61+
1, 2, 3, 4,
62+
5, 6, 7, 8,
63+
9, 10, 11, 12,
64+
13, 14, 15, 16
65+
};
66+
67+
RefEnumerable<int> enumerable = RefEnumerable<int>.DangerousCreate(ref data[0], values.Length, step);
68+
69+
for (int i = 0; i < enumerable.Length; i++)
70+
{
71+
Assert.AreEqual(enumerable[i], values[i]);
72+
}
73+
}
74+
75+
[TestCategory("RefEnumerable")]
76+
[TestMethod]
77+
public void Test_RefEnumerable_Indexer_ThrowsIndexOutOfRange()
78+
{
79+
int[] array = new[]
80+
{
81+
0, 0, 0, 0
82+
};
83+
84+
Assert.ThrowsException<IndexOutOfRangeException>(() => RefEnumerable<int>.DangerousCreate(ref array[0], array.Length, 1)[-1]);
85+
Assert.ThrowsException<IndexOutOfRangeException>(() => RefEnumerable<int>.DangerousCreate(ref array[0], array.Length, 1)[array.Length]);
86+
}
87+
88+
#if NETCOREAPP3_1_OR_GREATER
89+
[TestCategory("RefEnumerable")]
90+
[TestMethod]
91+
[DataRow(1, new[] { 1 })]
92+
[DataRow(1, new[] { 1, 2, 3, 4 })]
93+
[DataRow(4, new[] { 1, 5, 9, 13 })]
94+
[DataRow(5, new[] { 1, 6, 11, 16 })]
95+
public void Test_RefEnumerable_Index_Indexer(int step, int[] values)
96+
{
97+
Span<int> data = new[]
98+
{
99+
1, 2, 3, 4,
100+
5, 6, 7, 8,
101+
9, 10, 11, 12,
102+
13, 14, 15, 16
103+
};
104+
105+
RefEnumerable<int> enumerable = RefEnumerable<int>.DangerousCreate(ref data[0], values.Length, step);
106+
107+
for (int i = 1; i <= enumerable.Length; i++)
108+
{
109+
Assert.AreEqual(values[^i], enumerable[^i]);
110+
}
111+
}
112+
113+
[TestCategory("RefEnumerable")]
114+
[TestMethod]
115+
public void Test_RefEnumerable_Index_Indexer_ThrowsIndexOutOfRange()
116+
{
117+
int[] array = new[]
118+
{
119+
0, 0, 0, 0
120+
};
121+
122+
Assert.ThrowsException<IndexOutOfRangeException>(() => RefEnumerable<int>.DangerousCreate(ref array[0], array.Length, 1)[new Index(array.Length)]);
123+
Assert.ThrowsException<IndexOutOfRangeException>(() => RefEnumerable<int>.DangerousCreate(ref array[0], array.Length, 1)[^0]);
124+
}
125+
#endif
50126
}
51127
}
52128

0 commit comments

Comments
 (0)