csharplang/meetings/2017/LDM-2017-05-31.md
2019-04-13 14:44:05 -04:00

7.3 KiB

C# Language Design Notes for May 31, 2017

Agenda

  1. Default interface members: overriding or implementing?
  2. Downlevel poisoning of ref readonly in signatures
  3. Extension methods with ref this and generics
  4. Default in operators

Default interface members: overriding or implementing?

So far, when a derived interface wants to provide a default implementation for a member declared by a base interface, we have been thinking of it by analogy with overriding of virtual methods in classes, even using the override keyword:

interface I1
{
	void M1();
}

interface I2 : I1
{
	override void M1() { ... }
}

This design philosophy forces us to grapple with a lot of stuff from classes that may not be very useful.

Implementing on the other hand is different. You don't have to match accessibility, etc. You can implement multiple interface members with one class member. A base class can highjack an implementation from an interface:

class C1
{
	public void M1() { ... }
}

class C2 : C1, I2
{
	// class wins
}

Maybe we should try to stay true to the notion of implementing when it comes to default interface members. So "overriding" should instead be expressed as an explicit interface member implementation:

interface I2 : I1
{
	void I1.M1 { ... }
}

This seems to have more of an "interface camp" feel to it: it is more similar to what you can do with interface members today, only you can now do it in interfaces.

We like this approach! Let's investigate some of its consequences.

"Base calls" to default interface implementations

With the "explicit implementation" approach above, how do you call the implementation from another one? Well you have that problem today: you cannot directly call an explicit implementation today; not even from within the implementing class itself.

However, for default interface implementations that's a significant reduction in expressiveness. You'd have to do so via a non-virtual helper method. So the implementation has to anticipate needing to be reused and factor out to a helper method.

For instance, when you do an "implement interface" refactoring, what does it do? Leave out already implemented ones? implement them again with a call to base (if that's possible)? With a throw?

Won't it be common to want to reuse a default implementation, adding "just a little bit more", i.e., calling base? Well, that gets into an accessibility game: the accessibility for ultimate use, versus the accessibility for being non-virtually called as base.

Ambiguity of base calls

If we allow base calls to default implementations, there are two different kinds of ambiguities.

Ambiguity of declaration:

interface I1 { void M() { ... } }
interface I2 { void M() { ... } }
interface I3 : I1, I2 { void N() { base.M(); } } // which declaration of M()?

Ambiguity of implementation:

interface I1 { void M(); }
interface I2: I1 { void I1.M() { ... } }
interface I3: I1 { void I1.M() { ... } }
interface I4: I2, I3 { void N() { base.M(); } } // which implementation of M()?

You need to get into weird-ish stuff like this:

base(I3).(I1.M1)<string>(1, 2, 3); // Call I3's implementation of I1's M1

If we want to do something like this, we would also consider allowing classes to pick which one they grab the implementation from: even though classes have no ambiguity about which is the most specific override, you could potentially allow "reaching around" and grabbing an older one.

Deimplementation

When we were on the override plan, we intended to allow "reabstraction", where an interface could override a base method to not have an implementation.

In this new scheme, should we allow "deimplementation" - the ability for an interface to say that an inherited member does not have a default implementation, contrary to what a base interface said?

The meaning would be "I declare that the default implementation is not useful (or is harmful) to classes implementing me."

This would probably be rare. People can implement to throw an exception instead.

Conclusion

Strangely this approach moves the design closer to its intended purpose. It keeps it about implementation, without mixing inheritance into it.

It does mean you have to type the interface name every time you want to provide a new implementation for a base interface member. Should we have a shorthand for when the interface name is unambiguous and obvious? No. We haven't needed it for explicit implementation in classes, we probably don't need it now.

Let's think about whether we can live entirely without base calls to default implementations, and come back to this when we've mulled it a bit.

Downlevel poisoning of ref readonly in signatures

We currently poison with a modreq all the places in signatures where unaware compilers could do something unsafe (write into read-only memory) by using a ref readonly as if it was a mutable ref.

Modreqs aren't very discerning, so a virtual method cannot even be called by unaware compilers, even when only overriding is unsafe. We accept this degree of granularity as the best possible state of affairs.

The only role of the modreq is to poison unaware compilers. The actual information about read-only-ness of refs is carried in attributes. Should we reject methods that have the attributes but not the modreq? It makes it harder to relax it later. But it protects the contract from manual finagling. So yes, refuse to consume such methods.

It's hard for us to add a new modifier. We may have to reuse one. If we had our own, we could avoid using attributes altogether, just make it modopt or modreq depending on whether it is required for safety. Let's keep this idea around, in case we do get to have our own.

Extension methods with ref this and generics

Extension methods will be allowed to take value types by ref or ref readonly. It doesn't make sense for reference types, so those are disallowed. However, what about unconstrained (or interface constrained) type parameters?

Should we allow so as to get the benefit when a type argument happens to be a value type? Probably not.

  • For mutable ref it seems a little dangerous and surprising that an extension method can modify a variable of reference type
  • For readonly ref, chances are it would lead to much unintended copying, as the readonly-ness would cause the value to get copied whenever you tried to do something useful with it in the body (e.g. based on an interface constraint).

Conclusion

Don't allow.

Default in operators

default as an operand to unary or binary operators would sometimes work, and sometimes not, depending on whether there happens to be a best operator across all available predefined or user-defined ones for all types:

        var a = default + default;  // error
        var b = default - default;  // ok
        var c = default * default;  // ok
        var d = default / default;  // error

This feels arbitrary. But worse, it is actually a recipe for future compat disasters. Imagine we added a - operator to, say, arrays in the future. Now the second line above would break, because the int overload of the pre-defined - operator would no longer happen to be best.

Conclusion

Don't allow default as an operand to a unary or binary operator. We need to protect the ability to add new operator overloads in the future.