Skip to content

Commit 2a749e8

Browse files
Added LazyListUnsafe. Improved CopyTo methods for use with Span<T>. Migrated tests.
1 parent 5d573c3 commit 2a749e8

13 files changed

+352
-234
lines changed

Open.Collections.sln

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.31410.357
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Open.Collections", "source\Open.Collections.csproj", "{F1C64CD8-E90A-46C8-9373-3BAB5498CC14}"
7+
EndProject
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Open.Collections.Tests", "testing\Open.Collections.Tests\Open.Collections.Tests.csproj", "{44063AEE-8909-4A26-80BE-8623F5EBBA1F}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Release|Any CPU = Release|Any CPU
14+
EndGlobalSection
15+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
16+
{F1C64CD8-E90A-46C8-9373-3BAB5498CC14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17+
{F1C64CD8-E90A-46C8-9373-3BAB5498CC14}.Debug|Any CPU.Build.0 = Debug|Any CPU
18+
{F1C64CD8-E90A-46C8-9373-3BAB5498CC14}.Release|Any CPU.ActiveCfg = Release|Any CPU
19+
{F1C64CD8-E90A-46C8-9373-3BAB5498CC14}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{44063AEE-8909-4A26-80BE-8623F5EBBA1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{44063AEE-8909-4A26-80BE-8623F5EBBA1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{44063AEE-8909-4A26-80BE-8623F5EBBA1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{44063AEE-8909-4A26-80BE-8623F5EBBA1F}.Release|Any CPU.Build.0 = Release|Any CPU
24+
EndGlobalSection
25+
GlobalSection(SolutionProperties) = preSolution
26+
HideSolutionNode = FALSE
27+
EndGlobalSection
28+
GlobalSection(ExtensibilityGlobals) = postSolution
29+
SolutionGuid = {323AE57A-3C44-4EB8-890D-FD1107AADD8D}
30+
EndGlobalSection
31+
EndGlobal

source/CollectionWrapper.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,9 @@ public virtual void Clear()
4848
public virtual bool Remove(T item)
4949
=> InternalSource.Remove(item);
5050

51-
/// <inheritdoc cref="ReadOnlyCollectionWrapper&lt;T, TCollection&gt;" />
51+
/// <inheritdoc />
5252
public override bool IsReadOnly
5353
=> InternalSource.IsReadOnly;
54-
55-
/// <inheritdoc cref="ReadOnlyCollectionWrapper&lt;T, TCollection&gt;" />
56-
public override void CopyTo(T[] array, int arrayIndex)
57-
=> InternalSource.CopyTo(array, arrayIndex);
5854
#endregion
5955
}
6056
}

source/DictionaryToHashSetWrapper.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections;
1+
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34

45
namespace Open.Collections
@@ -13,12 +14,15 @@ public DictionaryToHashSetWrapper(IDictionary<T, bool> source)
1314
InternalSource = source;
1415
}
1516

17+
/// <inheritdoc />
1618
public int Count
1719
=> InternalSource.Count;
1820

21+
/// <inheritdoc />
1922
public bool IsReadOnly
2023
=> InternalSource.IsReadOnly;
2124

25+
/// <inheritdoc />
2226
public virtual bool Add(T item)
2327
{
2428
if (InternalSource.ContainsKey(item))
@@ -35,31 +39,45 @@ public virtual bool Add(T item)
3539
return true;
3640
}
3741

42+
/// <inheritdoc />
3843
public bool Remove(T item)
3944
// ReSharper disable once AssignNullToNotNullAttribute
4045
=> InternalSource.Remove(item);
4146

47+
/// <inheritdoc />
4248
public void Clear()
4349
=> InternalSource.Clear();
4450

51+
/// <inheritdoc />
4552
public bool Contains(T item)
4653
// ReSharper disable once AssignNullToNotNullAttribute
4754
=> InternalSource.ContainsKey(item);
4855

56+
/// <inheritdoc />
4957
public void CopyTo(T[] array, int arrayIndex)
5058
=> InternalSource.Keys.CopyTo(array, arrayIndex);
5159

60+
/// <inheritdoc cref="ReadOnlyCollectionWrapper{T, TCollection}.CopyTo(Span{T})"/>
61+
public virtual Span<T> CopyTo(Span<T> span)
62+
=> InternalSource.Keys.CopyToSpan(span);
63+
64+
/// <summary>
65+
/// Returns a copy of the underlying keys.
66+
/// </summary>
5267
public HashSet<T> ToHashSet()
5368
=> new(InternalSource.Keys);
5469

70+
/// <inheritdoc />
5571
public IEnumerator<T> GetEnumerator()
5672
=> InternalSource.Keys.GetEnumerator();
5773

74+
/// <inheritdoc />
5875
public void ExceptWith(IEnumerable<T> other)
5976
{
6077
foreach (var e in other) Remove(e);
6178
}
6279

80+
/// <inheritdoc />
6381
public void IntersectWith(IEnumerable<T> other)
6482
{
6583
foreach (var e in other)
@@ -69,24 +87,31 @@ public void IntersectWith(IEnumerable<T> other)
6987
}
7088
}
7189

90+
/// <inheritdoc />
7291
public bool IsProperSubsetOf(IEnumerable<T> other)
7392
=> ToHashSet().IsProperSubsetOf(other);
7493

94+
/// <inheritdoc />
7595
public bool IsProperSupersetOf(IEnumerable<T> other)
7696
=> ToHashSet().IsProperSupersetOf(other);
7797

98+
/// <inheritdoc />
7899
public bool IsSubsetOf(IEnumerable<T> other)
79100
=> ToHashSet().IsSubsetOf(other);
80101

102+
/// <inheritdoc />
81103
public bool IsSupersetOf(IEnumerable<T> other)
82104
=> ToHashSet().IsSupersetOf(other);
83105

106+
/// <inheritdoc />
84107
public bool Overlaps(IEnumerable<T> other)
85108
=> ToHashSet().Overlaps(other);
86109

110+
/// <inheritdoc />
87111
public bool SetEquals(IEnumerable<T> other)
88112
=> ToHashSet().SetEquals(other);
89113

114+
/// <inheritdoc />
90115
public void SymmetricExceptWith(IEnumerable<T> other)
91116
{
92117
foreach (var e in other)
@@ -98,6 +123,7 @@ public void SymmetricExceptWith(IEnumerable<T> other)
98123
}
99124
}
100125

126+
/// <inheritdoc />
101127
public void UnionWith(IEnumerable<T> other)
102128
{
103129
foreach (var e in other) Add(e);

source/Extensions.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,9 +816,30 @@ public static IEnumerable<T> Weave<T>(this IEnumerable<IEnumerable<T>> source)
816816
}
817817
}
818818

819+
/// <summary>
820+
/// Caches the results up to the last index requested.
821+
/// </summary>
822+
/// <param name="list">The source enumerable to cache.</param>
823+
/// <param name="isEndless">When true, will throw an InvalidOperationException if anything causes the list to evaluate to completion.</param>
824+
/// <returns>A LazyList<typeparamref name="T"/> for accessing the cached results.</returns>
819825
public static LazyList<T> Memoize<T>(this IEnumerable<T> list, bool isEndless = false)
820826
=> new(list, isEndless);
821827

828+
/// <summary>
829+
/// Caches the results up to the last index requested.
830+
///
831+
/// WARNING:
832+
/// - Is not thread safe.
833+
/// - There is a risk of recursion.
834+
/// - An endless enumerable may result in a stack overflow.
835+
///
836+
/// If any of the above are of concern, then use .Memoize() instead.
837+
/// </summary>
838+
/// <param name="list">The source enumerable to cache.</param>
839+
/// <returns>A LazyList<typeparamref name="T"/> for accessing the cached results.</returns>
840+
public static LazyListUnsafe<T> MemoizeUnsafe<T>(this IEnumerable<T> list)
841+
=> new(list);
842+
822843
/// <summary>
823844
/// .IndexOf extension optimized for an array.
824845
/// </summary>
@@ -834,5 +855,29 @@ public static int IndexOf<T>(this T[] source, T value)
834855
return -1;
835856
}
836857

858+
/// <summary>
859+
/// Copies the results to the provided span up to its length or until the end of the results.
860+
/// </summary>
861+
/// <param name="target">The span to copy to.</param>
862+
/// <returns>
863+
/// A span representing the results.
864+
/// If the count was less than the target length, a new span representing the results.
865+
/// Otherwise the target is returned.
866+
/// </returns>
867+
public static Span<T> CopyToSpan<T>(this IEnumerable<T> source, Span<T> target)
868+
{
869+
var len = target.Length;
870+
if (len == 0) return target;
871+
872+
var count = 0;
873+
foreach (var e in source)
874+
{
875+
target[count] = e;
876+
if (len == ++count) return target;
877+
}
878+
879+
return target.Slice(0, count);
880+
}
881+
837882
}
838883
}

source/LazyList.cs

Lines changed: 23 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -11,125 +11,46 @@
1111

1212
namespace Open.Collections
1313
{
14-
public class LazyList<T> : DisposableBase, IReadOnlyList<T>
14+
/// <summary>
15+
/// A a thread-safe list for caching the results of an enumerable.
16+
/// Note: should be disposed manually whenever possible as the locking mechanism is a ReaderWriterLockSlim.
17+
/// </summary>
18+
public class LazyList<T> : LazyListUnsafe<T>
1519
{
16-
List<T> _cached;
17-
IEnumerator<T> _enumerator;
18-
1920
ReaderWriterLockSlim Sync;
21+
int _safeCount;
2022

23+
/// <summary>
24+
/// A value indicating whether the results are known or expected to be finite.
25+
/// A list that was constructed as endless but has reached the end of the results will return false.
26+
/// </summary>
2127
public bool IsEndless { get; private set; }
22-
public LazyList(IEnumerable<T> source, bool isEndless = false)
28+
29+
public LazyList(IEnumerable<T> source, bool isEndless = false) : base(source)
2330
{
24-
_enumerator = source.GetEnumerator();
25-
_cached = new List<T>();
2631
Sync = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); // This is important as it's possible to recurse infinitely to generate a result. :(
2732
IsEndless = isEndless; // To indicate if a source is not allowed to fully enumerate.
2833
}
2934

3035
protected override void OnDispose()
3136
{
32-
using (Sync.WriteLock())
33-
{
34-
DisposeOf(ref _enumerator);
35-
Nullify(ref _cached)?.Clear();
36-
}
37+
using (Sync.WriteLock()) base.OnDispose();
3738

3839
DisposeOf(ref Sync);
3940
}
4041

41-
public T this[int index]
42-
{
43-
get
44-
{
45-
AssertIsAlive();
46-
47-
if (index < 0)
48-
throw new ArgumentOutOfRangeException(nameof(index), "Cannot be less than zero.");
49-
if (!EnsureIndex(index))
50-
throw new ArgumentOutOfRangeException(nameof(index), "Greater than total count.");
51-
52-
return _cached[index];
53-
}
54-
}
42+
private const string MARKED_ENDLESS = "This list is marked as endless and may never complete.";
5543

56-
private int _safeCount;
57-
public int Count
44+
/// <inheritdoc/>
45+
public override int IndexOf(T item)
5846
{
59-
get
60-
{
61-
AssertIsAlive();
62-
Finish();
63-
return _cached.Count;
64-
}
65-
}
47+
const string MESSAGE = MARKED_ENDLESS+" Use an enumerator, then Take(x).IndexOf().";
48+
if (IsEndless) throw new InvalidOperationException(MESSAGE);
6649

67-
public bool TryGetValueAt(int index, out T value)
68-
{
69-
if (EnsureIndex(index))
70-
{
71-
value = _cached[index];
72-
return true;
73-
}
74-
75-
value = default!;
76-
return false;
77-
}
78-
79-
public IEnumerator<T> GetEnumerator()
80-
{
81-
AssertIsAlive();
82-
83-
var index = -1;
84-
// Interlocked allows for multi-threaded access to this enumerator.
85-
while (TryGetValueAt(Interlocked.Increment(ref index), out var value))
86-
yield return value;
87-
}
88-
89-
public int IndexOf(T item)
90-
{
91-
AssertIsAlive();
92-
if (IsEndless)
93-
throw new InvalidOperationException("This list is marked as endless and may never complete. Use an enumerator, then Take(x).IndexOf().");
94-
95-
var index = 0;
96-
while (EnsureIndex(index))
97-
{
98-
var value = _cached[index];
99-
if (value is null)
100-
{
101-
if (item is null) return index;
102-
}
103-
else if (value.Equals(item))
104-
return index;
105-
106-
index++;
107-
}
108-
109-
return -1;
110-
111-
}
112-
113-
public bool Contains(T item)
114-
{
115-
AssertIsAlive();
116-
return IndexOf(item) != -1;
117-
}
118-
119-
public void CopyTo(T[] array, int arrayIndex = 0)
120-
{
121-
AssertIsAlive();
122-
var len = Math.Min(IsEndless ? int.MaxValue : Count, array.Length - arrayIndex);
123-
for (var i = 0; i < len; i++)
124-
array[i + arrayIndex] = this[i];
125-
}
126-
127-
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
128-
{
129-
return GetEnumerator();
50+
return base.IndexOf(item);
13051
}
13152

132-
private bool EnsureIndex(int maxIndex)
53+
protected override bool EnsureIndex(int maxIndex)
13354
{
13455
if (maxIndex < _safeCount)
13556
return true;
@@ -186,11 +107,11 @@ private bool EnsureIndex(int maxIndex)
186107
return false;
187108
}
188109

189-
private void Finish()
110+
protected override void Finish()
190111
{
191112
if (IsEndless)
192-
throw new InvalidOperationException("This list is marked as endless and may never complete.");
193-
while (EnsureIndex(int.MaxValue)) { }
113+
throw new InvalidOperationException(MARKED_ENDLESS);
114+
base.Finish();
194115
}
195116

196117
}

0 commit comments

Comments
 (0)