Skip to content

Commit 3e9ebca

Browse files
authored
Merge pull request #860 from polyadic/memoize-optimizations
Specialize Memoize for `IList` and `ICollection`
2 parents 5097016 + 486b183 commit 3e9ebca

File tree

6 files changed

+199
-19
lines changed

6 files changed

+199
-19
lines changed

Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/MemoizeTest.cs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,30 @@ public async Task DisposingAMemoizedBufferDoesNotDisposeOriginalBuffer()
8080
}
8181

8282
[Fact]
83-
public async Task MemoizingAMemoizedBufferTwiceReturnsTheOriginalObject()
83+
public async Task DisposingAMemoizedBorrowedBufferDoesNotDisposeOriginalBorrowedBuffer()
8484
{
85-
var source = AsyncEnumerateOnce.Create(Enumerable.Empty<int>());
86-
await using var memoized = source.Memoize();
87-
await using var memoizedBuffer = memoized.Memoize();
88-
await using var memoizedBuffer2 = memoizedBuffer.Memoize();
89-
Assert.Same(memoizedBuffer, memoizedBuffer2);
85+
var source = AsyncEnumerateOnce.Create<int>([]);
86+
await using var firstMemoization = source.Memoize();
87+
await using var borrowedBuffer = firstMemoization.Memoize();
88+
89+
await using (borrowedBuffer.Memoize())
90+
{
91+
}
92+
93+
await borrowedBuffer.ForEachAsync(NoOperation<int>);
94+
}
95+
96+
/// <summary>This test disallows "re-borrowing" i.e. creating a fresh BorrowedBuffer over the original buffer.</summary>
97+
[Fact]
98+
public async Task UsagesOfSecondBorrowThrowAfterFirstBorrowIsDisposed()
99+
{
100+
var source = AsyncEnumerateOnce.Create<int>([]);
101+
await using var firstMemoization = source.Memoize();
102+
await using var firstBorrow = firstMemoization.Memoize();
103+
await using var secondBorrow = firstBorrow.Memoize();
104+
#pragma warning disable IDISP017
105+
await firstBorrow.DisposeAsync();
106+
#pragma warning restore IDISP017
107+
await Assert.ThrowsAsync<ObjectDisposedException>(async () => await secondBorrow.ForEachAsync(NoOperation<int>));
90108
}
91109
}

Funcky.Async/Extensions/AsyncEnumerableExtensions/Memoize.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public static IAsyncBuffer<TSource> Memoize<TSource>(this IAsyncEnumerable<TSour
1717
: MemoizedAsyncBuffer.Create(source);
1818

1919
private static IAsyncBuffer<TSource> Borrow<TSource>(IAsyncBuffer<TSource> buffer)
20-
=> buffer as BorrowedAsyncBuffer<TSource> ?? new BorrowedAsyncBuffer<TSource>(buffer);
20+
=> new BorrowedAsyncBuffer<TSource>(buffer);
2121

2222
private static class MemoizedAsyncBuffer
2323
{

Funcky.Test/Extensions/EnumerableExtensions/MemoizeTest.cs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public void TheUnderlyingEnumerableIsOnlyEnumeratedOnce()
2828
[Fact]
2929
public void MemoizingAnEmptyListIsEmpty()
3030
{
31-
var empty = Enumerable.Empty<string>();
31+
var empty = Enumerable.Empty<string>().PreventLinqOptimizations();
3232
using var memoized = empty.Memoize();
3333

3434
Assert.Empty(memoized);
@@ -80,12 +80,46 @@ public void DisposingAMemoizedBufferDoesNotDisposeOriginalBuffer()
8080
}
8181

8282
[Fact]
83-
public void MemoizingAMemoizedBufferTwiceReturnsTheOriginalObject()
83+
public void DisposingAMemoizedBorrowedBufferDoesNotDisposeOriginalBorrowedBuffer()
8484
{
8585
var source = EnumerateOnce.Create<int>([]);
86+
using var firstMemoization = source.Memoize();
87+
using var borrowedBuffer = firstMemoization.Memoize();
88+
89+
using (borrowedBuffer.Memoize())
90+
{
91+
}
92+
93+
borrowedBuffer.ForEach(NoOperation);
94+
}
95+
96+
/// <summary>This test disallows "re-borrowing" i.e. creating a fresh BorrowedBuffer over the original buffer.</summary>
97+
[Fact]
98+
public void UsagesOfSecondBorrowThrowAfterFirstBorrowIsDisposed()
99+
{
100+
var source = EnumerateOnce.Create<int>([]);
101+
using var firstMemoization = source.Memoize();
102+
using var firstBorrow = firstMemoization.Memoize();
103+
using var secondBorrow = firstBorrow.Memoize();
104+
#pragma warning disable IDISP017
105+
firstBorrow.Dispose();
106+
#pragma warning restore IDISP017
107+
Assert.Throws<ObjectDisposedException>(() => secondBorrow.ForEach(NoOperation));
108+
}
109+
110+
[Fact]
111+
public void MemoizingAListReturnsAnObjectImplementingIList()
112+
{
113+
var source = new List<int> { 10, 20, 30 };
114+
using var memoized = source.Memoize();
115+
Assert.IsType<IList<int>>(memoized, exactMatch: false);
116+
}
117+
118+
[Fact]
119+
public void MemoizingACollectionReturnsAnObjectImplementingICollection()
120+
{
121+
var source = new HashSet<int> { 10, 20, 30 };
86122
using var memoized = source.Memoize();
87-
using var memoizedBuffer = memoized.Memoize();
88-
using var memoizedBuffer2 = memoizedBuffer.Memoize();
89-
Assert.Same(memoizedBuffer, memoizedBuffer2);
123+
Assert.IsType<ICollection<int>>(memoized, exactMatch: false);
90124
}
91125
}

Funcky/Buffers/CollectionBuffer.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Collections;
2+
using System.Diagnostics.CodeAnalysis;
3+
4+
namespace Funcky.Buffers;
5+
6+
internal static class CollectionBuffer
7+
{
8+
public static CollectionBuffer<T> Create<T>(ICollection<T> list) => new(list);
9+
}
10+
11+
[SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP025:Class with no virtual dispose method should be sealed", Justification = "Dispose doesn't do anything except flag object as disposed")]
12+
internal class CollectionBuffer<T>(ICollection<T> source) : IBuffer<T>, ICollection<T>
13+
{
14+
private bool _disposed;
15+
16+
public int Count
17+
{
18+
get
19+
{
20+
ThrowIfDisposed();
21+
return source.Count;
22+
}
23+
}
24+
25+
public bool IsReadOnly
26+
{
27+
get
28+
{
29+
ThrowIfDisposed();
30+
return source.IsReadOnly;
31+
}
32+
}
33+
34+
public void Dispose()
35+
{
36+
_disposed = true;
37+
}
38+
39+
public IEnumerator<T> GetEnumerator()
40+
{
41+
ThrowIfDisposed();
42+
return source.GetEnumerator();
43+
}
44+
45+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
46+
47+
public void Add(T item)
48+
{
49+
ThrowIfDisposed();
50+
source.Add(item);
51+
}
52+
53+
public void Clear()
54+
{
55+
ThrowIfDisposed();
56+
source.Clear();
57+
}
58+
59+
public bool Contains(T item)
60+
{
61+
ThrowIfDisposed();
62+
return source.Contains(item);
63+
}
64+
65+
public void CopyTo(T[] array, int arrayIndex)
66+
{
67+
ThrowIfDisposed();
68+
source.CopyTo(array, arrayIndex);
69+
}
70+
71+
public bool Remove(T item)
72+
{
73+
ThrowIfDisposed();
74+
return source.Remove(item);
75+
}
76+
77+
protected void ThrowIfDisposed()
78+
{
79+
if (_disposed)
80+
{
81+
throw new ObjectDisposedException(nameof(ListBuffer<T>));
82+
}
83+
}
84+
}

Funcky/Buffers/ListBuffer.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
namespace Funcky.Buffers;
2+
3+
internal static class ListBuffer
4+
{
5+
public static ListBuffer<T> Create<T>(IList<T> list) => new(list);
6+
}
7+
8+
internal sealed class ListBuffer<T>(IList<T> source) : CollectionBuffer<T>(source), IList<T>
9+
{
10+
public T this[int index]
11+
{
12+
get
13+
{
14+
ThrowIfDisposed();
15+
return source[index];
16+
}
17+
18+
set
19+
{
20+
ThrowIfDisposed();
21+
source[index] = value;
22+
}
23+
}
24+
25+
public int IndexOf(T item)
26+
{
27+
ThrowIfDisposed();
28+
return source.IndexOf(item);
29+
}
30+
31+
public void Insert(int index, T item)
32+
{
33+
ThrowIfDisposed();
34+
source.Insert(index, item);
35+
}
36+
37+
public void RemoveAt(int index)
38+
{
39+
ThrowIfDisposed();
40+
source.RemoveAt(index);
41+
}
42+
}

Funcky/Extensions/EnumerableExtensions/Memoize.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
#pragma warning disable SA1010 // StyleCop support for collection expressions is missing
21
using System.Collections;
3-
using System.Diagnostics.CodeAnalysis;
2+
using Funcky.Buffers;
43

54
namespace Funcky.Extensions;
65

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

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

2628
private static class MemoizedBuffer
2729
{

0 commit comments

Comments
 (0)