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. |
Presumably that's analyzable. At least in the linked case, since it's a) it has explicitly implemented ICollection.Count, and b) has a (different) public member named Count. In that case, IReadOnlyCollection.Count will be considered implicitly implemented, and not lifted from the DIM on ICollection. |
Maybe but I'll struggle with writing down what I would be looking for. There are many ways one could share the logic/semantics without necessarily binding to the same method across all v-table slots. |
Incidentally it's also fixable. Define a new attribute for default interface mehods that means "do not look for public methods to replace this with, but only methods declared to implement the interface. I can tell that random public methods of the same name as interface methods don't necessarily impute themselves onto newly added interface methods because the opposite behavior exists in VB.NET; adding a (non-default) interface method results in a load error in VB.NET until the method is implemented in the class implementing the interface. |
The low tech approach could be just about looking at the list of methods. This would have to be explicit interface method implementation and the C# compiler mangles them in a distinct way. If there's a method with name
If I understand it right, the fix would cause |
If we want to run this experiment this year it would probably need to be merged before the end of January in time for Preview 1. Any volunteers willing to source an implementation? |
If nobody else steps up by Jan 2, I'll try it; but an earlier attempt would be better. Sorry, I cannot do it this month. |
No need to apologize for not being able to volunteer your time to an OSS project, especially over the holiday period :-) |
I'll try my hand at it with #95830. |
Change got reverted in #101644, reopening to keep track of next steps. |
😭What are the next steps? |
With respect to C++/CLI (I’ve never used it but it sounds neat), this change is not a breaking change in any meaningful way, and is only breaking on C++/CLI due to a limitation in their type system. I’m not sure if it’s considered a bug to be fixed or a new feature that requires some cycles to design and implement, but I do hope this can be resolved on C++/CLI’s side and the change go through again. It would be a colossal disappointment if this change gets denied. |
Previously DIM in static interface method were blocked by C++/CLI too and temporarily reverted until RTM. |
On considering the failure mode in C++/CLR; I am reasonably convinced it is not an ABI breaking change but only an API breaking change (that is, it only fails at compile time). That being said, the required source fix is ugly and something should be done. On further analysis, this seems to be a bug in the C++/CLR compiler. The problem happened upon adding If the type follows CLR rules, it doesn't have a public method called #include <iostream>
class IEnumerable {
};
class IReadOnlyList: public virtual IEnumerable {
public:
int getCount() { return 3; }
};
class IList: public virtual IEnumerable {
public:
int getCount() { return 3; }
};
class MyList: public virtual IList {};
int main()
{
MyList l;
std::cout << l.getCount() << "\n";
} Which builds and runs; the C++/CLR compiler is behaving as though the change imputed the following change to the code: |
it's about a simple static cast dotnet/wpf#9055 (comment) |
@eiriktsarpalis Any next steps on this yet? Is the path to fix the incompatibilities in C++/CLI in a general solution or perhaps just target the API's where this change causes issues? Thanks. |
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. |
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: