-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Make mutable generic collection interfaces implement read-only collection interfaces #31001
Comments
@terrajobst I would like to have your input on these w.r.t new default implementations and adding new interfaces or new members to interfaces. |
For folks confused as to why this would be a breaking change without default interface implementations, consider three separate assemblies defined as such: namespace MyClassLib
{
public interface IFoo
{
void PrintHello();
}
} namespace MyOtherClassLib
{
public interface IBar
{
void PrintHello();
}
} namespace MyApplication
{
class Program
{
static void Main(string[] args)
{
object myFoo = MakeNewMyFoo();
IFoo foo = myFoo as IFoo;
if (foo is null)
{
Console.WriteLine("IFoo not implemented.");
}
foo?.PrintHello();
IBar bar = myFoo as IBar;
if (bar is null)
{
Console.WriteLine("IBar not implemented.");
}
bar?.PrintHello();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static object MakeNewMyFoo()
{
return new MyFoo();
}
class MyFoo : IFoo
{
void IFoo.PrintHello()
{
Console.WriteLine("Hello from MyFoo.IFoo.PrintHello!");
}
}
}
} Compile and run the main application, and you'll see the following output. Hello from MyFoo.IFoo.PrintHello!
IBar not implemented. Now change the assembly which contains namespace MyClassLib
{
public interface IFoo : IBar
{
}
} The main application will crash upon launch with the following exception. Unhandled exception. System.MissingMethodException: Method not found: 'Void MyClassLib.IFoo.PrintHello()'.
at MyApplication.Program.Main(String[] args) The reason for this is that the method that would normally be at the slot As OP points out, adding default interface implementations works around the issue by ensuring that an appropriate method exists in the expected slot. |
@terrajobst any input on this collection interface change as well? |
@safern @GrabYourPitchforks @terrajobst Is this something we could get triaged soon? I'd love for this to make it into .NET 5. |
@eiriktsarpalis and @layomia are the new System.Collections owners so I'll defer to them. |
Here's a comment @terrajobst had about this in a separate issue, #2293 (comment) Hoping for some more insight. |
I updated the proposal to add a The new method would take precedence in this case and it's implementation if not overridden would delegate to |
As an alternative since With that change then So instead of this. namespace System.Collections.Generic {
public interface ICollection<T> : IReadOnlyCollection<T> {
bool Contains(T value);
...
}
public interface IReadOnlySet<T> : IReadOnlyCollection<T> {
bool Contains(T value);
...
}
public interface ISet<T> : IReadOnlySet<T> {
new bool Contains(T value) => ((ICollection<T>)this).Contains(value);
bool ICollection<T>.Contains(T value);
bool IReadOnlySet<T>.Contains(T value) => ((ICollection<T>)this).Contains(value);
...
}
} it would be this. namespace System.Collections.Generic {
public interface IReadOnlyCollectionInvariant<T> : IReadOnlyCollection<T> {
bool Contains(T value);
...
}
public interface ICollection<T> : IReadOnlyCollectionInvariant<T> {
new bool Contains(T value);
bool IReadOnlyCollectionInvariant<T>.Contains(T value) => Contains(value);
...
}
public interface IReadOnlySet<T> : IReadOnlyCollectionInvariant<T> {
...
}
} |
As a follow-up to this: Perhaps LINQ methods that have optimized execution paths on interfaces like
I could be wrong but I believe there have been runtime updates to address point 1. Point 2 would be addressed by this issue. Point 3 is something that we could move away from with these changes to make read-only collection interfaces real first-class citizens in the framework that are actually widely usable. A remaining sore point would be the annoying lack of support for read-only interfaces in many places where they should be supported as inputs and adding overloads that accept read-only interfaces is problematic because many collections currently implement both The above issues combined with the inability to pass an |
I would like to propose an alternative solution: For example, for a List we have this: IList<T>
IReadOnlyList<T>
List<T> : IList<T> , IReadOnlyList<T> What I propose is this: IList<T>
IReadOnlyList<T>
IWriteableList<T> : IList<T>, IReadOnlyList<T>
List<T> : IWriteableList<T> So, from the point of view of the runtime, it would just require introducing a new interface, so I don't think it would break any compatibility. And developers would progressively move from using IList to IWriteableList |
So it turns out this is a very peanut-buttery performance optimization as well. There are a number of places in Linq that check for IEnumerable for ICollection or IList for performance improvements and the report is that these can't be changed to check both for performance reasons; yet I found place after place in my own code where only IReadOnlyCollection/List is provided and a mutable collection would be meaningless. I also found many places where ICollection/List is dynamically downcast to its ReadOnly variant with an actual synthesis if the downcast fails. Particularly to the implemenation of .Select, I found that making this change to that all ICollection/List also provide their IReadOnly variants yields 33-50% performance improvement at only the cost of JIT time (to provide the interfaces where they are not). I also found that adding an IReadOnlyCollection.CopyTo() is a possible improvement but the gains aren't needed because implementing IListSource on the projection returned by Select() on an IReadOnlyCollection yields more gain than that ever could. Synthetic benchmark attached showing the component changes. The first number is garbage (ensures the memory is paged in); the second and third numbers show the baseline, and the last number shows the gains. (It's approximately equal to the second number, which means I found all the performance gains). I found on my machine about every third run is garbage due to disturbance, so run it three times and keep the closest two numbers. |
Update: Saying that IReadOnlyCollection.CopyTo() was not needed because of a better implementation involving IListSource() proved to be overfitting to the test. It turns out that there is more cheap (but not free) performance improvements lying around for direct invocation of .ToList() and .ToArray() (commonly used for shallow copies of arrays), List.AddRange(), List.InsertRange(), and List's own Constructor to the point where adding The default implementation would be:
|
Potential alternative design: #23337. Please also see this comment demonstrating the potential for compile-time and runtime diamond errors when adding DIMs to existing interfaces. Would need to investigate if the current proposal is susceptible to the same issue (I suspect it might not be as long as we're not adding new methods). |
@eiriktsarpalis : I found that to be not an alternative design but a different design defect that could be fixed independently. |
Not implementing this is slowly causing us more and more problems. The root is It almost feels like a dumb compiler, but it's not. It doesn't matter which way half of the the overloads are resolved. The implementations are copy/paste of each other. Latest trigger. One of our programmers really likes calling |
@jhudsoncedaron This could potentially alleviate some of those issues: Would still be nice to fix collections though. |
@mikernet : It won't. To work I need it to do exactly what it says it won't do: "Overload priorities should only be considered when disambiguating methods within the same assembly". I need to win (or in one case lose) against overloads shipped with .NET itself. |
Is there intention to try to get this back in time for a .Net 9 Preview? Is it being delayed to .Net 10+, or is it dead? The various discussions I've seen/heard seem to indicate that this is just a temporary issue when C++/CLI, but how temporary are we talking? I hope it's not as temporary as the lack of adoption for C++ modules 😅 |
It's not dead yet but it depends on whether or not we can control the failure cases. At this point .NET 9 is extremely unlikely. |
Given we only have a month left of .NET 9 development, it is too late for a change carrying so much risk. Moving to 10.0.0 |
Why not just do this already? 😔 It would be a huge quality of life improvement if every custom collection didn't have to derive from like 6 different interfaces (readonly/nonreadonly/nongeneric) and implement tons of bloat. It would also be nice if the generic interfaces derived from and implemented the corresponding non-generic interfaces. |
I hope that, come November already (maybe early next year to allow time for Net9 to cook) they'll try to merge it in again |
If you read the thread upwards; they have to come up with a solution to the problem that the C++/CLR compiler has a hard time of it. It looks like a deficiency in that compiler, but it's not going to be a trivial fix. |
First, https://www.youtube.com/watch?v=OP4CKn86qGY It's on the docket for .NET 10. I agree that it's a huge quality of life improvement for C#, but we also need to make sure not to break everything else. And that requires coordination. |
Shouldn't it be the other direction? So public interface IReadOnlyCollection<out T> : ICollection<T> {
bool ICollection<T>.IsReadOnly => true;
void ICollection<T>.Add(T item) => throw new NotSupportedException();
void ICollection<T>.Clear() => throw new NotSupportedException();
bool ICollection<T>.Contains(T item) => Enumerable.Contains(this, item);
void ICollection<T>.CopyTo(T[] array, int arrayIndex) => /*...*/;
bool ICollection<T>.Remove(T item) => throw new NotSupportedException();
} But it looks like the compiler doesn't like that since |
This is heavily violating Liskov Substitution principle |
It doesn't make sense. You don't want to expose modification methods on the read-only interface, even if they're not implemented. |
is there a definitive list of protected APIs that cannot be modified due to C++/CLI constraints? It's been a decade since dotnet became open source, yet it still retains elements from its closed source origins :( |
No. MSVC can change to fit new features, for example the static interface method used in .NET 8 requires msvc 14.38+(VS17.8+). Everything used by the core BCL types can affect C++/CLI. |
you say that like it’s a good thing? if everything is off-limits, it just reinforces how locked down things still are. after all these years of dotnet being open source, it’s frustrating to still be working around these closed-source-era limitations |
Is there a tracking issue for the MSVC improvements which would enable this feature? I would love to give it a thumbs-up and I'm sure others would as well. |
One of the big reasons I ported all of my C++/CLI interop glue code over to C# was because of all the problems I kept running into with new runtime and C# features causing compile errors (for C++/CLI code), or preventing me from using things like |
It would be a little at odds with the names of the interfaces, but they've always had weird semantics, and in practice that's how immutable collections are implemented: (If I may digress, Kotlin made the right choice by defining |
@YoshiRulz
I'm not sure I understand this argument. This is the same as |
“read only” has always been a misnomer, it’s always really meant “readable”. The ship has long since sailed on naming them |
@YoshiRulz : If you want interfaces for things that cannot change; we have IImmutable* interfaces now. |
Is the final shape of the API set in stone? What will it be? If the breaking change is unavoidable, the later, the worse. And that's the greatest reason to make a big change, cleaning all the hierarchy, and adding all the missing interfaces (non-generic) and wiring all with DIMs. Backwards compatibility is a marvelous goal, and you have kept it like no other. But this time, developers (or companies) must stop being selfish, get their hands dirty, and review, rewrite and rebuild their code to improve the future for everyone, including themselves. |
Sorry for pinging, but @terrajobst in this comment and especially in the .Net Api Review (IIRC) insinuated that in order for this to have any chance of merging it has to be done very early in development*, which is where we're at now (Some could argue even this is too late; There's only like half a year or so until feature freeze after all). Is there any news here? Would be nice to have some closure here (Be it good or bad). I continue to use * They were naturally referring to .Net 9 in that review, but I digress. |
Yeah, I'm kind of curious who's responsible for this coordination Immo mentioned in readying it for .NET 10. Is it @eiriktsarpalis or @tarekgh as listed in the project it's attached to? If no-one's assigned to it within Microsoft we can't expect this to get done. |
CC @jeffhandley |
Rationale
It's long been a source of confusion that the mutable generic collection interfaces don't implement their respective read-only collection interfaces. This was of course due to the read-only collection interfaces being added after the fact and thus would cause breaking changes by changing a published interface API.
With the addition of default interface implementations in C#8/.NET Core 3.0 I think the mutable generic collection interfaces,
ICollection<T>
,IList<T>
,IDictionary<K, V>
, andISet<T>
should now implicitly inherit their respective read-only collection interfaces. This can now be done without causing breaking changes.While it would have been nice for these interfaces to share members, I think the proposed API below is the best we can possibly do with the read-only interfaces being added after the fact.
As an added bonus, this should allow some simplification of the type checking in LINQ code to check for the read-only interfaces instead of the mutable interfaces.
Proposed API
Binary Compatibility Test
I was able to test that this change doesn't break existing implementers with the following custom interfaces and by simply dropping the new interfaces dll to the publish folder without recompiling the consuming code, the
IMyReadOnlyList<T>
interface was automatically supported without breaking the code.Original Interfaces DLL code
New Interfaces DLL code
Consuming Code
Original Output
New Output
Moved from #16151
Updates
ISet<T>
implementingIReadOnlySet<T>
since Please add interface IReadOnlySet and make HashSet, SortedSet implement it #2293 has been implemented.The text was updated successfully, but these errors were encountered: