Skip to content

Commit 46127ea

Browse files
authored
CollectionsMarshal.GetValueRef(Dictionary) (dotnet#49388)
1 parent 604ea07 commit 46127ea

File tree

4 files changed

+179
-1
lines changed

4 files changed

+179
-1
lines changed

src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ public virtual void GetObjectData(SerializationInfo info, StreamingContext conte
352352
}
353353
}
354354

355-
private ref TValue FindValue(TKey key)
355+
internal ref TValue FindValue(TKey key)
356356
{
357357
if (key == null)
358358
{

src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/CollectionsMarshal.cs

+13
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,20 @@ public static class CollectionsMarshal
1414
/// Get a <see cref="Span{T}"/> view over a <see cref="List{T}"/>'s data.
1515
/// Items should not be added or removed from the <see cref="List{T}"/> while the <see cref="Span{T}"/> is in use.
1616
/// </summary>
17+
/// <param name="list">The list to get the data view over.</param>
1718
public static Span<T> AsSpan<T>(List<T>? list)
1819
=> list is null ? default : new Span<T>(list._items, 0, list._size);
20+
21+
/// <summary>
22+
/// Gets either a ref to a <typeparamref name="TValue"/> in the <see cref="Dictionary{TKey, TValue}"/> or a ref null if it does not exist in the <paramref name="dictionary"/>.
23+
/// </summary>
24+
/// <param name="dictionary">The dictionary to get the ref to <typeparamref name="TValue"/> from.</param>
25+
/// <param name="key">The key used for lookup.</param>
26+
/// <remarks>
27+
/// Items should not be added or removed from the <see cref="Dictionary{TKey, TValue}"/> while the ref <typeparamref name="TValue"/> is in use.
28+
/// The ref null can be detected using System.Runtime.CompilerServices.Unsafe.IsNullRef
29+
/// </remarks>
30+
public static ref TValue GetValueRefOrNullRef<TKey, TValue>(Dictionary<TKey, TValue> dictionary, TKey key) where TKey : notnull
31+
=> ref dictionary.FindValue(key);
1932
}
2033
}

src/libraries/System.Runtime.InteropServices/ref/System.Runtime.InteropServices.cs

+1
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ public CoClassAttribute(System.Type coClass) { }
176176
public static partial class CollectionsMarshal
177177
{
178178
public static System.Span<T> AsSpan<T>(System.Collections.Generic.List<T>? list) { throw null; }
179+
public static ref TValue GetValueRefOrNullRef<TKey, TValue>(System.Collections.Generic.Dictionary<TKey, TValue> dictionary, TKey key) where TKey : notnull { throw null; }
179180
}
180181
[System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Parameter | System.AttributeTargets.Property | System.AttributeTargets.ReturnValue, Inherited=false)]
181182
public sealed partial class ComAliasNameAttribute : System.Attribute

src/libraries/System.Runtime.InteropServices/tests/System/Runtime/InteropServices/CollectionsMarshalTests.cs

+164
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Runtime.CompilerServices;
8+
79
using Xunit;
810

911
namespace System.Runtime.InteropServices.Tests
@@ -142,9 +144,171 @@ public void ListAsSpanLinkBreaksOnResize()
142144
}
143145
}
144146

147+
[Fact]
148+
public void GetValueRefOrNullRefValueType()
149+
{
150+
var dict = new Dictionary<int, Struct>
151+
{
152+
{ 1, default },
153+
{ 2, default }
154+
};
155+
156+
Assert.Equal(2, dict.Count);
157+
158+
Assert.Equal(0, dict[1].Value);
159+
Assert.Equal(0, dict[1].Property);
160+
161+
var itemVal = dict[1];
162+
itemVal.Value = 1;
163+
itemVal.Property = 2;
164+
165+
// Does not change values in dictionary
166+
Assert.Equal(0, dict[1].Value);
167+
Assert.Equal(0, dict[1].Property);
168+
169+
CollectionsMarshal.GetValueRefOrNullRef(dict, 1).Value = 3;
170+
CollectionsMarshal.GetValueRefOrNullRef(dict, 1).Property = 4;
171+
172+
Assert.Equal(3, dict[1].Value);
173+
Assert.Equal(4, dict[1].Property);
174+
175+
ref var itemRef = ref CollectionsMarshal.GetValueRefOrNullRef(dict, 2);
176+
177+
Assert.Equal(0, itemRef.Value);
178+
Assert.Equal(0, itemRef.Property);
179+
180+
itemRef.Value = 5;
181+
itemRef.Property = 6;
182+
183+
Assert.Equal(5, itemRef.Value);
184+
Assert.Equal(6, itemRef.Property);
185+
Assert.Equal(dict[2].Value, itemRef.Value);
186+
Assert.Equal(dict[2].Property, itemRef.Property);
187+
188+
itemRef = new() { Value = 7, Property = 8 };
189+
190+
Assert.Equal(7, itemRef.Value);
191+
Assert.Equal(8, itemRef.Property);
192+
Assert.Equal(dict[2].Value, itemRef.Value);
193+
Assert.Equal(dict[2].Property, itemRef.Property);
194+
195+
// Check for null refs
196+
197+
Assert.True(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dict, 3)));
198+
Assert.Throws<NullReferenceException>(() => CollectionsMarshal.GetValueRefOrNullRef(dict, 3).Value = 9);
199+
200+
Assert.Equal(2, dict.Count);
201+
}
202+
203+
[Fact]
204+
public void GetValueRefOrNullRefClass()
205+
{
206+
var dict = new Dictionary<int, IntAsObject>
207+
{
208+
{ 1, new() },
209+
{ 2, new() }
210+
};
211+
212+
Assert.Equal(2, dict.Count);
213+
214+
Assert.Equal(0, dict[1].Value);
215+
Assert.Equal(0, dict[1].Property);
216+
217+
var itemVal = dict[1];
218+
itemVal.Value = 1;
219+
itemVal.Property = 2;
220+
221+
// Does change values in dictionary
222+
Assert.Equal(1, dict[1].Value);
223+
Assert.Equal(2, dict[1].Property);
224+
225+
CollectionsMarshal.GetValueRefOrNullRef(dict, 1).Value = 3;
226+
CollectionsMarshal.GetValueRefOrNullRef(dict, 1).Property = 4;
227+
228+
Assert.Equal(3, dict[1].Value);
229+
Assert.Equal(4, dict[1].Property);
230+
231+
ref var itemRef = ref CollectionsMarshal.GetValueRefOrNullRef(dict, 2);
232+
233+
Assert.Equal(0, itemRef.Value);
234+
Assert.Equal(0, itemRef.Property);
235+
236+
itemRef.Value = 5;
237+
itemRef.Property = 6;
238+
239+
Assert.Equal(5, itemRef.Value);
240+
Assert.Equal(6, itemRef.Property);
241+
Assert.Equal(dict[2].Value, itemRef.Value);
242+
Assert.Equal(dict[2].Property, itemRef.Property);
243+
244+
itemRef = new() { Value = 7, Property = 8 };
245+
246+
Assert.Equal(7, itemRef.Value);
247+
Assert.Equal(8, itemRef.Property);
248+
Assert.Equal(dict[2].Value, itemRef.Value);
249+
Assert.Equal(dict[2].Property, itemRef.Property);
250+
251+
// Check for null refs
252+
253+
Assert.True(Unsafe.IsNullRef(ref CollectionsMarshal.GetValueRefOrNullRef(dict, 3)));
254+
Assert.Throws<NullReferenceException>(() => CollectionsMarshal.GetValueRefOrNullRef(dict, 3).Value = 9);
255+
256+
Assert.Equal(2, dict.Count);
257+
}
258+
259+
[Fact]
260+
public void GetValueRefOrNullRefLinkBreaksOnResize()
261+
{
262+
var dict = new Dictionary<int, Struct>
263+
{
264+
{ 1, new() }
265+
};
266+
267+
Assert.Equal(1, dict.Count);
268+
269+
ref var itemRef = ref CollectionsMarshal.GetValueRefOrNullRef(dict, 1);
270+
271+
Assert.Equal(0, itemRef.Value);
272+
Assert.Equal(0, itemRef.Property);
273+
274+
itemRef.Value = 1;
275+
itemRef.Property = 2;
276+
277+
Assert.Equal(1, itemRef.Value);
278+
Assert.Equal(2, itemRef.Property);
279+
Assert.Equal(dict[1].Value, itemRef.Value);
280+
Assert.Equal(dict[1].Property, itemRef.Property);
281+
282+
// Resize
283+
dict.EnsureCapacity(100);
284+
for (int i = 2; i <= 50; i++)
285+
{
286+
dict.Add(i, new());
287+
}
288+
289+
itemRef.Value = 3;
290+
itemRef.Property = 4;
291+
292+
Assert.Equal(3, itemRef.Value);
293+
Assert.Equal(4, itemRef.Property);
294+
295+
// Check connection broken
296+
Assert.NotEqual(dict[1].Value, itemRef.Value);
297+
Assert.NotEqual(dict[1].Property, itemRef.Property);
298+
299+
Assert.Equal(50, dict.Count);
300+
}
301+
302+
private struct Struct
303+
{
304+
public int Value;
305+
public int Property { get; set; }
306+
}
307+
145308
private class IntAsObject
146309
{
147310
public int Value;
311+
public int Property { get; set; }
148312
}
149313
}
150314
}

0 commit comments

Comments
 (0)