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
Add a non-generic IReadOnlyCollection interface #23337
Comments
Note that if we really want to go down this road that not all "countable" types have a property named Count. Files, arrays, and streams all use the property name Length rather than Count to determine their size. So by the same token, we would need an I do see the advantage with covariance, but it doesn't seem right to force every |
@NightOwl888 Please allow me to disagree with you, because most of those types you mention implement
|
What would be the real-world use of such interface? Interfaces don't come for free - they have runtime impact, size on disk impact and also maintenance cost associated. |
@karelz int GetCountIfAvailable(IEnumerable obj)
{
if(obj is ICollection collection)
return collection.Count;
var gCollection = typeof(ICollection<>);
var iCollection = obj.GetType().GetInterfaces().FirstOrDefault(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>));
if (iCollection != null)
return (int)iCollection.GetProperty("Count").GetValue(obj);
return -1; //count unavailable
} If we branch out the int GetCountIfAvailable(IEnumerable obj) =>
return obj is ICountable countable ? countable.Count : -1; Additionally, |
That's no problem. You'd just implement the interface explicitly: public class SomeCollectionWithLengthProperty : ICountable
{
public int Length { get; }
public int ICountable.Count => Length;
} |
@khellang @NightOwl888 in fact, arrays and some other types actually do just this. |
I am going to have to retract this statement. There already is a As for the |
Yes, but What I am after is a centralized interface that tells that any collection ( |
@weitzhandler |
While I understand the technical value on the caller side, I am still not sure how common such code is which needs to know the Or are there other real-world usages where this would provide significant value? |
@NightOwl888 @svick The problem with @karelz it could be a bit faster, and it could be n times faster depending on the collection size. I bump into this scenario every day. Given an
Here's an example scenario, and that's when |
As someone who has recent experience dealing with custom .NET collection types and the challenges surrounding them, let me see if I can offer up a solution that would work not just for the Count property, but for better APIs around collections in general. The main issue here is not that we are lacking an interface, it is that we are lacking any kind of relationship between the interfaces to make them generally usable within APIs. One of the biggest issues is there is no way to create APIs that work with both generic and non-generic interfaces without resorting to lots of reflection and casting. This is not limited to getting the Count property from an Another issue exists when trying to implement custom collection types. We typically can't just pick an interface and go, we typically need to implement several interfaces, which involve time and research. And none of the existing collection types are very helpful. Although they are not sealed, none of their members are virtual, so we are always starting from scratch to make custom collection types in .NET because nothing was made reusable. Back on point, let's say for the sake of argument we want to make a method that works on any void DoSomething(IList list) Now, a problem with doing this is that void DoSomething(IList list)
{
if (list.GetType().ImplementsGenericInterface(typeof(IList<>))
// handle generic
else
// handle non-generic
}
private static bool ImplementsGenericInterface(this Type target, Type interfaceType)
{
return target.GetTypeInfo().IsGenericType && target.GetGenericTypeDefinition().GetInterfaces().Any(
x => x.GetTypeInfo().IsGenericType && interfaceType.IsAssignableFrom(x.GetGenericTypeDefinition())
);
} To further complicate things, although all of the built-in types implement all of the interfaces to ensure our API works, there are no guarantees that custom collection types will implement all of the interfaces that we may need (depending on the functionality we are after). So despite our best efforts to make APIs that "just work", there are no guarantees that they always will. What's missing from the .NET toolbox is the fact there are no abstractions that allow us to design APIs that seamlessly work with both generic and non-generic types. So, I propose that we make abstract classes to fill that gap. This is not without precedence - this is exactly how they do it in Java. Although in .NET, we need to explicitly handle both generic and non-generic types and we might also make abstractions for the covariant types.
Here is a basic example. The real advantage comes with the inheritance hierarchy which ensures our contract is shared between generic and non-generic implementations. // All read-only non-generic arrays, lists, sets, and dictionaries subclass this
public abstract class AbstractReadOnlyCollection : IEnumerable
{
public abstract int Count { get; }
public IEnumerator GetEnumerator()
{
throw new NotImplementedException();
}
}
// All read-only generic arrays, lists, sets, and dictionaries subclass this
public abstract class AbstractReadOnlyCollection<T> : AbstractReadOnlyCollection, IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable
{
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
throw new NotImplementedException();
}
}
// All non-generic arrays, lists, sets, and dictionaries subclass this
public abstract class AbstractCollection : AbstractReadOnlyCollection, ICollection, IEnumerable
{
public abstract bool IsSynchronized { get; }
public abstract object SyncRoot { get; }
public abstract void CopyTo(Array array, int index);
}
// All generic arrays, lists, sets, and dictionaries subclass this
public abstract class AbstractCollection<T> : AbstractReadOnlyCollection<T>, ICollection<T>, IReadOnlyCollection<T>, ICollection, IEnumerable<T>, IEnumerable
{
public abstract bool IsReadOnly { get; }
public abstract bool IsSynchronized { get; }
public abstract object SyncRoot { get; }
public abstract void Add(T item);
public abstract void Clear();
public abstract bool Contains(T item);
public abstract void CopyTo(T[] array, int arrayIndex);
public abstract void CopyTo(Array array, int index);
public abstract bool Remove(T item);
} Note that the above is intended as an example of the public API contract, not as a finished implementation. With that in place, it becomes much easier to implement custom collection types, since a lot of the boilerplate code can be moved into the abstract base types. For example, we could potentially move the Note also that none of this is breaking - it is completely backward compatible with existing APIs. But more on point, @weitzhandler won't need to make a custom function to get the Count, he simply needs to accept the lowest level abstract collection and it will always work whether collection is generic or not (including Array types). In fact, all shared functionality is readily available without reflection or casting. void DoSomething(AbstractReadOnlyCollection collection)
{
var count = collection.Count;
} @weitzhandler - do you think this proposal would address all of your concerns? @karelz - As you can see, this proposal has a broader real-world impact, both around creating APIs that deal with collection types and with creating custom collection types. Both of these tasks are challenging to do with today's .NET APIs. LINQ is a great for working with generic collections, but doesn't bridge the gap between generic and non-generic collections and in some cases the performance cost is too high to be practical. Collection EqualityOne other thing I would like to chime in on is the fact that in .NET it is very difficult to compare 2 collections and their nested collections for equality. This is yet another common thing that is asked for that Enumerable.SequenceEqual only covers part of because dictionaries and sets require the comparison to be done regardless of order, and it also doesn't work recursively. This is a very common requirement during unit testing. While I don't propose we change the default behavior, it would be nice if there were a constructor parameter that could be used to change the equality checking on built-in collection types from reference equality to deep value equality checking including all nested collection types. The actual default implementations of both of these strategies could be put into the abstract base classes, giving implementers a choice of which type of equality checking they prefer. Perhaps there could also be a |
Why do you need to work with non-generic interfaces in the first place? Is it because of some legacy API?
There is
I think you should open a separate proposal for that, it's far out of scope of this issue. (Which is also why I won't comment on the proposal itself here.) |
For one, to implement the aforementioned deep equality checking so it works on existing platform collection types. I have no control over what type of collections that a user might utilize in a generic type and since I opted to save time and only support nested generic collection types (without support for non-generic collections), there are gaps in my implementation. But in a nutshell, I am running into the same challenges that @weitzhandler is.
Thanks for pointing that out. Although, that still sounds too low level to be very useful, since lists, sets, and dictionaries have common behaviors that are generally very different from one another (for example, it only makes sense to sort lists in general, and most sets and dictionaries have undefined sort order). It is also not commonly implemented by existing collection types (for example
Thanks again. I will do that when I get a chance and take into account the existing abstract |
I'm on mobile not able to text much. |
There are many cases, and I often bump into them. One is when you need to store a collection of
What I was just gonna say. Tho it would be nice having
|
Really this post belongs on the coreclr repo. Migrated. |
It does not, all API proposals for .Net Core belong to this repo:
That applies even for proposals whose implementation is going to be in the CoreCLR repo. |
I like the idea of this from a design perspective e.g. if we were starting anew. However, my issue with this proposal is how many changes it would require devs to make i.e.
I don't think that being able to have a shared interface with the Because of the required breaking changes, it would be very unlikely that we would be able to push a change this large through the API review process without a huge number of people that wanted it. |
EDIT: I've pivoted towards standardizing the implementation of collections rather than introducing another interface. See my later comment. I, too, wish there was a go-to means for recognizing a collection as opposed to an iterable, whether that be an interface ( I did want to address @NightOwl888's proposed abstract base classes: you wouldn't be able to use them for collections that are value types, such as |
Personally, I am not convinced the value is worth the effort & overhead of yet another interface. But I let @safern make the final call here as area owner. |
We typically use While I do acknowledge that Assuming we did decide to add a non-generic |
I'm going to pivot from my 2017 position and agree with @eiriktsarpalis. If we had the ability to start over from scratch, I'd advocate for a non-generic I think the only workable solution is to pick one of the interfaces, such as If the framework cannot provide a way to unify these interfaces, the next best thing is for its types and documentation to be consistent and clear that implementing |
FWIW here's what a non-generic namespace System.Collections
{
+ public interface IReadOnlyCollection : IEnumerable
+ {
+ public int Count { get; }
+ }
public partial interface ICollection
+ : IReadOnlyCollection
{
- public int Count { get; }
}
}
namespace System.Collections.Generic
{
public partial interface IReadOnlyCollection<T>
+ : IReadOnlyCollection
{
- public int Count { get; }
}
public partial interface ICollection<T>
+ : IReadOnlyCollection
{
- public int Count { get; }
}
} I'm not sure if this could be considered a breaking change (only scenario I can think of is it would change reflection metadata of the cc @stephentoub |
Hmm. Not seeing it. I think the use cases need to be expanded a bit. I can't come up with a use case where |
@jhudsoncedaron Try casting an |
@eiriktsarpalis I think it would be a breaking change for any type that implements public interface ICollection : IReadOnlyCollection
{
int Count { get; }
int IReadOnlyCollection.Count => Count;
} |
As a fellow library author, I, too, run into this issue over and over. To name just one simple use case, JSON deserializers tend to need not just a generic There are plenty of cases, particularly in class libraries, where we need to support being unable to rely on the generic collection type. I believe @zeldafreak summarized the issue perfectly:
Remember, one key difference between just being an We have a single abstraction for any enumerable sequence, but we are missing one for any materialized sequence.
I would steer clear of this idea. If class libraries were to depend on Instead, we should achieve a situation where implementing either |
As a note of interest, we might identify a third level of materialization/concreteness:
By indexable sequence I mean a sequence that supports Let's look at an example. Say a class library needs to call
I do not mean to derail the thread. Clearly, having an abstraction for a materialized sequence is the first and primary concern. But it seems interesting to keep in mind that there might be another level of abstraction that is missing in the exact same way. |
@svick good point, I had not realized this doesn't work for inherited interface members. Your DIM suggestion seems interesting though, on quick inspection it doesn't seem to suffer from the usual diamond ambiguity issues since that signature is already taken by the derived interfaces. namespace System.Collections
{
+ public interface IReadOnlyCollection : IEnumerable
+ {
+ int Count { get; }
+ }
public partial interface ICollection
+ : IReadOnlyCollection
{
- int Count { get; }
+ new int Count { get; }
+ int IReadOnlyCollection.Count => Count;
}
}
namespace System.Collections.Generic
{
public partial interface IReadOnlyCollection<T>
+ : IReadOnlyCollection
{
- int Count { get; }
+ new int Count { get; }
+ int IReadOnlyCollection.Count => Count;
}
public partial interface ICollection<T>
+ : IReadOnlyCollection
{
- int Count { get; }
+ new int Count { get; }
+ int IReadOnlyCollection.Count => Count;
}
} This is likely not a source breaking change but would need to double check that no runtime breaks occur because of it. |
An update on this. We ended up having to implement a (nearly) complete set of generic collections that are structurally comparable. They rely on a
The documentation describes this in more detail, and of course you can analyze the code to see how hairy this gets in Aggressive mode when we are dealing with BCL collection types.
Actually, arrays do implement
This is a valid argument. Do note that:
But this does point out another hole in our implementation where we didn't take this special case into account because it does not subclass |
An alternative way around a non-generic int? GetCountIfKnown(IEnumerable enumerable) =>
(enumerable as IReadOnlyCollection<?>)?.Count; There are many cases we need to access the members of an instance cast to a type that requires a generic parameter. And we are passed a reference that doesn't include anything about the parameter ( What to do about calls to instance members that include the generic type? A few options:
This would solve the issue in a more widespread way and eliminate the constant need to go "back to non-generic" by creating new interfaces. Of course, this doesn't solve the multiple identity disorder of having to have branching code to check which of many collection interfaces we are dealing with, but it does fix a lot of other problems with analyzing generics without having their closing types handy. |
@NightOwl888 I was vouching for a .NET implementation of wildcard generics with a proposal here: dotnet/csharplang#1992 |
I believe this need was addressed with Enumerable.TryGetNonEnumeratedCount(). |
@terrajobst TryGetNonEnumeratedCount only works with generic enumerables. We should keep this open unless we feel we don't want to support the non-generic scenario. |
Description
I often bump into scenarios where I have to evaluate an
IEnumerable
that might be eitherICollection<T>
,ICollection
,IEnumerable<T>
orIEnumerable
, to get their size if available, so to avoid iterating on them to count them.The thing with
ICollection<T>
, is that it doesn't inherit fromICollection
, and there are types that inherit fromICollection<T>
but not fromICollection
(for instanceHashSet<T>
).Additionally,
ICollection<T>
isn't co-variant, meaning not everyICollection<T>
is anICollection<object>
, likeIEnumerable<T>
is.I'm not arguing about this design, which is a good thing to avoid confusion and mis-adding of types.
Motives
The thing I do find annoying here, and I bump into it a lot, is the
Count
property, which is common toICollection
andICollection<T>
andIReadOnlyCollection<T>
, but these interfaces don't overlap each other.Obtain a value from a generic type when the type argument is unknown at compile time is only possible with reflection. So in order to get the known size of a collection you'd have to do the following (see discussion here). And with the branching of the
TypeInfo
class, this gets even worse:As pointed out later in comments by @NightOwl888, the
IReadOnlyCollection<T>
interface also exposes theCount
property, but the reasons it doesn't address my issue is:ICollection<T>
doesn't inherit from it.There are many use cases of non-generic collections I bump into, the real reason we need non-generic support, is not because their not generic, but rather because their generic argument may not be known at runtime.
I can't remember all of the scenarios I've encountered this demand as they are many, but I'll name a few.
ICollection<T>
s of a variety of types. When enumerating the main collection, the count of the items can only be achieved by acquiring the collection type with reflection.Another reason is that
ICollection
has more methods to implement, and is more tedious.Solution
My suggestion is then, to introduce a new interface
ICountable
(orIReadOnlyCollection
- non generic) that its contract determines that a collection has aCount
property and exposed the collection size regardless of its item type.This interface should be implemented by (please comment if you're aware of others):
ICollection
ICollection<T>
IReadOnlyCollection<T>
The new interface should have one sole task: expose a known size of a collection via a
Count
property, with disregard to the collection type (T
), editability (ICollection
vsIReadOnlyCollection
), or other features.Concequences
ICollection
,ICollection<T>
andIReadOnlyCollection<T>
will have to change the pattern toICountable
(or if we call itIReadOnlyCollection
or anything else).Count
onICollection
(or the others) as "declared", will have to switch to either calling the runtime property, or call it onICountable
etc.Related: https://github.com/dotnet/corefx/issues/23700.
The text was updated successfully, but these errors were encountered: