Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow collection expressions for 'System.Collections.ObjectModel' collection types #111093

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace System.Collections.ObjectModel.Tests
Expand All @@ -21,6 +22,24 @@ public void Ctor_SetProperty_Roundtrips()
Assert.Same(set, new DerivedReadOnlySet<int>(set).Set);
}

[Fact]
public static void Ctor_CollectionExpressions_Empty()
{
ReadOnlySet<int> set = [];
Assert.IsType<ReadOnlySet<int>>(set);
Assert.Empty(set);
Assert.Same(set, ReadOnlySet<int>.Empty);
}

[Fact]
public static void Ctor_CollectionExpressions()
{
int[] array = [1, 2, 3, 3, 2, 1];
ReadOnlySet<int> set = [.. array];
Assert.IsType<ReadOnlySet<int>>(set);
Assert.Equal(set.Order(), array.Distinct().Order());
}

[Fact]
public void Empty_EmptyAndIdempotent()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace System.Collections.ObjectModel
{
[Serializable]
[CollectionBuilder(typeof(ReadOnlyCollection), "CreateCollection")]
[DebuggerTypeProxy(typeof(ICollectionDebugView<>))]
[DebuggerDisplay("Count = {Count}")]
[TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
Expand Down Expand Up @@ -229,4 +230,34 @@ void IList.RemoveAt(int index)
ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ReadOnlyCollection);
}
}

/// <summary>
/// Provides static methods to create read-only collections and sets.
/// </summary>
public static class ReadOnlyCollection
AlexRadch marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Creates a new <see cref="ReadOnlyCollection{T}"/> from the specified span of values.
/// This method (simplifies collection initialization)[/dotnet/csharp/language-reference/operators/collection-expressions]
/// to create a new <see cref="ReadOnlyCollection{T}"/> with the specified values.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
/// <param name="values">The span of values to include in the collection.</param>
/// <returns>A new <see cref="ReadOnlyCollection{T}"/> containing the specified values.</returns>
[Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0301:Simplify collection initialization", Justification = "This method simplifies ReadOnlyCollection<T> initialization, so you cannot simplify collection initialization inside it.")]
AlexRadch marked this conversation as resolved.
Show resolved Hide resolved
public static ReadOnlyCollection<T> CreateCollection<T>(params ReadOnlySpan<T> values) =>
values.IsEmpty ? ReadOnlyCollection<T>.Empty : new ReadOnlyCollection<T>(values.ToArray());

/// <summary>
/// Creates a new <see cref="ReadOnlySet{T}"/> from the specified span of values.
/// This method (simplifies collection initialization)[/dotnet/csharp/language-reference/operators/collection-expressions]
/// to create a new <see cref="ReadOnlySet{T}"/> with the specified values.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
/// <param name="values">The span of values to include in the collection.</param>
/// <returns>A new <see cref="ReadOnlySet{T}"/> containing the specified values.</returns>
[Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0301:Simplify collection initialization", Justification = "This method simplifies ReadOnlySet<T> initialization, so you cannot simplify collection initialization inside it.")]
public static ReadOnlySet<T> CreateSet<T>(params ReadOnlySpan<T> values) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for this method to be ReadOnlyCollection.CreateSet<T>() instead of ReadOnlySet.Create<T>() in a new static class?
The later is much more user friendly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The builder methods aren't for users. They are for compilers to build collection expressions.
The compiler only requires static method in non-generic type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for this method to be ReadOnlyCollection.CreateSet<T>() instead of ReadOnlySet.Create<T>() in a new static class? The later is much more user friendly.

I could replace ReadOnlyCollection.CreateSet<T>() with the ReadOnlySet.Create<T>() method if that's more correct.

Copy link
Member

@huoyaoyuan huoyaoyuan Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could replace ReadOnlyCollection.CreateSet<T>() with the ReadOnlySet.Create<T>() method if that's more correct.

That's not what's approved in API review. Questions around the names should go back to the original issue.

Copy link
Contributor Author

@AlexRadch AlexRadch Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could replace ReadOnlyCollection.CreateSet<T>() with the ReadOnlySet.Create<T>() method if that's more correct.

That's not what's approved in API review. Questions around the names should go back to the original issue.

It says that methods can be created in one class or separate ones... #110161 (comment)

We could add another type (e.g. System.Collections.ObjectModel.Collection or one non-generic type per collection (like we do in immutable).

values.IsEmpty ? ReadOnlySet<T>.Empty : new ReadOnlySet<T>((HashSet<T>)[.. values]);
AlexRadch marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace System.Collections.ObjectModel
{
/// <summary>Represents a read-only, generic set of values.</summary>
/// <typeparam name="T">The type of values in the set.</typeparam>
[CollectionBuilder(typeof(ReadOnlyCollection), "CreateSet")]
[DebuggerDisplay("Count = {Count}")]
public class ReadOnlySet<T> : IReadOnlySet<T>, ISet<T>, ICollection
{
Expand Down
7 changes: 7 additions & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8366,6 +8366,12 @@ void System.Collections.ICollection.CopyTo(System.Array array, int index) { }
void System.Collections.IList.Insert(int index, object? value) { }
void System.Collections.IList.Remove(object? value) { }
}
public static partial class ReadOnlyCollection
{
public static System.Collections.ObjectModel.ReadOnlyCollection<T> CreateCollection<T>(params System.ReadOnlySpan<T> values) { throw null; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have [EditorBrowsable(Never)] on both these Create methods (same as the approved API surface in #108457 (comment)).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have [EditorBrowsable(Never)] on both these Create methods (same as the approved API surface in #108457 (comment)).

It was here but @stephentoub wrote "This should not be marked with EditorBrowsable"

API review do not have this attribute also.

Should I return [EditorBrowsable(Never)] attribute to both methods?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought API review had just left those out accidentally. I don't feel strongly either way, but we should make sure to be consistent with the other factory APIs and any other ones we're adding I assume (eg. the ones in the other issue).

Copy link
Member

@stephentoub stephentoub Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it wasn't accidental. These are perfectly valid for a dev to call directly, just like FrozenSet.Create, etc. Please remove the attributes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If those methods are meant to be called directly then based on this #110161 (comment) it seams more natural to create 1 non-generic type per collection
like it is done for Immutable and Frozen collections.

public static System.Collections.ObjectModel.ReadOnlySet<T> CreateSet<T>(params System.ReadOnlySpan<T> values) { throw null; }
}
[System.Runtime.CompilerServices.CollectionBuilder(typeof(System.Collections.ObjectModel.ReadOnlyCollection), "CreateCollection")]
public partial class ReadOnlyCollection<T> : System.Collections.Generic.ICollection<T>, System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IList<T>, System.Collections.Generic.IReadOnlyCollection<T>, System.Collections.Generic.IReadOnlyList<T>, System.Collections.ICollection, System.Collections.IEnumerable, System.Collections.IList
{
public ReadOnlyCollection(System.Collections.Generic.IList<T> list) { }
Expand Down Expand Up @@ -8471,6 +8477,7 @@ void System.Collections.ICollection.CopyTo(System.Array array, int index) { }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
}
}
[System.Runtime.CompilerServices.CollectionBuilder(typeof(System.Collections.ObjectModel.ReadOnlyCollection), "CreateSet")]
public partial class ReadOnlySet<T> : System.Collections.Generic.ICollection<T>, System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IReadOnlyCollection<T>, System.Collections.Generic.IReadOnlySet<T>, System.Collections.Generic.ISet<T>, System.Collections.ICollection, System.Collections.IEnumerable
{
public ReadOnlySet(System.Collections.Generic.ISet<T> @set) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,23 @@ public static void Ctor_IList()
Assert.Same(s_intArray, collection.GetItems());
}

[Fact]
public static void Ctor_CollectionExpressions_Empty()
{
ReadOnlyCollection<int> c = [];
Assert.IsType<ReadOnlyCollection<int>>(c);
Assert.Empty(c);
Assert.Same(c, ReadOnlyCollection<int>.Empty);
}

[Fact]
public static void Ctor_CollectionExpressions()
{
ReadOnlyCollection<int> c = [.. s_intArray];
Assert.IsType<ReadOnlyCollection<int>>(c);
Assert.Equal(c, s_intArray);
}

[Fact]
public static void Count()
{
Expand Down
Loading