Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,30 @@ public async Task DisposingAMemoizedBufferDoesNotDisposeOriginalBuffer()
}

[Fact]
public async Task MemoizingAMemoizedBufferTwiceReturnsTheOriginalObject()
public async Task DisposingAMemoizedBorrowedBufferDoesNotDisposeOriginalBorrowedBuffer()
{
var source = AsyncEnumerateOnce.Create(Enumerable.Empty<int>());
await using var memoized = source.Memoize();
await using var memoizedBuffer = memoized.Memoize();
await using var memoizedBuffer2 = memoizedBuffer.Memoize();
Assert.Same(memoizedBuffer, memoizedBuffer2);
var source = AsyncEnumerateOnce.Create<int>([]);
await using var firstMemoization = source.Memoize();
await using var borrowedBuffer = firstMemoization.Memoize();

await using (borrowedBuffer.Memoize())
{
}

await borrowedBuffer.ForEachAsync(NoOperation<int>);
}

/// <summary>This test disallows "re-borrowing" i.e. creating a fresh BorrowedBuffer over the original buffer.</summary>
[Fact]
public async Task UsagesOfSecondBorrowThrowAfterFirstBorrowIsDisposed()
{
var source = AsyncEnumerateOnce.Create<int>([]);
await using var firstMemoization = source.Memoize();
await using var firstBorrow = firstMemoization.Memoize();
await using var secondBorrow = firstBorrow.Memoize();
#pragma warning disable IDISP017
await firstBorrow.DisposeAsync();
#pragma warning restore IDISP017
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await secondBorrow.ForEachAsync(NoOperation<int>));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static IAsyncBuffer<TSource> Memoize<TSource>(this IAsyncEnumerable<TSour
: MemoizedAsyncBuffer.Create(source);

private static IAsyncBuffer<TSource> Borrow<TSource>(IAsyncBuffer<TSource> buffer)
=> buffer as BorrowedAsyncBuffer<TSource> ?? new BorrowedAsyncBuffer<TSource>(buffer);
=> new BorrowedAsyncBuffer<TSource>(buffer);

private static class MemoizedAsyncBuffer
{
Expand Down
44 changes: 39 additions & 5 deletions Funcky.Test/Extensions/EnumerableExtensions/MemoizeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void TheUnderlyingEnumerableIsOnlyEnumeratedOnce()
[Fact]
public void MemoizingAnEmptyListIsEmpty()
{
var empty = Enumerable.Empty<string>();
var empty = Enumerable.Empty<string>().PreventLinqOptimizations();
using var memoized = empty.Memoize();

Assert.Empty(memoized);
Expand Down Expand Up @@ -80,12 +80,46 @@ public void DisposingAMemoizedBufferDoesNotDisposeOriginalBuffer()
}

[Fact]
public void MemoizingAMemoizedBufferTwiceReturnsTheOriginalObject()
public void DisposingAMemoizedBorrowedBufferDoesNotDisposeOriginalBorrowedBuffer()
{
var source = EnumerateOnce.Create<int>([]);
using var firstMemoization = source.Memoize();
using var borrowedBuffer = firstMemoization.Memoize();

using (borrowedBuffer.Memoize())
{
}

borrowedBuffer.ForEach(NoOperation);
}

/// <summary>This test disallows "re-borrowing" i.e. creating a fresh BorrowedBuffer over the original buffer.</summary>
[Fact]
public void UsagesOfSecondBorrowThrowAfterFirstBorrowIsDisposed()
{
var source = EnumerateOnce.Create<int>([]);
using var firstMemoization = source.Memoize();
using var firstBorrow = firstMemoization.Memoize();
using var secondBorrow = firstBorrow.Memoize();
#pragma warning disable IDISP017
firstBorrow.Dispose();
#pragma warning restore IDISP017
Assert.Throws<ObjectDisposedException>(() => secondBorrow.ForEach(NoOperation));
}

[Fact]
public void MemoizingAListReturnsAnObjectImplementingIList()
{
var source = new List<int> { 10, 20, 30 };
using var memoized = source.Memoize();
Assert.IsType<IList<int>>(memoized, exactMatch: false);
}

[Fact]
public void MemoizingACollectionReturnsAnObjectImplementingICollection()
{
var source = new HashSet<int> { 10, 20, 30 };
using var memoized = source.Memoize();
using var memoizedBuffer = memoized.Memoize();
using var memoizedBuffer2 = memoizedBuffer.Memoize();
Assert.Same(memoizedBuffer, memoizedBuffer2);
Assert.IsType<ICollection<int>>(memoized, exactMatch: false);
}
}
84 changes: 84 additions & 0 deletions Funcky/Buffers/CollectionBuffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;

namespace Funcky.Buffers;

internal static class CollectionBuffer
{
public static CollectionBuffer<T> Create<T>(ICollection<T> list) => new(list);
}

[SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP025:Class with no virtual dispose method should be sealed", Justification = "Dispose doesn't do anything except flag object as disposed")]
internal class CollectionBuffer<T>(ICollection<T> source) : IBuffer<T>, ICollection<T>
{
private bool _disposed;

public int Count
{
get
{
ThrowIfDisposed();
return source.Count;
}
}

public bool IsReadOnly
{
get
{
ThrowIfDisposed();
return source.IsReadOnly;
}
}

public void Dispose()
{
_disposed = true;
}

public IEnumerator<T> GetEnumerator()
{
ThrowIfDisposed();
return source.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

public void Add(T item)
{
ThrowIfDisposed();
source.Add(item);
}

public void Clear()
{
ThrowIfDisposed();
source.Clear();
}

public bool Contains(T item)
{
ThrowIfDisposed();
return source.Contains(item);
}

public void CopyTo(T[] array, int arrayIndex)
{
ThrowIfDisposed();
source.CopyTo(array, arrayIndex);
}

public bool Remove(T item)
{
ThrowIfDisposed();
return source.Remove(item);
}

protected void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ListBuffer<T>));
}
}
}
42 changes: 42 additions & 0 deletions Funcky/Buffers/ListBuffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Funcky.Buffers;

internal static class ListBuffer
{
public static ListBuffer<T> Create<T>(IList<T> list) => new(list);
}

internal sealed class ListBuffer<T>(IList<T> source) : CollectionBuffer<T>(source), IList<T>
{
public T this[int index]
{
get
{
ThrowIfDisposed();
return source[index];
}

set
{
ThrowIfDisposed();
source[index] = value;
}
}

public int IndexOf(T item)
{
ThrowIfDisposed();
return source.IndexOf(item);
}

public void Insert(int index, T item)
{
ThrowIfDisposed();
source.Insert(index, item);
}

public void RemoveAt(int index)
{
ThrowIfDisposed();
source.RemoveAt(index);
}
}
16 changes: 9 additions & 7 deletions Funcky/Extensions/EnumerableExtensions/Memoize.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#pragma warning disable SA1010 // StyleCop support for collection expressions is missing
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using Funcky.Buffers;

namespace Funcky.Extensions;

Expand All @@ -15,13 +14,16 @@ public static partial class EnumerableExtensions
/// <returns>A lazy buffer of the underlying sequence.</returns>
[Pure]
public static IBuffer<TSource> Memoize<TSource>(this IEnumerable<TSource> source)
=> source is IBuffer<TSource> buffer
? Borrow(buffer)
: MemoizedBuffer.Create(source);
=> source switch
{
IBuffer<TSource> buffer => Borrow(buffer),
IList<TSource> list => ListBuffer.Create(list),
ICollection<TSource> list => CollectionBuffer.Create(list),
_ => MemoizedBuffer.Create(source),
};

[SuppressMessage("IDisposableAnalyzers", "IDISP015: Member should not return created and cached instance.", Justification = "False positive.")]
private static IBuffer<TSource> Borrow<TSource>(IBuffer<TSource> buffer)
=> buffer as BorrowedBuffer<TSource> ?? new BorrowedBuffer<TSource>(buffer);
=> new BorrowedBuffer<TSource>(buffer);

private static class MemoizedBuffer
{
Expand Down