Inconsistent use of keywords between classes and interfaces #7209
Replies: 2 comments 10 replies
-
Interfaces have evolved substantially since their initial design in C# 1.0, which is where I believe the majority (if not all) of these warts originate. If interfaces were to be redesigned today they would likely adopt different syntax. What is the intent of this discussion? To suggest changing interface syntax or improve diagnostics? It would be exceptionally difficult to do anything meaningful to the syntax without the potential for massive breaking changes. |
Beta Was this translation helpful? Give feedback.
-
As for the accessibility modifiers on the interface, there is nothing that stipulates that the class must match them. The contract is between the interface and the consumer, not the implementer. As such, the interface stipulates the accessibility modifiers of the members exposed to consumers of that interface. The class is free to implement the interface as it sees fit, though. Explicit implementation is always private, for instance, and that's been true since C# 1.0. |
Beta Was this translation helpful? Give feedback.
-
It is a reasonable assumption to make that interfaces work like abstract classes that provide only methods, and don't have their own storage (fields). It logically follows from that assumption that access modifiers (public, private, protected, internal and static) and inheritance keywords (virtual, abstract, sealed, and override) behave the same as with classes. However, usage of these keyword, as well as compiler messages (or lack thereof) can very easily lead to nonsense and bug-prone code.
I will present several scenarios that illustrate the poor design of interfaces:
Topic 1: protected is not respected
Currently the following is perfectly valid code and compiles with no issues:
output:
The compiler has allowed C to implement MyString+get, and has silently made a new public MyString+set method, different from the protected I.MyString+set, and has not given us a CS0108 warning.
Even stranger is that the following is allowed, and again does not give a CS0108 warning or an error:
This looks like the compiler is generating the backing field for C.MyString, and then implicitly implements I.MyString.
Additionally, explicitly implementing I.MyString causes even more issues.
The following is ok:
But if we change the setter to protected we suddenly cannot use the setter!
Ok getters and setters are weird, but what about normal methods? They don't make sense either:
Unlike the auto-implemented protected set, it is mandatory to implement the purely virtual I.Foo method, but once again we cannot access protected interface methods.
I am aware that the above errors wouldn't appear if C was an interface instead of a class, but given that interfaces HAVE to be implemented by a class eventually, this weird inheritance model is... convoluted to say the least, especially to a newcomer to c#.
Combined with the following issues, I believe making a library with a lot of interfaces and implementor classes becomes an exercise in madness
Topic 2: default implementations are implicitly virtual for implementor classes, and hidden by deriving interfaces
I think this is a very peculiar feature(?) Consider the following code:
output:
The virtual keyword serves no purpose. You either override implicitly in the case of classes, or hide in the case of interfaces.
Furthermore, both I and I2's Foo have been overridden by C's Foo. The compiler has solved the diamond problem for us. Did we mean for it to do it? Maybe.
Would it be easy for a developer to do this by accident? Absolutely.
If during development the author chooses to seal I2.Foo without being aware that C is implicitly overridding it, then other code will potentially misbehave.
It also doesn't help that when a coder wants to be explicit about what he is implementing (
void I.Bar() { //... }
) to avoid such an issue, that now a consumer of the class must always cast C to I before calling I.Bar, even when there is no ambiguity.The following scenario illustrates another "quirk" of default interface implementations:
output:
It would be easy to forget that I.Foo can be overridden, and not only are we overridding it, but we are also allowing it to be overridden by derived classes too!
Moreover, if no virtual or abstract keyword is applied by the base class C1, it is as if it implicitly adds the sealed keyword to the Foo, and the one writing the C2 may become perplexed as to why they can't override I's Foo.
For classes the sealed keyword can only be applied to virtual members, as such I think default interface implementations should be implicitly sealed, the virtual and abstract keywords should be applied explicitly, and implementors must apply the override and/or sealed keywords explicitly.
Topic 3: static operators
Building on 4436's proposal for mandatory operators, I think we should be able to apply or relax more restrictions. Suppose we want to define an interface for a commutative operation:
It is trivial to prove ICommutative's + operator is actually not commutative.
output:
C1 and C2 define their own + operators, and completely ignore ICommutative's + operator. As operators are static members, no hiding warnings are issued. I believe there is merit to allowing the abstract, virtual, override, and sealed keywords to static members of both classes and interfaces.
Bonus Topic 4: variant classes
Due to all the weird quirks with the interactions between classes and interfaces, I see merit to allowing variant classes.
From a language design perspective, the only extra restriction that classes would have would be to not allow covariant and contravariant generic parameters as Fields. Properties on the other hand can be covariant if they are get-only
Beta Was this translation helpful? Give feedback.
All reactions