Skip to content

Commit

Permalink
Added LazyListUnsafe. Improved CopyTo methods for use with Span<T>. M…
Browse files Browse the repository at this point in the history
…igrated tests.
  • Loading branch information
electricessence committed Jul 4, 2021
1 parent 5d573c3 commit 2a749e8
Show file tree
Hide file tree
Showing 13 changed files with 352 additions and 234 deletions.
31 changes: 31 additions & 0 deletions Open.Collections.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31410.357
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Open.Collections", "source\Open.Collections.csproj", "{F1C64CD8-E90A-46C8-9373-3BAB5498CC14}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Open.Collections.Tests", "testing\Open.Collections.Tests\Open.Collections.Tests.csproj", "{44063AEE-8909-4A26-80BE-8623F5EBBA1F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F1C64CD8-E90A-46C8-9373-3BAB5498CC14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1C64CD8-E90A-46C8-9373-3BAB5498CC14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1C64CD8-E90A-46C8-9373-3BAB5498CC14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1C64CD8-E90A-46C8-9373-3BAB5498CC14}.Release|Any CPU.Build.0 = Release|Any CPU
{44063AEE-8909-4A26-80BE-8623F5EBBA1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{44063AEE-8909-4A26-80BE-8623F5EBBA1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{44063AEE-8909-4A26-80BE-8623F5EBBA1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{44063AEE-8909-4A26-80BE-8623F5EBBA1F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {323AE57A-3C44-4EB8-890D-FD1107AADD8D}
EndGlobalSection
EndGlobal
6 changes: 1 addition & 5 deletions source/CollectionWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,9 @@ public virtual void Clear()
public virtual bool Remove(T item)
=> InternalSource.Remove(item);

/// <inheritdoc cref="ReadOnlyCollectionWrapper&lt;T, TCollection&gt;" />
/// <inheritdoc />
public override bool IsReadOnly
=> InternalSource.IsReadOnly;

/// <inheritdoc cref="ReadOnlyCollectionWrapper&lt;T, TCollection&gt;" />
public override void CopyTo(T[] array, int arrayIndex)
=> InternalSource.CopyTo(array, arrayIndex);
#endregion
}
}
28 changes: 27 additions & 1 deletion source/DictionaryToHashSetWrapper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections;
using System;
using System.Collections;
using System.Collections.Generic;

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

/// <inheritdoc />
public int Count
=> InternalSource.Count;

/// <inheritdoc />
public bool IsReadOnly
=> InternalSource.IsReadOnly;

/// <inheritdoc />
public virtual bool Add(T item)
{
if (InternalSource.ContainsKey(item))
Expand All @@ -35,31 +39,45 @@ public virtual bool Add(T item)
return true;
}

/// <inheritdoc />
public bool Remove(T item)
// ReSharper disable once AssignNullToNotNullAttribute
=> InternalSource.Remove(item);

/// <inheritdoc />
public void Clear()
=> InternalSource.Clear();

/// <inheritdoc />
public bool Contains(T item)
// ReSharper disable once AssignNullToNotNullAttribute
=> InternalSource.ContainsKey(item);

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

/// <inheritdoc cref="ReadOnlyCollectionWrapper{T, TCollection}.CopyTo(Span{T})"/>
public virtual Span<T> CopyTo(Span<T> span)
=> InternalSource.Keys.CopyToSpan(span);

/// <summary>
/// Returns a copy of the underlying keys.
/// </summary>
public HashSet<T> ToHashSet()
=> new(InternalSource.Keys);

/// <inheritdoc />
public IEnumerator<T> GetEnumerator()
=> InternalSource.Keys.GetEnumerator();

/// <inheritdoc />
public void ExceptWith(IEnumerable<T> other)
{
foreach (var e in other) Remove(e);
}

/// <inheritdoc />
public void IntersectWith(IEnumerable<T> other)
{
foreach (var e in other)
Expand All @@ -69,24 +87,31 @@ public void IntersectWith(IEnumerable<T> other)
}
}

/// <inheritdoc />
public bool IsProperSubsetOf(IEnumerable<T> other)
=> ToHashSet().IsProperSubsetOf(other);

/// <inheritdoc />
public bool IsProperSupersetOf(IEnumerable<T> other)
=> ToHashSet().IsProperSupersetOf(other);

/// <inheritdoc />
public bool IsSubsetOf(IEnumerable<T> other)
=> ToHashSet().IsSubsetOf(other);

/// <inheritdoc />
public bool IsSupersetOf(IEnumerable<T> other)
=> ToHashSet().IsSupersetOf(other);

/// <inheritdoc />
public bool Overlaps(IEnumerable<T> other)
=> ToHashSet().Overlaps(other);

/// <inheritdoc />
public bool SetEquals(IEnumerable<T> other)
=> ToHashSet().SetEquals(other);

/// <inheritdoc />
public void SymmetricExceptWith(IEnumerable<T> other)
{
foreach (var e in other)
Expand All @@ -98,6 +123,7 @@ public void SymmetricExceptWith(IEnumerable<T> other)
}
}

/// <inheritdoc />
public void UnionWith(IEnumerable<T> other)
{
foreach (var e in other) Add(e);
Expand Down
45 changes: 45 additions & 0 deletions source/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -816,9 +816,30 @@ public static IEnumerable<T> Weave<T>(this IEnumerable<IEnumerable<T>> source)
}
}

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

/// <summary>
/// Caches the results up to the last index requested.
///
/// WARNING:
/// - Is not thread safe.
/// - There is a risk of recursion.
/// - An endless enumerable may result in a stack overflow.
///
/// If any of the above are of concern, then use .Memoize() instead.
/// </summary>
/// <param name="list">The source enumerable to cache.</param>
/// <returns>A LazyList<typeparamref name="T"/> for accessing the cached results.</returns>
public static LazyListUnsafe<T> MemoizeUnsafe<T>(this IEnumerable<T> list)
=> new(list);

/// <summary>
/// .IndexOf extension optimized for an array.
/// </summary>
Expand All @@ -834,5 +855,29 @@ public static int IndexOf<T>(this T[] source, T value)
return -1;
}

/// <summary>
/// Copies the results to the provided span up to its length or until the end of the results.
/// </summary>
/// <param name="target">The span to copy to.</param>
/// <returns>
/// A span representing the results.
/// If the count was less than the target length, a new span representing the results.
/// Otherwise the target is returned.
/// </returns>
public static Span<T> CopyToSpan<T>(this IEnumerable<T> source, Span<T> target)
{
var len = target.Length;
if (len == 0) return target;

var count = 0;
foreach (var e in source)
{
target[count] = e;
if (len == ++count) return target;
}

return target.Slice(0, count);
}

}
}
125 changes: 23 additions & 102 deletions source/LazyList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,125 +11,46 @@

namespace Open.Collections
{
public class LazyList<T> : DisposableBase, IReadOnlyList<T>
/// <summary>
/// A a thread-safe list for caching the results of an enumerable.
/// Note: should be disposed manually whenever possible as the locking mechanism is a ReaderWriterLockSlim.
/// </summary>
public class LazyList<T> : LazyListUnsafe<T>
{
List<T> _cached;
IEnumerator<T> _enumerator;

ReaderWriterLockSlim Sync;
int _safeCount;

/// <summary>
/// A value indicating whether the results are known or expected to be finite.
/// A list that was constructed as endless but has reached the end of the results will return false.
/// </summary>
public bool IsEndless { get; private set; }
public LazyList(IEnumerable<T> source, bool isEndless = false)

public LazyList(IEnumerable<T> source, bool isEndless = false) : base(source)
{
_enumerator = source.GetEnumerator();
_cached = new List<T>();
Sync = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); // This is important as it's possible to recurse infinitely to generate a result. :(
IsEndless = isEndless; // To indicate if a source is not allowed to fully enumerate.
}

protected override void OnDispose()
{
using (Sync.WriteLock())
{
DisposeOf(ref _enumerator);
Nullify(ref _cached)?.Clear();
}
using (Sync.WriteLock()) base.OnDispose();

DisposeOf(ref Sync);
}

public T this[int index]
{
get
{
AssertIsAlive();

if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index), "Cannot be less than zero.");
if (!EnsureIndex(index))
throw new ArgumentOutOfRangeException(nameof(index), "Greater than total count.");

return _cached[index];
}
}
private const string MARKED_ENDLESS = "This list is marked as endless and may never complete.";

private int _safeCount;
public int Count
/// <inheritdoc/>
public override int IndexOf(T item)
{
get
{
AssertIsAlive();
Finish();
return _cached.Count;
}
}
const string MESSAGE = MARKED_ENDLESS+" Use an enumerator, then Take(x).IndexOf().";
if (IsEndless) throw new InvalidOperationException(MESSAGE);

public bool TryGetValueAt(int index, out T value)
{
if (EnsureIndex(index))
{
value = _cached[index];
return true;
}

value = default!;
return false;
}

public IEnumerator<T> GetEnumerator()
{
AssertIsAlive();

var index = -1;
// Interlocked allows for multi-threaded access to this enumerator.
while (TryGetValueAt(Interlocked.Increment(ref index), out var value))
yield return value;
}

public int IndexOf(T item)
{
AssertIsAlive();
if (IsEndless)
throw new InvalidOperationException("This list is marked as endless and may never complete. Use an enumerator, then Take(x).IndexOf().");

var index = 0;
while (EnsureIndex(index))
{
var value = _cached[index];
if (value is null)
{
if (item is null) return index;
}
else if (value.Equals(item))
return index;

index++;
}

return -1;

}

public bool Contains(T item)
{
AssertIsAlive();
return IndexOf(item) != -1;
}

public void CopyTo(T[] array, int arrayIndex = 0)
{
AssertIsAlive();
var len = Math.Min(IsEndless ? int.MaxValue : Count, array.Length - arrayIndex);
for (var i = 0; i < len; i++)
array[i + arrayIndex] = this[i];
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
return base.IndexOf(item);
}

private bool EnsureIndex(int maxIndex)
protected override bool EnsureIndex(int maxIndex)
{
if (maxIndex < _safeCount)
return true;
Expand Down Expand Up @@ -186,11 +107,11 @@ private bool EnsureIndex(int maxIndex)
return false;
}

private void Finish()
protected override void Finish()
{
if (IsEndless)
throw new InvalidOperationException("This list is marked as endless and may never complete.");
while (EnsureIndex(int.MaxValue)) { }
throw new InvalidOperationException(MARKED_ENDLESS);
base.Finish();
}

}
Expand Down
Loading

0 comments on commit 2a749e8

Please sign in to comment.