-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Proposal: Track subtype exhaustiveness for classes with only private constructors #4032
Comments
Seems like this would roll in with the general sealed type hierarchy proposals. |
Same idea, but also recognizing an existing pattern |
int M(C c) => s switch
{
A a => 0,
B b => 1
}; Does this still need a null check? -- Strongly in favor of this general area. Though on the fence if i think this special case is worth having support for, or if we should have the more general way to mark any hierarchy such that we can enumerate all the cases for it to do exhaustiveness checking |
Imagine it's prefixed with |
A different way of looking about it: this is a bug fix. It is simply true that any warning provided in the above switch expression is incorrect. The warning is that the switch expression is not exhaustive, but it is, in fact, exhaustive. |
Not quite - this makes the number of subtypes part of the public contract, and adding new subtypes is now a breaking change where it wasn't before. I still think it's worth it, but it does have costs. |
I don't think this is really true, because it's a broader language design opinion at the level of the system, as opposed to a pure consideration of the warning itself. The warning provided in this case is simply incorrect. The warning says that the switch is incomplete, but it isn't. The fact that a user changing their code can produce different behavior on the consumers part is true, but in the language we've never considered that as a "breaking change." |
I'm not saying that this is a breaking change. What I'm saying is that till now, a library author can freely add subtypes to a class without introducing new warnings to consuming code. Now he can't. In general if a library author cannot change something without introducing new warnings, that means this is a public contract, and in general it's best if public contracts are explicit, so authors don't accidentally tie themselves down. In this case I think authors probably would want to introduce new warnings in such a case, so it's worth it. |
@YairHalberstadt I see, so you're talking about the broader, design-level question. As to the question of whether this would count as an "explicit" opt-in to the behavior, I would guess so. But I don't think this negates from my fundamental point that 1) the current warning is incorrect and 2) it's clearly detectable by the compiler. |
Doesn't the same exhaustiveness check apply to internal classes too (or to public types with internal constructors)? All their inheritors will be known at compile time, unless I'm missing something. |
@Richiban I don't think it's possible because of |
Ahh, good point. It might be possible to detect that and treat any internal type defined in such an assembly as 'open' but it complicates the feature a fair bit. Sticking to |
The compiler already do something like this when it's immediately obvious: interface I {
sealed class A {
void M(A a) {
if (a is I) {} // won't warn if A is not sealed, but that would be still correct wrt effective accessibility
}
}
} Without any kind of modifier at the declaration-site to "verify" a closed hierarchy, I think it could get tricky to "infer" such relationship without a few false negatives, unless this is only about recognizing a specific pattern (like private constructors). |
Actually that should still warn unless A and B both are either sealed or define a private ctor. Question is how far the compiler should go to determine all possible subtypes. |
Wouldn't that still be exhaustive @alrz ? Any further subclasses would have do drive from A or B, and both of those are already checked in the switch... |
Agreed, any subtype of |
You're right, was thinking whether or not those could be inherited at all. That doesn't affect this particular switch, but that is also not exactly a closed hierarchy? |
Guess as long as base types are constant it's already close- Is there any other language that allows non-flat DUs? None comes to mind. |
It's closed on the sense that we can tell if they switch is exhaustive without needing a catch all clause :-) |
Java :) |
I mean, does it support exhaustiveness like what is described here? |
Yes, it was an explicit goal of sealed classes:
And the pattern matching in switch expressions is in its third preview with Java 19: public static String shape(Shape shape) {
// this would be a compiler error if considered non-exhaustive.
return switch(shape) {
case Circle x -> "Circle";
case Triangle x -> "Triangle";
case Quadrilateral x -> "Quadrilateral";
};
}
private sealed interface Shape permits Circle, Triangle, Quadrilateral { }
private static final class Circle implements Shape { }
private static final class Triangle implements Shape { }
private static sealed abstract class Quadrilateral implements Shape permits Square, Rectangle { }
private static final class Square extends Quadrilateral { }
private static final class Rectangle extends Quadrilateral { } Add in record patterns, also in preview, and I believe this is the combination of features intended to solve for DUs in the Java language. |
I actually like the approach Java has taken by making the base type declare the legal subtypes rather than trying to determine it through analysis. It more clearly codifies the intent of the developer and the expected contract of the hierarchy. I would expect that if DUs came to the language with an enum-like syntax that would cover that intent, but maybe it should be considered for closed hierarchies too. |
I think that depends on
From the link, that seem to be optional but given the above requirements I think it's mostly predictable? Not sure if there's any restriction on where those subtypes can actually be declared. |
|
That's true, it's optional in the cases where the subtypes are obvious, such as nested within the parent type. But you still have to explicitly specify that the parent type is a sealed/closed hierarchy and the legal subtypes are encoded in the metadata. |
Looking closer, there's another restriction: No such thing exists in C# so the scope is considerably broader. That should be fine, but I'd personally prefer it to be explicit rather than relying on a precise pattern to encode this semantics as a "side effect" of just having a particular accessibility on the constructor - it's too easy to accidentally break the contract. |
Will this fix also apply to any upcoming |
Private class constructor exhaustiveness
Summary
The switch expression already has a notion of exhaustiveness, and there are a variety of cases where switch arms can be confirmed to be exhaustiveness, as defined by reachability of the decision tree.
Right now the only state for reference types tracked for reachability is nullability, e.g. the following switch will not provide a warning about non-exhaustion:
This proposal is to extend tracking to subtypes of classes with only private constructors. For example,
The above should also produce no warnings, as the type checks in the switch expression exhaust all possible values of the input.
Unfortunately, the improvement as specified does have one backwards compatibility problem: it is an error if a switch arm is subsumed by a previous case. This would mean code which currently has a "catch all" pattern to avoid a warning in the above code would now be an error if the change were applied naively. The proposed fix for this is to not provide any warning or error if a case is unreachable only when arbitrary subclasses are not considered reachable.
This proposal should be an unalloyed benefit to programmers -- the compiler is simply smart enough to not provide incorrect warnings.
Design Meetings
The text was updated successfully, but these errors were encountered: