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

[API Proposal]: ReadOnly empty collection singletons #76028

Closed
Tracked by #77391
stephentoub opened this issue Sep 22, 2022 · 19 comments · Fixed by #76764
Closed
Tracked by #77391

[API Proposal]: ReadOnly empty collection singletons #76028

stephentoub opened this issue Sep 22, 2022 · 19 comments · Fixed by #76764
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Collections
Milestone

Comments

@stephentoub
Copy link
Member

stephentoub commented Sep 22, 2022

Background and motivation

Various pieces of code today allocate new read-only collections (e.g. ReadOnlyCollection<T>) around empty collections. Sometimes code implements its own singleton caching, e.g.

and sometimes it doesn't, e.g.

In other cases, empty could be special-cased but isn't as doing so would have required someone to add their own cache for it to have meaningful benefits).

In any case, we can make this easier and more efficient by exposing our own singletons that we and others can use.

(Replaces #41147, which is limited to just ReadOnlyCollection.)
(Related to #38340, which asked for such Empty methods on mutable collections like List<T> returning empty instances of those same collections. There we said we'd consider read-only variants with a proposal; this is that proposal.)

API Proposal

namespace System.Collections.Generic
{
    public interface IReadOnlyCollection<out T>
    {
+        public static IReadOnlyCollection<T> Empty { get; }
    }

    public interface IReadOnlyList<out T> : IReadOnlyCollection<T>
    {
+        public static new IReadOnlyList<T> Empty { get; }
    }

    public interface IReadOnlyDictionary<TKey, TValue> : IReadOnlyCollection<KeyValuePair<TKey, TValue>>
    {
+        public static new IReadOnlyDictionary<TKey, TValue> Empty { get; }
    }

    public interface IReadOnlySet<T> : IReadOnlyCollection<T>
    {
+        public static new IReadOnlySet<T> Empty { get; }
    }
}

namespace System.Collections.ObjectModel
{
    public class ReadOnlyCollection<T>
    {
+        public static ReadOnlyCollection<T> Empty { get; }
    }

    public class ReadOnlyDictionary<TKey, TValue>
    {
+        public static ReadOnlyDictionary<TKey, TValue> Empty { get; }
    }

    public class ReadOnlyObservableCollection<T>
    {
+        public static ReadOnlyObservableCollection<T> Empty { get; }
    }
}

API Usage

public ReadOnlyCollection<T> GetValues() =>
    _count != 0 ? new ReadOnlyCollection<T>(_values) : ReadOnlyCollection<T>.Empty;

Alternative Designs

No response

Risks

  • These might be our first non-abstract/virtual static interface methods in the core libraries?
@stephentoub stephentoub added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Collections labels Sep 22, 2022
@stephentoub stephentoub added this to the 8.0.0 milestone Sep 22, 2022
@ghost
Copy link

ghost commented Sep 22, 2022

Tagging subscribers to this area: @dotnet/area-system-collections
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and motivation

Various pieces of code today allocate new read-only collections (e.g. ReadOnlyCollection<T>) around empty collections. Sometimes code implements its own singleton caching, e.g.

and sometimes it doesn't, e.g.

In other cases, empty could be special-cased but isn't as doing so would have required someone to add their own cache for it to have meaningful benefits).

In any case, we can make this easier and more efficient by exposing our own singletons that we and others can use.

(Replaces #41147, which is limited to just ReadOnlyCollection.)
(Related to #38340, which asked for such Empty methods on mutable collections like List<T> returning empty instances of those same collections. There we said we'd consider read-only variants with a proposal; this is that proposal.)

API Proposal

namespace System.Collections.Generic
{
    public interface IReadOnlyCollection<out T>
    {
+        public static IReadOnlyCollection<T> Empty { get; }
    }

    public interface IReadOnlyList<out T> : IReadOnlyCollection<T>
    {
+        public static new IReadOnlyList<T> Empty { get; }
    }

    public interface IReadOnlyDictionary<TKey, TValue> : IReadOnlyCollection<KeyValuePair<TKey, TValue>>
    {
+        public static new IReadOnlyDictionary<TKey, TValue> Empty { get; }
    }

    public interface IReadOnlySet<T> : IReadOnlyCollection<T>
    {
+        public static new IReadOnlySet<T> Empty { get; }
    }
}

namespace System.Collections.ObjectModel
{
    public class ReadOnlyCollection<T>
    {
+        public static ReadOnlyCollection<T> Empty { get; }
    }

    public class ReadOnlyDictionary<TKey, TValue>
    {
+        public static ReadOnlyDictionary<TKey, TValue> Empty { get; }
    }
}

API Usage

public ReadOnlyCollection<T> GetValues() =>
    _count != 0 ? new ReadOnlyCollection<T>(_values) : ReadOnlyCollection<T>.Empty;

Alternative Designs

No response

Risks

  • These might be our first non-abstract/virtual static interface methods in the core libraries?
Author: stephentoub
Assignees: -
Labels:

api-suggestion, area-System.Collections

Milestone: 8.0.0

@eiriktsarpalis
Copy link
Member

These might be our first non-abstract/virtual static interface methods in the core libraries?

Any potential risks? From a discoverability standpoint they seem pretty nice, and possibly a pattern we could be introducing in other interface types.

@stephentoub
Copy link
Member Author

stephentoub commented Sep 22, 2022

Any potential risks?

Not that I'm aware of. It just hasn't been expressible until relatively recently (C# 8 if memory serves).

@huoyaoyuan
Copy link
Member

For IReadOnlyCollection<T> and IReadOnlyList<T>, what's the benefit comparing to Array.Empty?

@stephentoub
Copy link
Member Author

For IReadOnlyCollection and IReadOnlyList, what's the benefit comparing to Array.Empty?

Discoverability and consistency across the interfaces. There's no performance benefit I can think of.

@eiriktsarpalis
Copy link
Member

For IReadOnlyCollection and IReadOnlyList, what's the benefit comparing to Array.Empty?

Discoverability and consistency across the interfaces. There's no performance benefit I can think of.

Should we be using private implementations for each these interfaces or does returning Array.Empty suffice? Assuming we do the latter, Hyrum's law dictates that users will take a dependency on the underlying type being an array, but I don't foresee this needing to change.

@stephentoub
Copy link
Member Author

Should we be using private implementations for each these interfaces or does returning Array.Empty suffice?

Unless we can optimize something better than what array does, I think we should just use Array.Empty.

@omariom
Copy link
Contributor

omariom commented Sep 23, 2022

ReadOnlyObservableCollection seems to be a good candidate too

@GSPP
Copy link

GSPP commented Sep 26, 2022

Unless we can optimize something better than what array does, I think we should just use Array.Empty.

This will invariably lead to people taking a dependency on these values being actual arrays. Of course, nobody should do that but it's going to happen. This could make future changes to these properties difficult from a compatibility standpoint. Maybe it's safer to have a private implementation.

For example, a reflection-based serializer might take a different path based on runtime type.

@eiriktsarpalis
Copy link
Member

For example, a reflection-based serializer might take a different path based on runtime type.

That's a good point, actually. Any reflection-based serializer using polymorphism might provide divergent serialization contracts for empty instances.

@stephentoub stephentoub added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Sep 27, 2022
@danmoseley
Copy link
Member

danmoseley commented Sep 27, 2022

What does using the interface members look like -- var empty = IReadOnlyList<Foo>.Empty; ?

List<Foo>.Empty would be more discoverable, presumably. (or create a new ReadOnlyList<T> and put it on there, similar for ReadOnlyHashSet<T> perhaps). what am I missing?

@stephentoub
Copy link
Member Author

stephentoub commented Sep 27, 2022

what am I missing?

That List<Foo>.Empty would be expected to return a List<Foo> (which we can't do), not IReadOnlyList<Foo>. So it would discoverably return the wrong thing.

@danmoseley
Copy link
Member

Right, I figured the return type is something that could be navigated, once you've found the thing. But I defer to API experts.

@danmoseley
Copy link
Member

And is there value in considering adding ReadOnlyList<T> and ReadOnlyHashSet<T>, including Empty following the pattern above?

@stephentoub
Copy link
Member Author

And is there value in considering adding ReadOnlyList<T> and ReadOnlyHashSet<T>, including Empty following the pattern above?

ReadOnlyCollection<T> is an IReadOnlyList<T>. I don't think adding a whole new type that's a duplicate of ReadOnlyCollection<T> adds enough values.

There might be value in a ReadOnlySet<T> ala ReadOnlyDictionary<TKey, TValue>, but I wouldn't add that purely for Empty. Rather, we could choose to add such a thing on its own merit, and if we ever did, it's surface area would include an Empty property.

@tfenise
Copy link
Contributor

tfenise commented Sep 27, 2022

Would this cause confusions, as the proposed static properties would be inherited? For example, if IReadOnlyList<T>.Empty were introduced, IImmutableList<T>, which inherits from IReadOnlyList<T>, would also have an inherited static property Empty, which unfortunately would be of type IReadOnlyList<T>, not IImmutableList<T>.

For other languages like VB or F#, would static interface members be difficult to consume?

@stephentoub
Copy link
Member Author

add IReadOnlySet<T>.Empty

That's already there in the list.

@terrajobst
Copy link
Member

terrajobst commented Oct 4, 2022

Video

  • We're undecided on the statics on the interface
    • We haven't used non-virtual statics on interface
    • Unclear what this means for language interop
    • Unclear how it impacts IntelliSense on interfaces extending these
    • It's new pattern
  • We are in favor of exposing them on the concrete types
  • We currently don't have a ReadOnlySet<T>; if we had this, we should also a static Empty there
namespace System.Collections.ObjectModel
{
    public partial class ReadOnlyCollection<T>
    {
        public static ReadOnlyCollection<T> Empty { get; }
    }

    public partial class ReadOnlyDictionary<TKey, TValue>
    {
        public static ReadOnlyDictionary<TKey, TValue> Empty { get; }
    }

    public partial class ReadOnlyObservableCollection<T>
    {
        public static ReadOnlyObservableCollection<T> Empty { get; }
    }
}

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Oct 4, 2022
@alexrp
Copy link
Contributor

alexrp commented Oct 5, 2022

FWIW, I just ran into a situation today where I would have liked to have IReadOnlySet<T>.Empty and had to roll my own. Would be a bit unfortunate if that doesn't get included here in some form.

@stephentoub stephentoub self-assigned this Oct 7, 2022
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Oct 7, 2022
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Oct 24, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Nov 23, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Collections
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants