csharplang/meetings/2017/LDM-2017-05-31.md
2017-06-01 13:52:44 -07:00

4.7 KiB

C# Language Design Notes for May 31, 2017

Raw notes, yet to be cleaned up - read at your own peril

Default interface members

We are mixing two different concepts: Overriding and implementing.

Current design:

interface I1
{
	void M1();
}

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

This design (thinking of it as overriding, in analogy with classes), forces us to grab 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 with one method. 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 methods. 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.

How do you call the implementation? Well you have that problem today: you cannot call an explicit implementation today.

However, that's a significant reduction in expressiveness. You'd have to do so via a non-virtual helper method. So the implementation needs to anticipate needing to be reused.

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.

Problem with base

There are two ambiguities.

Ambiguity of declaration:

interface I1 { void M() { ... } }
interface I2 { void M() { ... } }
interface I3 : I1, I2 { void N() { base.M(); } } // which 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 M()?

We could say, just no base calls to default implementations, for now.

Alternatively you need to get into stuff like this:

base(I3).(I1.M1)<string>(1, 2, 3);

If we were to come back and do something about this, we could also consider allowing classes to pick which one they grab the implementation for.

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."

Not worth it. This would be rare. People can implement to throw an exception instead.

Conclusion

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

Shorthand for when the interface name is obvious? No. We haven't needed it for explicit implementation in classes, we don't need it now.

base calls? No.

Ref stuff

Poisoning

We currently poison with a modreq all the places in signatures where unaware compilers could do something unsafe (write into readonly memory).

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 as the best state of affairs.

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.

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, and generics

Extension methods will be allowed to take value types by ref or ref readonly. It doesn't make sense for reference types. However, what about unconstrained generics.

Let's allow for ref readonly, disallow for ref.

Default in operators

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

Non-trailing named arguments