Skip to content

Commit 125988c

Browse files
authored
Refactoring OneOrMany (#118)
* Overhaul of OneOrMany - Only uses an array internally - Hot path for strings - Addresses inconsistency null rules for items * Updating tests to match new logic The change relates to value types now having no items actually acting that they have no items, not returning an item because the default value isn't equal to null. * Simplify HashCode generation * Add Span support * Added custom methods for `ToArray` and `FirstOrDefault` * Optimise zero-length array allocation * Removed custom FirstOrDefault
1 parent 937b63f commit 125988c

File tree

6 files changed

+137
-133
lines changed

6 files changed

+137
-133
lines changed

Source/Schema.NET/OneOrMany{T}.cs

Lines changed: 111 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace Schema.NET
44
using System.Collections;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Runtime.CompilerServices;
78

89
/// <summary>
910
/// A single or list of values.
@@ -15,131 +16,134 @@ public readonly struct OneOrMany<T>
1516
: IReadOnlyCollection<T>, IEnumerable<T>, IValues, IEquatable<OneOrMany<T>>
1617
#pragma warning restore CA1710 // Identifiers should have correct suffix
1718
{
18-
private readonly List<T> collection;
19-
private readonly T item;
19+
private readonly T[] collection;
2020

2121
/// <summary>
2222
/// Initializes a new instance of the <see cref="OneOrMany{T}"/> struct.
2323
/// </summary>
2424
/// <param name="item">The single item value.</param>
2525
public OneOrMany(T item)
2626
{
27-
this.collection = null;
28-
this.item = item;
27+
if (item is null || (item is string itemAsString && string.IsNullOrWhiteSpace(itemAsString)))
28+
{
29+
this.collection = null;
30+
this.HasOne = false;
31+
}
32+
else
33+
{
34+
this.collection = new[] { item };
35+
this.HasOne = true;
36+
}
2937
}
3038

3139
/// <summary>
3240
/// Initializes a new instance of the <see cref="OneOrMany{T}"/> struct.
3341
/// </summary>
34-
/// <param name="array">The array of values.</param>
35-
public OneOrMany(params T[] array)
36-
: this(array is null ? null : new List<T>(array))
42+
/// <param name="span">The span of values.</param>
43+
public OneOrMany(ReadOnlySpan<T> span)
3744
{
45+
if (!span.IsEmpty)
46+
{
47+
var items = new T[span.Length];
48+
var index = 0;
49+
50+
if (typeof(T) == typeof(string))
51+
{
52+
for (var i = 0; i < span.Length; i++)
53+
{
54+
var item = span[i];
55+
if (!string.IsNullOrWhiteSpace(item as string))
56+
{
57+
items[index] = item;
58+
index++;
59+
}
60+
}
61+
}
62+
else
63+
{
64+
for (var i = 0; i < span.Length; i++)
65+
{
66+
var item = span[i];
67+
if (item != null)
68+
{
69+
items[index] = item;
70+
index++;
71+
}
72+
}
73+
}
74+
75+
if (index > 0)
76+
{
77+
if (index == span.Length)
78+
{
79+
this.collection = items;
80+
}
81+
else
82+
{
83+
this.collection = new T[index];
84+
Array.Copy(items, 0, this.collection, 0, index);
85+
}
86+
87+
this.HasOne = index == 1;
88+
return;
89+
}
90+
}
91+
92+
this.collection = null;
93+
this.HasOne = false;
3894
}
3995

4096
/// <summary>
4197
/// Initializes a new instance of the <see cref="OneOrMany{T}"/> struct.
4298
/// </summary>
43-
/// <param name="collection">The collection of values.</param>
44-
public OneOrMany(IEnumerable<T> collection)
45-
: this(collection is null ? null : new List<T>(collection))
99+
/// <param name="array">The array of values.</param>
100+
public OneOrMany(params T[] array)
101+
: this(array.AsSpan())
46102
{
47103
}
48104

49105
/// <summary>
50106
/// Initializes a new instance of the <see cref="OneOrMany{T}"/> struct.
51107
/// </summary>
52-
/// <param name="list">The list of values.</param>
53-
public OneOrMany(List<T> list)
108+
/// <param name="collection">The collection of values.</param>
109+
public OneOrMany(IEnumerable<T> collection)
110+
: this(collection.ToArray().AsSpan())
54111
{
55-
if (list is null)
56-
{
57-
throw new ArgumentNullException(nameof(list));
58-
}
59-
60-
if (list.Count == 1)
61-
{
62-
this.collection = null;
63-
this.item = list[0];
64-
}
65-
else
66-
{
67-
this.collection = list;
68-
this.item = default;
69-
}
70112
}
71113

72114
/// <summary>
73115
/// Initializes a new instance of the <see cref="OneOrMany{T}"/> struct.
74116
/// </summary>
75-
/// <param name="list">The list of values.</param>
76-
public OneOrMany(IEnumerable<object> list)
117+
/// <param name="collection">The list of values.</param>
118+
public OneOrMany(IEnumerable<object> collection)
119+
: this(collection.Cast<T>().ToArray().AsSpan())
77120
{
78-
if (list is null)
79-
{
80-
throw new ArgumentNullException(nameof(list));
81-
}
82-
83-
var items = new List<T>();
84-
foreach (var item in list)
85-
{
86-
if (item is T itemT)
87-
{
88-
items.Add(itemT);
89-
}
90-
}
91-
92-
if (items.Count == 1)
93-
{
94-
this.collection = null;
95-
this.item = items[0];
96-
}
97-
else
98-
{
99-
this.collection = items;
100-
this.item = default;
101-
}
102121
}
103122

104123
/// <summary>
105124
/// Gets the number of elements contained in the <see cref="OneOrMany{T}"/>.
106125
/// </summary>
107-
public int Count
108-
{
109-
get
110-
{
111-
if (this.HasOne)
112-
{
113-
return 1;
114-
}
115-
else if (this.HasMany)
116-
{
117-
return this.collection.Count;
118-
}
119-
120-
return 0;
121-
}
122-
}
126+
public int Count => this.collection?.Length ?? 0;
123127

124128
/// <summary>
125129
/// Gets a value indicating whether this instance has a single item value.
126130
/// </summary>
127131
/// <value><c>true</c> if this instance has a single item value; otherwise, <c>false</c>.</value>
128-
public bool HasOne => this.collection is null && this.item != null;
132+
public bool HasOne { get; }
129133

130134
/// <summary>
131135
/// Gets a value indicating whether this instance has more than one value.
132136
/// </summary>
133137
/// <value><c>true</c> if this instance has more than one value; otherwise, <c>false</c>.</value>
134-
public bool HasMany => this.collection != null;
138+
public bool HasMany => this.collection?.Length > 1;
135139

136140
/// <summary>
137141
/// Performs an implicit conversion from <typeparamref name="T"/> to <see cref="OneOrMany{T}"/>.
138142
/// </summary>
139143
/// <param name="item">The single item value.</param>
140144
/// <returns>The result of the conversion.</returns>
141145
#pragma warning disable CA2225 // Operator overloads have named alternates
142-
public static implicit operator OneOrMany<T>(T item) => item != null && IsStringNullOrWhiteSpace(item) ? default : new OneOrMany<T>(item);
146+
public static implicit operator OneOrMany<T>(T item) => new OneOrMany<T>(item);
143147
#pragma warning restore CA2225 // Operator overloads have named alternates
144148

145149
/// <summary>
@@ -148,7 +152,7 @@ public int Count
148152
/// <param name="array">The array of values.</param>
149153
/// <returns>The result of the conversion.</returns>
150154
#pragma warning disable CA2225 // Operator overloads have named alternates
151-
public static implicit operator OneOrMany<T>(T[] array) => new OneOrMany<T>(array?.Where(x => x != null && !IsStringNullOrWhiteSpace(x)));
155+
public static implicit operator OneOrMany<T>(T[] array) => new OneOrMany<T>(array);
152156
#pragma warning restore CA2225 // Operator overloads have named alternates
153157

154158
/// <summary>
@@ -157,7 +161,7 @@ public int Count
157161
/// <param name="list">The list of values.</param>
158162
/// <returns>The result of the conversion.</returns>
159163
#pragma warning disable CA2225 // Operator overloads have named alternates
160-
public static implicit operator OneOrMany<T>(List<T> list) => new OneOrMany<T>(list?.Where(x => x != null && !IsStringNullOrWhiteSpace(x)));
164+
public static implicit operator OneOrMany<T>(List<T> list) => new OneOrMany<T>(list);
161165
#pragma warning restore CA2225 // Operator overloads have named alternates
162166

163167
/// <summary>
@@ -219,17 +223,13 @@ public int Count
219223
/// <returns>An enumerator for the <see cref="OneOrMany{T}"/>.</returns>
220224
public IEnumerator<T> GetEnumerator()
221225
{
222-
if (this.HasMany)
226+
if (this.collection != null)
223227
{
224-
foreach (var item in this.collection)
228+
for (var i = 0; i < this.collection.Length; i++)
225229
{
226-
yield return item;
230+
yield return this.collection[i];
227231
}
228232
}
229-
else if (this.HasOne)
230-
{
231-
yield return this.item;
232-
}
233233
}
234234

235235
/// <summary>
@@ -238,6 +238,32 @@ public IEnumerator<T> GetEnumerator()
238238
/// <returns>An enumerator for the <see cref="OneOrMany{T}"/>.</returns>
239239
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
240240

241+
/// <summary>
242+
/// Creates an array from <see cref="OneOrMany{T}" />.
243+
/// </summary>
244+
/// <returns>An array containing all the elements.</returns>
245+
public T[] ToArray()
246+
{
247+
if (this.HasOne)
248+
{
249+
return new[] { this.collection[0] };
250+
}
251+
else if (this.HasMany)
252+
{
253+
var result = new T[this.collection.Length];
254+
Array.Copy(this.collection, 0, result, 0, this.collection.Length);
255+
return result;
256+
}
257+
else
258+
{
259+
#if NETSTANDARD1_1
260+
return new T[0];
261+
#else
262+
return Array.Empty<T>();
263+
#endif
264+
}
265+
}
266+
241267
/// <summary>
242268
/// Indicates whether the current object is equal to another object of the same type.
243269
/// </summary>
@@ -253,16 +279,16 @@ public bool Equals(OneOrMany<T> other)
253279
}
254280
else if (this.HasOne && other.HasOne)
255281
{
256-
return this.item.Equals(other.item);
282+
return this.collection[0].Equals(other.collection[0]);
257283
}
258284
else if (this.HasMany && other.HasMany)
259285
{
260-
if (this.collection.Count != other.collection.Count)
286+
if (this.collection.Length != other.collection.Length)
261287
{
262288
return false;
263289
}
264290

265-
for (var i = 0; i < this.collection.Count; i++)
291+
for (var i = 0; i < this.collection.Length; i++)
266292
{
267293
if (!EqualityComparer<T>.Default.Equals(this.collection[i], other.collection[i]))
268294
{
@@ -291,26 +317,6 @@ public bool Equals(OneOrMany<T> other)
291317
/// <returns>
292318
/// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
293319
/// </returns>
294-
public override int GetHashCode()
295-
{
296-
if (this.HasOne)
297-
{
298-
return HashCode.Of(this.item);
299-
}
300-
else if (this.HasMany)
301-
{
302-
return HashCode.OfEach(this.collection);
303-
}
304-
305-
return 0;
306-
}
307-
308-
/// <summary>
309-
/// Checks whether the generic T item is a string that is either null or contains whitespace.
310-
/// </summary>
311-
/// <returns>
312-
/// Returns true if the supplied item is a string that is null or contains whitespace.
313-
/// </returns>
314-
private static bool IsStringNullOrWhiteSpace(T item) => item.GetType() == typeof(string) && string.IsNullOrWhiteSpace(item as string);
320+
public override int GetHashCode() => HashCode.OfEach(this.collection);
315321
}
316322
}

Source/Schema.NET/Schema.NET.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="1.0.0" />
4545
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.4.43" />
4646
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
47+
<PackageReference Include="System.Memory" Version="4.5.3" />
4748
</ItemGroup>
4849

4950
<ItemGroup Label="Analyzer Package References">

Tests/Schema.NET.Test/OneOrManyTest.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public void Constructor_NullList_ThrowsArgumentNullException() =>
5757
Assert.Throws<ArgumentNullException>(() => new OneOrMany<int>((List<int>)null));
5858

5959
[Fact]
60-
public void Count_DefaultStructConstructor_ReturnsOne() => Assert.Single(default(OneOrMany<int>));
60+
public void Count_DefaultStructConstructor_ReturnsZero() => Assert.Empty(default(OneOrMany<int>));
6161

6262
[Fact]
6363
public void Count_DefaultClassConstructor_ReturnsZero() => Assert.Empty(default(OneOrMany<string>));
@@ -76,7 +76,7 @@ public void Count_Enumerable_ReturnsTwo() =>
7676
Assert.Equal(2, new OneOrMany<int>(new List<int>() { 1, 2 }).Count);
7777

7878
[Fact]
79-
public void HasOne_DefaultStructConstructor_ReturnsTrue() => Assert.True(default(OneOrMany<int>).HasOne);
79+
public void HasOne_DefaultStructConstructor_ReturnsFalse() => Assert.False(default(OneOrMany<int>).HasOne);
8080

8181
[Fact]
8282
public void HasOne_DefaultClassConstructor_ReturnsFalse() => Assert.False(default(OneOrMany<string>).HasOne);
@@ -169,11 +169,8 @@ public void NotEqualsOperator_IsNotEqual_ReturnsTrue() =>
169169
Assert.True(new OneOrMany<int>(1) != new OneOrMany<int>(2));
170170

171171
[Fact]
172-
public void GetEnumerator_DefaultStructConstructor_ReturnsZero()
173-
{
174-
var item = Assert.Single(((IEnumerable)default(OneOrMany<int>)).Cast<object>());
175-
Assert.Equal(0, item);
176-
}
172+
public void GetEnumerator_DefaultStructConstructor_ReturnsNull() =>
173+
Assert.Empty(((IEnumerable)default(OneOrMany<int>)).Cast<object>());
177174

178175
[Fact]
179176
public void GetEnumerator_DefaultClassConstructor_ReturnsNull() =>

0 commit comments

Comments
 (0)