Disallow interfaces with static virtual members as type arguments #8922
Replies: 72 comments
-
I understand that the language should provide language constructs which are executable at runtime and do not throw unneccessary exceptions. But I do not agree that constraints must not contain interfaces with static virtual members. This restricts the usefulness of such interfaces with static virtual members in a major way. Instead I recommend that the the language defines, that such constraints can only be fulfilled by non interface types like structs or classes (implementing this static members) . This should be checked by the compiler. Similar constraints exist today: 'where T : struct, I' and 'where T : class, I'. |
Beta Was this translation helpful? Give feedback.
-
This was discussed and accepted in LDM: https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts |
Beta Was this translation helpful? Give feedback.
-
@jaredpar, would you please assign to whomever will be implementing this restriction? |
Beta Was this translation helpful? Give feedback.
-
I read these discussions and understand the problem. |
Beta Was this translation helpful? Give feedback.
-
From the example, class D : C<I>
{
public override void M<U>() => Console.WriteLine(U.P);
} This would mean |
Beta Was this translation helpful? Give feedback.
-
Can you give an example?
Can you clarify why? |
Beta Was this translation helpful? Give feedback.
-
As an example: Code which implements the strategy pattern.
When I understand the proposal right, then This is the point I'm not satisfied with and I think it should be possible in a release version bejond preview. |
Beta Was this translation helpful? Give feedback.
-
I don't believe that's the case. T is not an interface containing static abstracts there. So there would be no issue. |
Beta Was this translation helpful? Give feedback.
-
Maybe the proposal can clarify, where the runtime error is raised now and where the compiler error will be raised in future. |
Beta Was this translation helpful? Give feedback.
-
The compiler error would be given at a site where an interface is used as a type argument and that interface contains at least one static without an impl (e.g. a static-abstract). In your case, here are the places where you supply type arguments (i've used interface INumber<T> {
//static abstract members here
}
interface ICalculationStrategy<T> where T : INumber<*T*> {
T PerformCalculation(T arg1, T arg2);
}
class ConcreteCalculationStrategy<T> : ICalculationStrategy<*T*> where T : INumber<*T*> {
public T PerformCalculation(T arg1, T arg2) {
// use static members of T for some math
}
}
// some concrete usage
ICalculationStrategy<*double*> strategy = new ConcreteCalculationStrategy<*double*>();
double result = strategy.PerformCalculation(1.0, 2.0); In none of those places are you passing an interface with abstract-statics. So this is all fine. -- I'm not sure on the runtime side yet. I'll have the runtime people weigh in on that. |
Beta Was this translation helpful? Give feedback.
-
Thanks for your clarification. I see no direct problem now. |
Beta Was this translation helpful? Give feedback.
-
Terrific! |
Beta Was this translation helpful? Give feedback.
-
Because of this decision that breaks my codebase, I would like for |
Beta Was this translation helpful? Give feedback.
-
ProblemI understand the hole that this closes, but this seems very, very limiting. This change puts interfaces with static abstract members in a whole new category where you can't use them in most places you'd expect to, similar to ref structs. Except ref structs are usually used in specific, limited contexts. Interfaces are everywhere. Adding a new abstract static member to an interface should be a breaking change to implementors only, not to every single usage as a type parameter. You now can't use the interface type in common types such as Let's take the following interface: interface INode
{
string Text { get; }
static abstract INode Create(string text);
} The abstract static method is only for convenience, so you can do things like: T CreateNode<T>(string text) where T : INode => T.Create(text); which is awesome, and what abstract static methods were made for. Until you realize that all the following don't work anymore: var nodes = new List<INode>(); // CS8920
void Process(IEnumerable<INode> nodes) { } // CS8920
string[] GetTexts(INode[] nodes) => nodes.Select(node => node.Text).ToArray(); // CS8920
Task<INode> GetNodeAsync() { ... } // CS8920 Those methods aren't made to accept concrete implementations of By closing the small hole in the wall, you've ended up trapped in the basement. Solutions
In the The examples in the OP become: interface I
{
static abstract string P { get; }
}
class C<T> where T : concrete I
{
void M() { Console.WriteLine(T.P); }
}
new C<I>().M(); // CS8920 abstract class C<T>
{
public abstract void M<U>() where U : T;
public void M0() { M<T>(); }
}
interface I
{
static abstract string P { get; }
}
class D : C<I>
{
public override void M<U>() => Console.WriteLine(U.P); // CS8920
}
new D().M0(); |
Beta Was this translation helpful? Give feedback.
-
I've said it before and I'll say it again: It'd be quite useful to bring a |
Beta Was this translation helpful? Give feedback.
-
I'm ok with dynamic causing runtime errors. 'dynamic' is the feature that says 'resolve at runtime. I'm not ok with it here for non-dynamic cases. Thanks :) |
Beta Was this translation helpful? Give feedback.
-
The overwhelming majority of developers ARE fine with it but hold the wire, everyone else's dozens of legitimate use cases be damned because Cyrus Najmabadi is "not ok with it". |
Beta Was this translation helpful? Give feedback.
-
Do you find that method of argument to be at all effective? Cyrus isn't the only member of the language design team to find the potential of a runtime exception to be an unacceptable risk, and short of convincing them of a path forward that is safe enough to be implemented you're not going to find something happening here. It's not going to happen by browbeating the members of the language team. |
Beta Was this translation helpful? Give feedback.
-
It's clearly not going to happen at all regardless of what is said. That was made very clear by the two and a half years of comment history in this thread being met with nothing but stubborn refusal. Unsubscribing from this thread since it's very clear that the language team has already made up its mind and isn't interested in actually receiving input. |
Beta Was this translation helpful? Give feedback.
-
@CyrusNajmabadi How about introducing the |
Beta Was this translation helpful? Give feedback.
-
I would argue strenuously that rendering interface abstract static members practically unusable in so many valid use cases was absolutely worse than allowing a runtime exception (and perhaps documenting it) in a scenario that less than 0.00001% of people are likely to ever come across. But that's just me. |
Beta Was this translation helpful? Give feedback.
-
I would be willing to review such a proposal if someone wants to create it. |
Beta Was this translation helpful? Give feedback.
-
You're definitely welcome to have that opinion. We don't agree. If someone wants to get data indicating this is more important, and has a reasonable proposal for a way to resolve this(with whatever might be needed in lang or runtime), we'd definitely look at the ideas. |
Beta Was this translation helpful? Give feedback.
-
@CyrusNajmabadi You're definitely welcome not to agree, but you haven't provided any evidence for your claim whatsoever, whereas the actual feedback on this issue so far (such as the ratio of thumbs-ups/thumbs-downs, plus the actual written feedback by users) is all evidence in support of my stance.
If you aren't already convinced that this is "important", I honestly don't know what to say. I believe the case has been made patently clear.
People have pointed out |
Beta Was this translation helpful? Give feedback.
-
The argument for avoiding runtime exceptions is reasonable, but it sounds strained to me. The language is full of other places where typesafety is left to runtime checks; better to be able to express something that needs checking at runtime than not to be able to express it at all. Really major features such as nullable references types, static initialization ordering, reflection, array access - all are crucial to virtually every program, and yet we accept that soundness cannot be determined compile time. If anything, this is a much easier runtime check to document and understand than issues with static initialization ordering, for instance. Still, the workaround today of using a static virtual that throws at least exists. However, a concrete constraint sounds like it would be a great compromise that doesn't involve everybody not forgetting to override a static virtual or just give up on static abstract to express type-classes entirely. |
Beta Was this translation helpful? Give feedback.
-
I'm not the one wanting a change here. I'm fine with the status quo. If you want a change you'll have to convince people on the team it is needed.
My time is entirely full currently with tons of other things we think are much more important. I can help with reviews, but that's about it. Sorry. |
Beta Was this translation helpful? Give feedback.
-
Yup. Seems very workable to me. And you now aren't blocked and you get a runtime exception which you seem ok with. I'm not really understanding why this isn't sufficient. What doesn't work in such a situation? |
Beta Was this translation helpful? Give feedback.
-
Error reporting and dev experience is worse. Failing to implement a static abstract is a compile-time error; failing to override a static virtual is perfectly fine. The same type hole as previously still exists, except now error reporting never catches it, instead of catching it in all straightforward cases. static virtual won't even report an error in the direct, non-type-constrained case. |
Beta Was this translation helpful? Give feedback.
-
You can make this an analyzer error if that's a concern of yours.
You can make that an analyzer error if that's a concern.
Yes. but you can at least compile and run. And if you do need compile time errors enough on the need for override, it's a trivial analyzer to add. |
Beta Was this translation helpful? Give feedback.
-
Converting to a discussion for continued commenting. This issue is part of #4436. |
Beta Was this translation helpful? Give feedback.
-
This proposal attempts to address a type hole that has been pointed out a number of times with static virtual members, e.g. here.
Static virtual members (Proposal: static-abstracts-in-interfaces.md, tracking issue: #4436) have a restriction disallowing interfaces as type arguments where the constraint contains an interface which has static virtual members.
This is to prevent situations like this:
If this were allowed, the call to
C<I>().M()
would try to executeI.P
, which of course doesn't have an implementation! Instead the compiler preventsI
(and interface) from being used as a type argument, because the constraint contains an interface (alsoI
) that has static virtual members.However, the situation can still occur in a way that goes undetected by the compiler:
Here
I
is not directly used as a constraint, so the compiler does not detect a violation. But it is still indirectly used as a constraint, when substituted for the type parameterT
which is used as a constraint forU
.The runtime protects against this case by throwing an exception when
M0
tries to instantiateM<>
withI
, but it would be better if the compiler could prevent it.I propose that we change the rule so that:
This simple rule protects the assumption that any type used as a type argument satisfies itself as a constraint. That is the assumption behind the language allowing e.g. the
M<T>
instantiation above, where the type parameterU
is constrained byT
itself.It is possible that this restriction would limit some useful scenarios, but it might be better to be restrictive now and then find mitigations in the future if and when we find specific common situations that are overly constrained by it.
Beta Was this translation helpful? Give feedback.
All reactions