From 45b1c75b6a911fd6506b8973249374d42c621ba2 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 3 Dec 2023 16:51:46 -0500 Subject: [PATCH] Added `AsyncEnumerable.Repeat` API. --- Package/Core/Linq/Generators/Range.cs | 1 + Package/Core/Linq/Generators/Repeat.cs | 132 ++++++++++++++++++ Package/Core/Linq/Generators/Repeat.cs.meta | 11 ++ .../APIs/Linq/AsyncEnumerableTests.cs | 32 +++++ 4 files changed, 176 insertions(+) create mode 100644 Package/Core/Linq/Generators/Repeat.cs create mode 100644 Package/Core/Linq/Generators/Repeat.cs.meta diff --git a/Package/Core/Linq/Generators/Range.cs b/Package/Core/Linq/Generators/Range.cs index e407ee411..d459ac368 100644 --- a/Package/Core/Linq/Generators/Range.cs +++ b/Package/Core/Linq/Generators/Range.cs @@ -116,6 +116,7 @@ internal override Promise DisposeAsync(int id) protected override void DisposeAndReturnToPool() { Dispose(); + _disposed = true; ObjectPool.MaybeRepool(this); } diff --git a/Package/Core/Linq/Generators/Repeat.cs b/Package/Core/Linq/Generators/Repeat.cs new file mode 100644 index 000000000..a85045d67 --- /dev/null +++ b/Package/Core/Linq/Generators/Repeat.cs @@ -0,0 +1,132 @@ +#if PROTO_PROMISE_DEBUG_ENABLE || (!PROTO_PROMISE_DEBUG_DISABLE && DEBUG) +#define PROMISE_DEBUG +#else +#undef PROMISE_DEBUG +#endif + +#pragma warning disable IDE0090 // Use 'new(...)' + +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Proto.Promises.Linq +{ +#if CSHARP_7_3_OR_NEWER // We only expose AsyncEnumerable where custom async method builders are supported. + public static partial class AsyncEnumerable + { + /// + /// Generates an async-enumerable sequence that contains one repeated value. + /// + /// The type of the value to be repeated in the result async-enumerable sequence. + /// The value to be repeated. + /// The number of sequential integers to generate. + /// An async-enumerable sequence that contains a range of sequential integral numbers. + /// is less than zero. + public static AsyncEnumerable Repeat(T element, int count) + => AsyncEnumerable.Repeat(element, count); + } + + partial struct AsyncEnumerable + { + /// + /// Generates an async-enumerable sequence that contains one repeated value. + /// + /// The value to be repeated. + /// The number of sequential integers to generate. + /// An async-enumerable sequence that contains a range of sequential integral numbers. + /// is less than zero. + public static AsyncEnumerable Repeat(T element, int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "count is less than zero", Internal.GetFormattedStacktrace(1)); + } + + return count == 0 + ? Empty() +#if PROMISE_DEBUG + // In DEBUG mode we use the Create function so its proper use will be verified. + : Create((element, count), async (cv, writer, cancelationToken) => + { + unchecked + { + while (--cv.count > 0) + { + await writer.YieldAsync(cv.element); + } + } + }); +#else + : new AsyncEnumerable(Internal.AsyncEnumerableRepeat.GetOrCreate(element, count)); +#endif + } + } +#endif // CSHARP_7_3_OR_NEWER +} + +namespace Proto.Promises +{ +#if CSHARP_7_3_OR_NEWER && !PROMISE_DEBUG + partial class Internal + { + internal sealed class AsyncEnumerableRepeat : PromiseRefBase.AsyncEnumerableBase + { + private int _count; + + private AsyncEnumerableRepeat() { } + + [MethodImpl(InlineOption)] + private static AsyncEnumerableRepeat GetOrCreate() + { + var obj = ObjectPool.TryTakeOrInvalid>(); + return obj == InvalidAwaitSentinel.s_instance + ? new AsyncEnumerableRepeat() + : obj.UnsafeAs>(); + } + + [MethodImpl(InlineOption)] + internal static AsyncEnumerableRepeat GetOrCreate(T element, int count) + { + var enumerable = GetOrCreate(); + enumerable.Reset(); + enumerable._count = count; + enumerable._current = element; + return enumerable; + } + + internal override Promise MoveNextAsync(int id) + { + unchecked + { + return Promise.Resolved(--_count >= 0); + } + } + + internal override Promise DisposeAsync(int id) + { + if (Interlocked.CompareExchange(ref _enumerableId, id + 1, id) == id) + { + // This was not already disposed. +#if PROMISE_DEBUG || PROTO_PROMISE_DEVELOPER_MODE + SetCompletionState(null, Promise.State.Resolved); +#endif + DisposeAndReturnToPool(); + } + // IAsyncDisposable.DisposeAsync must not throw if it's called multiple times, according to MSDN documentation. + return Promise.Resolved(); + } + + protected override void DisposeAndReturnToPool() + { + Dispose(); + _current = default; + _disposed = true; + ObjectPool.MaybeRepool(this); + } + + protected override void Start(int enumerableId) { throw new System.InvalidOperationException(); } + internal override void MaybeDispose() { throw new System.InvalidOperationException(); } + } + } +#endif // CSHARP_7_3_OR_NEWER && !PROMISE_DEBUG +} \ No newline at end of file diff --git a/Package/Core/Linq/Generators/Repeat.cs.meta b/Package/Core/Linq/Generators/Repeat.cs.meta new file mode 100644 index 000000000..133ff804c --- /dev/null +++ b/Package/Core/Linq/Generators/Repeat.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2fc3303206a4b604fbac4f43ad60775d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Package/Tests/CoreTests/APIs/Linq/AsyncEnumerableTests.cs b/Package/Tests/CoreTests/APIs/Linq/AsyncEnumerableTests.cs index e3867d603..48b7a741b 100644 --- a/Package/Tests/CoreTests/APIs/Linq/AsyncEnumerableTests.cs +++ b/Package/Tests/CoreTests/APIs/Linq/AsyncEnumerableTests.cs @@ -516,6 +516,38 @@ public void AsyncEnumerableRange_Empty() Assert.AreEqual(0, count); } + + [Test] + public void AsyncEnumerableRepeat_BadArgs() + { + Assert.Catch(() => AsyncEnumerable.Repeat(0, -1)); + } + + [Test] + public void AsyncEnumerableRepeat_Empty() + { + int count = 0; + AsyncEnumerable.Repeat(2, 0) + .ForEachAsync(num => ++count) + .WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1)); + + Assert.AreEqual(0, count); + } + + [Test] + public void AsyncEnumerableRepeat_Count() + { + int count = 0; + AsyncEnumerable.Repeat(2, 5) + .ForEachAsync(num => + { + Assert.AreEqual(2, num); + ++count; + }) + .WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1)); + + Assert.AreEqual(5, count); + } } }