diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/MetadataCollectionTests.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/MetadataCollectionTests.cs index e09d4f9f9f0..ca931b3bc9d 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/MetadataCollectionTests.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/MetadataCollectionTests.cs @@ -62,7 +62,7 @@ public void CreateAndCompareCollections(int size) [InlineData(64)] public void EnumeratorReturnsAllItemsInCollection(int size) { - var pairs = new List>(); + var pairs = new KeyValuePair[size]; var map = new Dictionary(); for (var i = 0; i < size; i++) @@ -70,7 +70,7 @@ public void EnumeratorReturnsAllItemsInCollection(int size) var key = i.ToString(CultureInfo.InvariantCulture); var value = (int.MaxValue - i).ToString(CultureInfo.InvariantCulture); - pairs.Add(new(key, value)); + pairs[i] = new(key, value); map.Add(key, false); } @@ -196,8 +196,8 @@ static void Swap(ref KeyValuePair pair1, ref KeyValuePair> pairs1, ImmutableArray> pairs2) { - var collection1 = MetadataCollection.Create(pairs1); - var collection2 = MetadataCollection.Create(pairs2); + var collection1 = MetadataCollection.Create(pairs1.AsSpan()); + var collection2 = MetadataCollection.Create(pairs2.AsSpan()); Assert.Equal(collection1, collection2); Assert.Equal(collection1.GetHashCode(), collection2.GetHashCode()); @@ -207,8 +207,8 @@ public void TestEquality_TwoItems(ImmutableArray> [MemberData(nameof(ThreePairs))] public void TestEquality_ThreeItems(ImmutableArray> pairs1, ImmutableArray> pairs2) { - var collection1 = MetadataCollection.Create(pairs1); - var collection2 = MetadataCollection.Create(pairs2); + var collection1 = MetadataCollection.Create(pairs1.AsSpan()); + var collection2 = MetadataCollection.Create(pairs2.AsSpan()); Assert.Equal(collection1, collection2); Assert.Equal(collection1.GetHashCode(), collection2.GetHashCode()); @@ -218,8 +218,8 @@ public void TestEquality_ThreeItems(ImmutableArray [MemberData(nameof(FourPairs))] public void TestEquality_FourItems(ImmutableArray> pairs1, ImmutableArray> pairs2) { - var collection1 = MetadataCollection.Create(pairs1); - var collection2 = MetadataCollection.Create(pairs2); + var collection1 = MetadataCollection.Create(pairs1.AsSpan()); + var collection2 = MetadataCollection.Create(pairs2.AsSpan()); Assert.Equal(collection1, collection2); Assert.Equal(collection1.GetHashCode(), collection2.GetHashCode()); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/ComponentTagHelperDescriptorProvider.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/ComponentTagHelperDescriptorProvider.cs index ea8f3ff61bd..14cc7b75ec1 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/ComponentTagHelperDescriptorProvider.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/CSharp/ComponentTagHelperDescriptorProvider.cs @@ -398,7 +398,7 @@ private static void CreateTypeParameterProperty(TagHelperDescriptorBuilder build pb.Name = typeParameter.Name; pb.TypeName = typeof(Type).FullName; - using var _ = ListPool>.GetPooledObject(out var metadataPairs); + using var metadataPairs = PooledSpanBuilder>.Empty; metadataPairs.Add(PropertyName(typeParameter.Name)); metadataPairs.Add(MakeTrue(ComponentMetadata.Component.TypeParameterKey)); metadataPairs.Add(new(ComponentMetadata.Component.TypeParameterIsCascadingKey, cascade.ToString())); @@ -504,7 +504,7 @@ private static void CreateTypeParameterProperty(TagHelperDescriptorBuilder build metadataPairs.Add(new(ComponentMetadata.Component.TypeParameterWithAttributesKey, withAttributes.ToString())); } - pb.SetMetadata(MetadataCollection.Create(metadataPairs)); + pb.SetMetadata(MetadataCollection.Create(metadataPairs.AsSpan())); pb.SetDocumentation( DocumentationDescriptor.From( diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataBuilder.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataBuilder.cs index 70a6c07e5f5..a37cc3ee878 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataBuilder.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataBuilder.cs @@ -1,25 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.Extensions.ObjectPool; using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.Utilities; namespace Microsoft.AspNetCore.Razor.Language; +[NonCopyable] internal ref struct MetadataBuilder { - private readonly ObjectPool>> _pool; - private List>? _list; + private PooledSpanBuilder> _list; public MetadataBuilder() - : this(ListPool>.Default) - { - } - - public MetadataBuilder(ObjectPool>> pool) { - _pool = pool; + _list = PooledSpanBuilder>.Empty; } public void Dispose() @@ -28,32 +23,23 @@ public void Dispose() } public void Add(string key, string? value) - { - _list ??= _pool.Get(); - _list.Add(new(key, value)); - } + => _list.Add(new(key, value)); public void Add(KeyValuePair pair) - { - _list ??= _pool.Get(); - _list.Add(pair); - } + => Add(pair.Key, pair.Value); public MetadataCollection Build() { - var result = MetadataCollection.CreateOrEmpty(_list); + var result = MetadataCollection.CreateOrEmpty(_list.AsSpan()); - _list = null; + ClearAndFree(); return result; } public void ClearAndFree() { - if (_list is { } list) - { - _pool.Return(list); - _list = null; - } + _list.Dispose(); + _list = PooledSpanBuilder>.Empty; } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataCollection.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataCollection.cs index 9fb712ccbf0..915c5e2ff5d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataCollection.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/MetadataCollection.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -87,17 +88,9 @@ public static MetadataCollection Create(KeyValuePair pair1, Key => new OneToThreeItems(pair1.Key, pair1.Value, pair2.Key, pair2.Value, pair3.Key, pair3.Value); public static MetadataCollection Create(params KeyValuePair[] pairs) - => pairs switch - { - [] => Empty, - [var pair] => new OneToThreeItems(pair.Key, pair.Value), - [var pair1, var pair2] => new OneToThreeItems(pair1.Key, pair1.Value, pair2.Key, pair2.Value), - [var pair1, var pair2, var pair3] => new OneToThreeItems(pair1.Key, pair1.Value, pair2.Key, pair2.Value, pair3.Key, pair3.Value), - _ => new FourOrMoreItems(pairs), - }; + => Create(pairs.AsSpan()); - public static MetadataCollection Create(T pairs) - where T : IReadOnlyList> + public static MetadataCollection Create(ReadOnlySpan> pairs) => pairs switch { [] => Empty, @@ -160,18 +153,17 @@ public static MetadataCollection Create(Dictionary map) return new OneToThreeItems(pair1.Key, pair1.Value, pair2.Key, pair2.Value, pair3.Key, pair3.Value); } - // Finally, if there are four or more items, add the pairs to a list in order to construct + // Finally, if there are four or more items, add the pairs to an array in order to construct // a FourOrMoreItems instance. Note that the constructor will copy the key-value pairs and won't - // hold onto the list we're passing, so it's safe to use a pooled list. - using var _ = ListPool>.GetPooledObject(out var list); - list.SetCapacityIfLarger(count); + // hold onto the list we're passing, so it's safe to use a pooled array. + using var array = new PooledSpanBuilder>(count); foreach (var pair in map) { - list.Add(pair); + array.Add(pair); } - return Create(list); + return Create(array.AsSpan()); } public static MetadataCollection Create(IReadOnlyDictionary map) @@ -188,8 +180,7 @@ public static MetadataCollection Create(IReadOnlyDictionary map return Create(map.ToArray()); } - public static MetadataCollection CreateOrEmpty(T? pairs) - where T : IReadOnlyList> + public static MetadataCollection CreateOrEmpty(ReadOnlySpan> pairs) => pairs is { } realPairs ? Create(realPairs) : Empty; public static MetadataCollection CreateOrEmpty(Dictionary? map) @@ -533,14 +524,9 @@ private sealed class FourOrMoreItems : MetadataCollection private readonly int _count; - public FourOrMoreItems(IReadOnlyList> pairs) + public FourOrMoreItems(ReadOnlySpan> pairs) { - if (pairs is null) - { - throw new ArgumentNullException(nameof(pairs)); - } - - var count = pairs.Count; + var count = pairs.Length; // Create a sorted array of keys. var keys = new string[count]; diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.Test/PooledObjects/PooledSpanBuilderTests.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.Test/PooledObjects/PooledSpanBuilderTests.cs new file mode 100644 index 00000000000..91fc0f8026f --- /dev/null +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.Test/PooledObjects/PooledSpanBuilderTests.cs @@ -0,0 +1,141 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using Microsoft.AspNetCore.Razor.PooledObjects; + +namespace Microsoft.AspNetCore.Razor.Utilities.Shared.Test.PooledObjects; + +public class PooledSpanBuilderTests +{ + [Fact] + public void Create_FromSpan_CreatesWithElements() + { + var source = new[] { 1, 2, 3, 4 }; + using var builder = PooledSpanBuilder.Create(source); + + Assert.Equal(source.Length, builder.Count); + Assert.True(builder.Any()); + Assert.Equal(source, builder.AsSpan().ToArray()); + } + + [Fact] + public void Add_AddsElements() + { + using var builder = PooledSpanBuilder.Empty; + builder.Add(10); + builder.Add(20); + + Assert.Equal(2, builder.Count); + Assert.Equal([10, 20], builder.AsSpan().ToArray()); + } + + [Fact] + public void AddRange_AddsSpan() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 1, 2, 3 }); + + Assert.Equal([1, 2, 3], builder.AsSpan().ToArray()); + } + + [Fact] + public void Insert_InsertsAtIndex() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 1, 3 }); + builder.Insert(1, 2); + + Assert.Equal([1, 2, 3], builder.AsSpan().ToArray()); + } + + [Fact] + public void RemoveAt_RemovesElement() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 1, 2, 3 }); + builder.RemoveAt(1); + + Assert.Equal([1, 3], builder.AsSpan().ToArray()); + } + + [Fact] + public void Clear_ResetsCount() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 1, 2, 3 }); + builder.Clear(); + + Assert.Equal(0, builder.Count); + Assert.Empty(builder.AsSpan().ToArray()); + } + + [Fact] + public void ToImmutableAndClear_ReturnsImmutableAndClears() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 1, 2, 3 }); + var immutable = builder.ToImmutableAndClear(); + + Assert.Equal(new[] { 1, 2, 3 }, immutable); + Assert.Equal(0, builder.Count); + } + + [Fact] + public void PeekAndPop_WorkAsStack() + { + using var builder = PooledSpanBuilder.Empty; + builder.Push(1); + builder.Push(2); + + Assert.Equal(2, builder.Peek()); + Assert.Equal(2, builder.Pop()); + Assert.Equal(1, builder.Peek()); + } + + [Fact] + public void TryPop_Empty_ReturnsFalse() + { + using var builder = PooledSpanBuilder.Empty; + var result = builder.TryPop(out var value); + + Assert.False(result); + Assert.Equal(0, value); + } + + [Fact] + public void Any_All_First_Last_Single() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 1, 2, 3 }); + + Assert.True(builder.Any()); + Assert.True(builder.Any(x => x == 2)); + Assert.True(builder.All(x => x > 0)); + Assert.Equal(1, builder.First()); + Assert.Equal(3, builder.Last()); + Assert.Equal(2, builder.Single(x => x == 2)); + } + + [Fact] + public void ToArrayAndClear_ReturnsArrayAndClears() + { + using var builder = PooledSpanBuilder.Empty; + builder.AddRange(new[] { 5, 6, 7 }); + var arr = builder.ToArrayAndClear(); + + Assert.Equal([5, 6, 7], arr); + Assert.Equal(0, builder.Count); + } + + [Fact] + public void Dispose_ReturnsArrayToPool() + { + var builder = new PooledSpanBuilder(4); + builder.AddRange(new[] { 1, 2, 3, 4 }); + builder.Dispose(); + + Assert.Equal(0, builder.Count); + Assert.Equal(0, builder.Capacity); + } +} diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledSpanBuilder.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledSpanBuilder.cs new file mode 100644 index 00000000000..aa3abc06085 --- /dev/null +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledSpanBuilder.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +internal static class PooledSpanBuilder +{ + public static PooledSpanBuilder Create(ReadOnlySpan source) + { + var spanBuilder = new PooledSpanBuilder(source.Length); + spanBuilder.AddRange(source); + return spanBuilder; + } +} diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledSpanBuilder`1.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledSpanBuilder`1.cs new file mode 100644 index 00000000000..bb895a62042 --- /dev/null +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledSpanBuilder`1.cs @@ -0,0 +1,1345 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Razor.Utilities; + +namespace Microsoft.AspNetCore.Razor.PooledObjects; + +/// +/// Wraps a pooled array but doesn't allocate it until it's needed. Provides +/// access to the backing array as a . +/// Note: Disposal ensures the pooled array is returned to the pool. +/// +[NonCopyable] +[CollectionBuilder(typeof(PooledSpanBuilder), nameof(PooledSpanBuilder.Create))] +internal partial struct PooledSpanBuilder(int capacity) : IDisposable +{ + public static PooledSpanBuilder Empty => new(); + + /// + /// An array to be used as storage. + /// + private T[] _array = capacity > 0 ? ArrayPool.Shared.Rent(capacity) : []; + + /// + /// Number of items in this collection. + /// + private int _count = 0; + + public PooledSpanBuilder() + : this(capacity: 0) + { + } + + public void Dispose() + { + // Return _array to the pool if necessary. + if (_array.Length > 0) + { + ArrayPool.Shared.Return(_array, clearArray: true); + _array = []; + _count = 0; + } + } + + /// + /// Ensures the inner 's capacity is at least the specified value. + /// + /// + /// This should only be used by methods that will add to the inner . + /// + private void EnsureCapacity(int capacity) + { + if (_array.Length < capacity) + { + var newArray = ArrayPool.Shared.Rent(capacity); + Array.Copy(_array, 0, newArray, 0, Count); + ArrayPool.Shared.Return(_array); + _array = newArray; + } + } + + public readonly T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return _array[index]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + _array[index] = value; + } + } + + public T this[Index index] + { + readonly get => this[index.GetOffset(Count)]; + set => this[index.GetOffset(Count)] = value; + } + + public readonly int Count + => _count; + + public readonly int Capacity + => _array.Length; + + public void Add(T item) + => Insert(Count, item); + + public void AddRange(ImmutableArray items) + => InsertRange(Count, items); + + public void AddRange(ReadOnlySpan items) + => InsertRange(Count, items); + + public void AddRange(TList list) + where TList : struct, IReadOnlyList + => InsertRange(Count, list); + + public void AddRange(TList list, int startIndex, int count) + where TList : struct, IReadOnlyList + => InsertRange(Count, list, startIndex, count); + + public void AddRange(IEnumerable items) + => InsertRange(Count, items); + + public void Clear() + { + // Keep the array to avoid churn in the object pool. + Array.Clear(_array, 0, Count); + _count = 0; + } + + public readonly Span.Enumerator GetEnumerator() + => AsSpan().GetEnumerator(); + + public void Insert(int index, T item) + => InsertSpan(index, [item]); + + public void InsertRange(int index, ImmutableArray items) + => InsertSpan(index, ImmutableCollectionsMarshal.AsArray(items).AsSpan()); + + public void InsertRange(int index, ReadOnlySpan items) + => InsertSpan(index, items); + + private void InsertSpan(int index, ReadOnlySpan items) + { + Debug.Assert(index >= 0 && index <= Count); + + var count = items.Length; + if (count == 0) + { + return; + } + + var newCount = Count + count; + EnsureCapacity(newCount); + + if (index != Count) + { + Array.Copy(_array, index, _array, index + count, Count - index); + } + + items.CopyTo(_array.AsSpan(index)); + _count = newCount; + } + + public void InsertRange(int index, TList list) + where TList : struct, IReadOnlyList + => InsertRange(index, list, startIndex: 0, list.Count); + + public void InsertRange(int index, TList list, int startIndex, int count) + where TList : struct, IReadOnlyList + { + if (count == 0) + { + return; + } + + var newCount = Count + count; + EnsureCapacity(newCount); + + if (startIndex != Count) + { + Array.Copy(_array, index, _array, index + count, Count - index); + } + + list.CopyTo(_array.AsSpan(index)); + _count = newCount; + } + + public void InsertRange(int index, IEnumerable items) + { + if (!items.TryGetCount(out var count)) + { + // We couldn't retrieve a count, so we have to enumerate the elements. + foreach (var item in items) + { + Insert(index++, item); + } + + return; + } + + if (count == 0) + { + // No items, so there's nothing to do. + return; + } + + var newCount = Count + count; + EnsureCapacity(newCount); + + if (index != Count) + { + Array.Copy(_array, index, _array, index + count, Count - index); + } + + items.CopyTo(_array.AsSpan(index)); + _count = newCount; + } + + public void RemoveAt(int index) + { + Array.Copy(_array, index + 1, _array, index, _array.Length - index - 1); + _count--; + } + + public void RemoveAt(Index index) + => RemoveAt(index.GetOffset(Count)); + + /// + /// Returns the current contents as an and changes + /// the collection to zero length. + /// + public ImmutableArray ToImmutableAndClear() + { + var newArray = ToImmutable(); + + Clear(); + + return newArray; + } + + public readonly ImmutableArray ToImmutable() + => AsSpan().ToImmutableArray(); + + /// + /// Returns a span representing the active portion of the underlying array. + /// + /// The returned span should not be used after disposal. + public readonly Span AsSpan() + => _array.AsSpan(0, Count); + + public readonly T[] ToArray() + => AsSpan().ToArray(); + + public T[] ToArrayAndClear() + { + var result = ToArray(); + + Clear(); + + return result; + } + + public void Push(T item) + => Add(item); + + public readonly T Peek() + => this[^1]; + + public T Pop() + { + var index = ^1; + var item = this[index]; + RemoveAt(index); + + return item; + } + + public bool TryPop([MaybeNullWhen(false)] out T item) + { + if (Count == 0) + { + item = default; + return false; + } + + item = Pop(); + return true; + } + + /// + /// Determines whether this builder contains any elements. + /// + /// + /// if this builder contains any elements; otherwise, . + /// + public readonly bool Any() + => Count > 0; + + /// + /// Determines whether any element in this builder satisfies a condition. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// if this builder is not empty and at least one of its elements passes + /// the test in the specified predicate; otherwise, . + /// + public readonly bool Any(Func predicate) + { + foreach (var item in this) + { + if (predicate(item)) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether any element in this builder satisfies a condition. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// if this builder is not empty and at least one of its elements passes + /// the test in the specified predicate; otherwise, . + /// + public readonly bool Any(TArg arg, Func predicate) + { + foreach (var item in this) + { + if (predicate(item, arg)) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether all elements in this builder satisfy a condition. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// if every element in this builder passes the test + /// in the specified predicate, or if the builder is empty; otherwise, + /// . + public readonly bool All(Func predicate) + { + foreach (var item in this) + { + if (!predicate(item)) + { + return false; + } + } + + return true; + } + + /// + /// Determines whether all elements in this builder satisfy a condition. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// if every element in this builder passes the test + /// in the specified predicate, or if the builder is empty; otherwise, + /// . + public readonly bool All(TArg arg, Func predicate) + { + foreach (var item in this) + { + if (!predicate(item, arg)) + { + return false; + } + } + + return true; + } + + /// + /// Returns the first element in this builder. + /// + /// + /// The first element in this builder. + /// + /// + /// The builder is empty. + /// + public readonly T First() + => Count > 0 ? this[0] : ThrowInvalidOperation(SR.Contains_no_elements); + + /// + /// Returns the first element in this builder that satisfies a specified condition. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The first element in this builder that passes the test in the specified predicate function. + /// + /// + /// No element satisfies the condition in . + /// + public readonly T First(Func predicate) + { + foreach (var item in this) + { + if (predicate(item)) + { + return item; + } + } + + return ThrowInvalidOperation(SR.Contains_no_matching_elements); + } + + /// + /// Returns the first element in this builder that satisfies a specified condition. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The first element in this builder that passes the test in the specified predicate function. + /// + /// + /// No element satisfies the condition in . + /// + public readonly T First(TArg arg, Func predicate) + { + foreach (var item in this) + { + if (predicate(item, arg)) + { + return item; + } + } + + return ThrowInvalidOperation(SR.Contains_no_matching_elements); + } + + /// + /// Returns the first element in this builder, or a default value if the builder is empty. + /// + /// + /// () if this builder is empty; otherwise, + /// the first element in this builder. + /// + public readonly T? FirstOrDefault() + => Count > 0 ? this[0] : default; + + /// + /// Returns the first element in this builder, or a specified default value if the builder is empty. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// if this builder is empty; otherwise, + /// the first element in this builder. + /// + public readonly T FirstOrDefault(T defaultValue) + => Count > 0 ? this[0] : defaultValue; + + /// + /// Returns the first element in this builder that satisfies a condition, or a default value + /// if no such element is found. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// () if this builder is empty or if no element + /// passes the test specified by ; otherwise, the first element in this + /// builder that passes the test specified by . + /// + public readonly T? FirstOrDefault(Func predicate) + { + foreach (var item in this) + { + if (predicate(item)) + { + return item; + } + } + + return default; + } + + /// + /// Returns the first element in this builder that satisfies a condition, or a specified default value + /// if no such element is found. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// if this builder is empty or if no element + /// passes the test specified by ; otherwise, the first element in this + /// builder that passes the test specified by . + /// + public readonly T FirstOrDefault(Func predicate, T defaultValue) + { + foreach (var item in this) + { + if (predicate(item)) + { + return item; + } + } + + return defaultValue; + } + + /// + /// Returns the first element in this builder that satisfies a condition, or a default value + /// if no such element is found. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// () if this builder is empty or if no element + /// passes the test specified by ; otherwise, the first element in this + /// builder that passes the test specified by . + /// + public readonly T? FirstOrDefault(TArg arg, Func predicate) + { + foreach (var item in this) + { + if (predicate(item, arg)) + { + return item; + } + } + + return default; + } + + /// + /// Returns the first element in this builder that satisfies a condition, or a default value + /// if no such element is found. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// if this builder is empty or if no element + /// passes the test specified by ; otherwise, the first element in this + /// builder that passes the test specified by . + /// + public readonly T FirstOrDefault(TArg arg, Func predicate, T defaultValue) + { + foreach (var item in this) + { + if (predicate(item, arg)) + { + return item; + } + } + + return defaultValue; + } + + /// + /// Returns the last element in this builder. + /// + /// + /// The value at the last position in this builder. + /// + /// + /// The builder is empty. + /// + public readonly T Last() + => Count > 0 ? this[^1] : ThrowInvalidOperation(SR.Contains_no_elements); + + /// + /// Returns the last element in this builder that satisfies a specified condition. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The last element in this builder that passes the test in the specified predicate function. + /// + /// + /// No element satisfies the condition in . + /// + public readonly T Last(Func predicate) + { + for (var i = Count - 1; i >= 0; i--) + { + var item = this[i]; + if (predicate(item)) + { + return item; + } + } + + return ThrowInvalidOperation(SR.Contains_no_matching_elements); + } + + /// + /// Returns the last element in this builder that satisfies a specified condition. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The last element in this builder that passes the test in the specified predicate function. + /// + /// + /// No element satisfies the condition in . + /// + public readonly T Last(TArg arg, Func predicate) + { + for (var i = Count - 1; i >= 0; i--) + { + var item = this[i]; + if (predicate(item, arg)) + { + return item; + } + } + + return ThrowInvalidOperation(SR.Contains_no_matching_elements); + } + + /// + /// Returns the last element in this builder, or a default value if the builder is empty. + /// + /// + /// () if this builder is empty; otherwise, + /// the last element in this builder. + /// + public readonly T? LastOrDefault() + => Count > 0 ? this[^1] : default; + + /// + /// Returns the last element in this builder, or a specified default value if the builder is empty. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// if this builder is empty; otherwise, + /// the last element in this builder. + /// + public readonly T LastOrDefault(T defaultValue) + => Count > 0 ? this[^1] : defaultValue; + + /// + /// Returns the last element in this builder that satisfies a condition, or a default value + /// if no such element is found. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// () if this builder is empty or if no element + /// passes the test specified by ; otherwise, the last element in this + /// builder that passes the test specified by . + /// + public readonly T? LastOrDefault(Func predicate) + { + for (var i = Count - 1; i >= 0; i--) + { + var item = this[i]; + if (predicate(item)) + { + return item; + } + } + + return default; + } + + /// + /// Returns the last element in this builder that satisfies a condition, or a specified default value + /// if no such element is found. + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// if this builder is empty or if no element + /// passes the test specified by ; otherwise, the last element in this + /// builder that passes the test specified by . + /// + public readonly T LastOrDefault(Func predicate, T defaultValue) + { + for (var i = Count - 1; i >= 0; i--) + { + var item = this[i]; + if (predicate(item)) + { + return item; + } + } + + return defaultValue; + } + + /// + /// Returns the last element in this builder that satisfies a condition, or a default value + /// if no such element is found. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// () if this builder is empty or if no element + /// passes the test specified by ; otherwise, the last element in this + /// builder that passes the test specified by . + /// + public readonly T? LastOrDefault(TArg arg, Func predicate) + { + for (var i = Count - 1; i >= 0; i--) + { + var item = this[i]; + if (predicate(item, arg)) + { + return item; + } + } + + return default; + } + + /// + /// Returns the last element in this builder that satisfies a condition, or a default value + /// if no such element is found. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test each element for a condition. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// if this builder is empty or if no element + /// passes the test specified by ; otherwise, the last element in this + /// builder that passes the test specified by . + /// + public readonly T LastOrDefault(TArg arg, Func predicate, T defaultValue) + { + for (var i = Count - 1; i >= 0; i--) + { + var item = this[i]; + if (predicate(item, arg)) + { + return item; + } + } + + return defaultValue; + } + + /// + /// Returns the only element in this builder, and throws an exception if there is not exactly one element. + /// + /// + /// The single element in this builder. + /// + /// + /// The builder is empty. + /// + /// + /// The builder contains more than one element. + /// + public readonly T Single() + { + return Count switch + { + 1 => this[0], + 0 => ThrowInvalidOperation(SR.Contains_no_elements), + _ => ThrowInvalidOperation(SR.Contains_more_than_one_element) + }; + } + + /// + /// Returns the only element in this builder that satisfies a specified condition, + /// and throws an exception if more than one such element exists. + /// + /// + /// A function to test an element for a condition. + /// + /// + /// The single element in this builder that satisfies a condition. + /// + /// + /// No element satisfies the condition in . + /// + /// + /// More than one element satisfies the condition in . + /// + public readonly T Single(Func predicate) + { + var firstSeen = false; + T? result = default; + + foreach (var item in this) + { + if (predicate(item)) + { + if (firstSeen) + { + return ThrowInvalidOperation(SR.Contains_more_than_one_matching_element); + } + + firstSeen = true; + result = item; + } + } + + if (!firstSeen) + { + return ThrowInvalidOperation(SR.Contains_no_matching_elements); + } + + return result!; + } + + /// + /// Returns the only element in this builder that satisfies a specified condition, + /// and throws an exception if more than one such element exists. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test an element for a condition. + /// + /// + /// The single element in this builder that satisfies a condition. + /// + /// + /// No element satisfies the condition in . + /// + /// + /// More than one element satisfies the condition in . + /// + public readonly T Single(TArg arg, Func predicate) + { + var firstSeen = false; + T? result = default; + + foreach (var item in this) + { + if (predicate(item, arg)) + { + if (firstSeen) + { + return ThrowInvalidOperation(SR.Contains_more_than_one_matching_element); + } + + firstSeen = true; + result = item; + } + } + + if (!firstSeen) + { + return ThrowInvalidOperation(SR.Contains_no_matching_elements); + } + + return result!; + } + + /// + /// Returns the only element in this builder, or a default value if the builder is empty; + /// this method throws an exception if there is more than one element in the builder. + /// + /// + /// The single element in this builder, or () + /// if this builder contains no elements. + /// + /// + /// The builder contains more than one element. + /// + public readonly T? SingleOrDefault() + { + return Count switch + { + 1 => this[0], + 0 => default, + _ => ThrowInvalidOperation(SR.Contains_more_than_one_element) + }; + } + + /// + /// Returns the only element in this builder, or a specified default value if the builder is empty; + /// this method throws an exception if there is more than one element in the builder. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// The single element in this builder, or + /// if this builder contains no elements. + /// + /// + /// The builder contains more than one element. + /// + public readonly T SingleOrDefault(T defaultValue) + { + return Count switch + { + 1 => this[0], + 0 => defaultValue, + _ => ThrowInvalidOperation(SR.Contains_more_than_one_element) + }; + } + + /// + /// Returns the only element in this builder that satisfies a specified condition or a default + /// value if no such element exists; this method throws an exception if more than one element + /// satisfies the condition. + /// + /// + /// A function to test an element for a condition. + /// + /// + /// The single element in this builder that satisfies the condition, or + /// () if no such element is found. + /// + /// + /// More than one element satisfies the condition in predicate. + /// + public readonly T? SingleOrDefault(Func predicate) + { + var firstSeen = false; + T? result = default; + + foreach (var item in this) + { + if (predicate(item)) + { + if (firstSeen) + { + return ThrowInvalidOperation(SR.Contains_more_than_one_matching_element); + } + + firstSeen = true; + result = item; + } + } + + return result; + } + + /// + /// Returns the only element in this builder that satisfies a specified condition or a specified default + /// value if no such element exists; this method throws an exception if more than one element + /// satisfies the condition. + /// + /// + /// A function to test an element for a condition. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// The single element in this builder that satisfies the condition, or + /// if no such element is found. + /// + /// + /// More than one element satisfies the condition in predicate. + /// + public readonly T SingleOrDefault(Func predicate, T defaultValue) + { + var firstSeen = false; + var result = defaultValue; + + foreach (var item in this) + { + if (predicate(item)) + { + if (firstSeen) + { + return ThrowInvalidOperation(SR.Contains_more_than_one_matching_element); + } + + firstSeen = true; + result = item; + } + } + + return result; + } + + /// + /// Returns the only element in this builder that satisfies a specified condition or a default + /// value if no such element exists; this method throws an exception if more than one element + /// satisfies the condition. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test an element for a condition. + /// + /// + /// The single element in this builder that satisfies the condition, or + /// () if no such element is found. + /// + /// + /// More than one element satisfies the condition in predicate. + /// + public readonly T? SingleOrDefault(TArg arg, Func predicate) + { + var firstSeen = false; + T? result = default; + + foreach (var item in this) + { + if (predicate(item, arg)) + { + if (firstSeen) + { + return ThrowInvalidOperation(SR.Contains_more_than_one_matching_element); + } + + firstSeen = true; + result = item; + } + } + + return result; + } + + /// + /// Returns the only element in this builder that satisfies a specified condition or a specified default + /// value if no such element exists; this method throws an exception if more than one element + /// satisfies the condition. + /// + /// + /// An argument to pass to . + /// + /// + /// A function to test an element for a condition. + /// + /// + /// The default value to return if this builder is empty. + /// + /// + /// The single element in this builder that satisfies the condition, or + /// if no such element is found. + /// + /// + /// More than one element satisfies the condition in predicate. + /// + public readonly T SingleOrDefault(TArg arg, Func predicate, T defaultValue) + { + var firstSeen = false; + var result = defaultValue; + + foreach (var item in this) + { + if (predicate(item, arg)) + { + if (firstSeen) + { + return ThrowInvalidOperation(SR.Contains_more_than_one_matching_element); + } + + firstSeen = true; + result = item; + } + } + + return result; + } + + /// + /// This is present to help the JIT inline methods that need to throw an . + /// + [DoesNotReturn] + private static T ThrowInvalidOperation(string message) + => ThrowHelper.ThrowInvalidOperationException(message); + + /// + /// Sorts the contents of this builder. + /// + public readonly void Sort() + => Array.Sort(_array); + + /// + /// Sorts the contents of this array using the provided . + /// + public readonly void Sort(IComparer comparer) + => Array.Sort(_array, comparer); + + /// + /// Sorts the contents of this array using the provided . + /// + public readonly void Sort(Comparison comparison) + => Array.Sort(_array, comparison); + + public readonly ImmutableArray ToImmutableOrdered() + { + var result = ToImmutable(); + result.Unsafe().Order(); + + return result; + } + + public readonly ImmutableArray ToImmutableOrdered(IComparer comparer) + { + var result = ToImmutable(); + result.Unsafe().Order(comparer); + + return result; + } + + public readonly ImmutableArray ToImmutableOrdered(Comparison comparison) + { + var result = ToImmutable(); + result.Unsafe().Order(comparison); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedDescending() + { + var result = ToImmutable(); + result.Unsafe().OrderDescending(); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedDescending(IComparer comparer) + { + var result = ToImmutable(); + result.Unsafe().OrderDescending(comparer); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedDescending(Comparison comparison) + { + var result = ToImmutable(); + result.Unsafe().OrderDescending(comparison); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedBy(Func keySelector) + { + var result = ToImmutable(); + result.Unsafe().OrderBy(keySelector); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedBy(Func keySelector, IComparer comparer) + { + var result = ToImmutable(); + result.Unsafe().OrderBy(keySelector, comparer); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedBy(Func keySelector, Comparison comparison) + { + var result = ToImmutable(); + result.Unsafe().OrderBy(keySelector, comparison); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedByDescending(Func keySelector) + { + var result = ToImmutable(); + result.Unsafe().OrderByDescending(keySelector); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedByDescending(Func keySelector, IComparer comparer) + { + var result = ToImmutable(); + result.Unsafe().OrderByDescending(keySelector, comparer); + + return result; + } + + public readonly ImmutableArray ToImmutableOrderedByDescending(Func keySelector, Comparison comparison) + { + var result = ToImmutable(); + result.Unsafe().OrderByDescending(keySelector, comparison); + + return result; + } + + public readonly ImmutableArray ToImmutableReversed() + { + var result = ToImmutable(); + result.Unsafe().Reverse(); + + return result; + } + + public ImmutableArray ToImmutableOrderedAndClear() + { + var result = ToImmutableAndClear(); + result.Unsafe().Order(); + + return result; + } + + public ImmutableArray ToImmutableOrderedAndClear(IComparer comparer) + { + var result = ToImmutableAndClear(); + result.Unsafe().Order(comparer); + + return result; + } + + public ImmutableArray ToImmutableOrderedAndClear(Comparison comparison) + { + var result = ToImmutableAndClear(); + result.Unsafe().Order(comparison); + + return result; + } + + public ImmutableArray ToImmutableOrderedDescendingAndClear() + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderDescending(); + + return result; + } + + public ImmutableArray ToImmutableOrderedDescendingAndClear(IComparer comparer) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderDescending(comparer); + + return result; + } + + public ImmutableArray ToImmutableOrderedDescendingAndClear(Comparison comparison) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderDescending(comparison); + + return result; + } + + public ImmutableArray ToImmutableOrderedByAndClear(Func keySelector) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderBy(keySelector); + + return result; + } + + public ImmutableArray ToImmutableOrderedByAndClear(Func keySelector, IComparer comparer) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderBy(keySelector, comparer); + + return result; + } + + public ImmutableArray ToImmutableOrderedByAndClear(Func keySelector, Comparison comparison) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderBy(keySelector, comparison); + + return result; + } + + public ImmutableArray ToImmutableOrderedByDescendingAndClear(Func keySelector) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderByDescending(keySelector); + + return result; + } + + public ImmutableArray ToImmutableOrderedByDescendingAndClear(Func keySelector, IComparer comparer) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderByDescending(keySelector, comparer); + + return result; + } + + public ImmutableArray ToImmutableOrderedByDescendingAndClear(Func keySelector, Comparison comparison) + { + var result = ToImmutableAndClear(); + result.Unsafe().OrderByDescending(keySelector, comparison); + + return result; + } + + public ImmutableArray ToImmutableReversedAndClear() + { + var result = ToImmutableAndClear(); + result.Unsafe().Reverse(); + + return result; + } +}