csharplang/meetings/2021/LDM-2021-02-08.md
2021-02-08 20:13:46 -08:00

7.5 KiB

C# Language Design Meeting for Feb 8th, 2021

Agenda

  1. Virtual statics in interfaces
    1. Syntax Clashes
    2. Self-applicability as a constraint
    3. Relaxed operator operand types
    4. Constructors

Quote(s) of the Day

  • "If you need to kick yourself I see that [redacted] has a foot in the chat you can kick yourself with"
  • "Are we at the point where we derail your meeting with other proposals?"
  • "It's the classic, noble art of retconning"

Discussion

Today, we want to take a look at a few top-level design decisions for virtual statics in interfaces that will drive further design and implementation decisions for this feature.

Syntax Clashes

C# 8 brought 2 new features to interfaces: default members, and non-virtual static members. This sets up a clash between static virtual members and static non-virtual members. In pre-C# 8, interface members were always virtual and abstract. C# 8 blurred the line here: private and static members in interfaces are non-virtual. For private members, this makes perfect sense, as the concept of a virtual private method is an oxymoron: if you can't see it, you can't override it. For statics, it's a bit more unfortunate because we now have inconsistency in the default-virtualness of members in the interface. We have a few options for addressing this inconsistency:

  1. Accept life as it is. We'd require abstract and/or virtual on static members to mark them as such. There is some benefit here: static has meant something for 21 years in C#, and that is not virtual. Requiring a modifier means this does not change.
  2. Break the world. Make static members in interfaces mean static virtual by default. This gets us consistency, but breaks consumers in some fashion and/or introduces multiple dialects of C# to support.
  3. We could introduce a bifurcation in interfaces with a virtual modifier on the interface declaration itself. When marked with virtual, static members of the interface are automatically virtual by default. This is very not C#-like: we require static on all members of static classes, why would virtual interfaces be any different here? And how would you define the existing non-virtual members in such an interface?

Options 2 and 3 also have a question of how they will apply to class members. Due to the size of the changes required we may have to split virtual static members, shipping with just interfaces in the first iteration and adding class types at a later point. However, we need to make sure that we design the language side of these changes together, so when class virtual statics are added they don't feel like an afterthought. The second and third proposals would likely need to have the first proposal for the class version of the feature anyway. While it would be consistent with instance members, neither of them would totally eliminate the needs to apply the modifiers to interface members.

Conclusion

We will require abstract or virtual be applied to a virtual static member. We will also look at allowing these modifiers for instance interface methods, even though they are redundant, much like we allow public on members today.

Self-applicability as a constraint

abstract static members introduce an interesting scenario in which an interface is no longer valid as a substitute for a type parameter constrained to that interface type. Consider this scenario:

interface IHasStatics
{
    abstract static int GetValue(); // No implementation of GetValue here
}

class C : IHasStatics
{
    static int GetValue() => 0;
}

void UseStatics<T>() where T : IHasStatics
{
    int v = T.GetValue();
}

UseStatics<C>();           // C satisfies constraint
UseStatics<IHasStatics>(); // Error: IHasStatics doesn't satisfy constraint

The main question here is what do we do about this? We have 2 paths:

  1. Forbid interfaces with an abstract static from satisfying a constraint on itself.
  2. Forbid access to static virtuals with a type parameter unless you have an additional constraint like concrete.

Option 2 seems weird here. Why would a user have constrained to a type that implements an interface, rather than just taking the interface, unless they wanted to use these methods? Yes, adding an abstract static method to an interface where one does not exist today would be a breaking change for consumers, but that's nothing new: that's why we added DIMs in the first place, and it would continue to be possible to avoid the break by providing a default method body for the virtual method, instead of making it abstract.

Conclusion

We choose option 1.

Relaxed operator operand types

Today, C# requires that at least one of the operand-types of a user-defined operator be the current type. This breaks down with user-defined virtual operators in interfaces, however, as the type in the operator won't be the current type, it will be some type derived from the current type. Here, we naturally look at self types as a possible option. We are concerned with the amount of work that self-types will require, however, and aren't sure that we want to tie the shipping of virtual statics to the need for self types (and any other associated-type feature). We also need to make sure that we relax operators enough, and define BCL-native interfaces in a way, such that asymmetric types are representable. For example, a matrix type would want to be able to add a matrix and a numeric type, and return a matrix. Or byte's + operator, which does not return a byte today. Given that, we think it is alright to ship this feature and define a set of operator interfaces without the self type, as we would likely be forced to not use it in the general interfaces anyway to keep them flexible enough for all our use cases.

Conclusion

We're ok with relaxing the constraints as much as we need to here. We won't block on self types being in the language.

Constructors

Finally, we looked at allowing or disallowing constructors as virtual in interfaces. This is an interesting area: either derived types would be required to provide a constructor that matches the interface, or derived types would be allowed to not implement interfaces that their base types do. The feature itself is a parallel to regular static methods; in order to properly describe it in terms of static methods, you'd need to have a self type, which is what makes it hard to describe in today's C# terms in the first place. Adding this also brings in the question of what do we do about new()? This may be an area where we should prefer a structural approach over a nominal approach, or add a form of type classes to the language: if we were to effectively deprecate new(), that would mean that every type that has a parameterless constructor would need to implement an IHasConstructor interface instead. And we would need to have infinite variations of that interface, and add them to every type in the BCL. This would be a serious issue, both in terms of sheer surface area required and in terms of effect on type loads and runtime performance penalties for thousands and thousands of new types.

Conclusion

We will not have virtual constructors for now. We think that if we add type classes (and allow implementing a type class on a type the user doesn't own), that will be a better place for them. If we want to improve the new() constraint in the mean time, we can look at a more structural form, and users can work around the lack of additional constraints for now by using a static virtual.