csharplang/meetings/2018/LDM-2018-05-02.md
2018-05-18 16:04:30 -07:00

4.8 KiB

C# Language Design Notes for May 2, 2018

Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!

Revisit syntax for switch expression

We got a lot of feedback on the decision to use : between pattern and result in each case of a switch expression, and we want to revisit it one more time before releasing the prototype.

state = (state, action) switch {
    (DoorState.Closed, Action.Open)   => DoorState.Opened,
    (DoorState.Opened, Action.Close)  => DoorState.Closed,
    (DoorState.Closed, Action.Lock)   => DoorState.Locked,
    (DoorState.Locked, Action.Unlock) => DoorState.Closed,
    _                                 => state
};
state = (state, action) switch {
    (DoorState.Closed, Action.Open)   -> DoorState.Opened,
    (DoorState.Opened, Action.Close)  -> DoorState.Closed,
    (DoorState.Closed, Action.Lock)   -> DoorState.Locked,
    (DoorState.Locked, Action.Unlock) -> DoorState.Closed,
    _                                 -> state
};
state = (state, action) switch {
    (DoorState.Closed, Action.Open)   ~> DoorState.Opened,
    (DoorState.Opened, Action.Close)  ~> DoorState.Closed,
    (DoorState.Closed, Action.Lock)   ~> DoorState.Locked,
    (DoorState.Locked, Action.Unlock) ~> DoorState.Closed,
    _                                 ~> state
};
state = (state, action) switch {
    (DoorState.Closed, Action.Open)   : DoorState.Opened,
    (DoorState.Opened, Action.Close)  : DoorState.Closed,
    (DoorState.Closed, Action.Lock)   : DoorState.Locked,
    (DoorState.Locked, Action.Unlock) : DoorState.Closed,
    _                                 : state
};

There are pros and cons for all of these. In our previous decision we may have overemphasized one set of examples over another.

:: Has an affinity to switch statements - it just removes the case. However, in switch statements, statements come after, whereas here, expressions do. =>: We have already put those elsewhere (expression bodied members) to say that you yield an expression as a result. However:

  1. Pain to do parsing magic for the last one, maybe. (Not a strong argument)
  2. precedence issues: if you had a ?: in your pattern or when clause it would consume => for a lambda
  3. Looks like a lambda but isn't one
  4. Will drive an expectation of a block body

Conclusion: We're undecided, but we will switch back to => for the prototype.

Nullable special members

static bool IsNullOrEmpty([NotNullWhenFalse] string? s) { }

Should use "When" in the name.

static void AssertNotNull<T>([EnsuresNotNull] T? t) where T : class { }

Not necessarily for fatal methods. Could be a method that initializes a ref:

static void EnsureNotNull([EnsuresNotNull] ref string? s) { if (s is null) s = ""; }

Probably needs "Ensures" here to signal difference from restriction on incoming. Maybe "Ensures" on all of them?

class Object
{
    [NullableEquals] public static bool ReferenceEquals(object? x, object? y) { }
    [NullableEquals] public static bool Equals(object? x, object? y) { }
    [NullableEquals] public virtual bool Equals(object? other) { }
    [NullableEquals] public static bool operator==(object? x, object? y) { }
}

For operators and static binary methods on Object, the compiler can just know. For other user defined equalities we need an attribute. Also IEqualityComparer and IStructuralEqualityComparer, both generic and non. We probably still want to put the attribute on binary equality methods for clarity, but the behavior will be there regardless for the ones the compiler knows about.

For unary instance method equalities, the attribute should just be [NotNullWhenTrue] (since the receiver is inherently not null!). Again, for the ones the compiler knows about (Object.Equals and IEquatable.Equals), we should have the behavior regardless of the attribute, but we should probably put the attribute in anyway.

For not equal methods we may not need an attribute, since non-operator not-equalses are probably exceedingly rare.

We're a little concerned about int-returning comparers, since chasing in a compiler analysis whether their result is 0 is quite subtle. We're open to discussing it again.

As for [NullableEquals] on bool-returning binary equality comparisons, we should make it only work for exactly two parameters for now. If there are more parameters, it's not going to be clear that the first two are the ones being compared.

[NullEquality] for now.

We think we know what it does but we should drill in later to be sure.

For the virtual method, first there is the concern about whether we can expect overrides to follow the contracts.

Aside: when we see o.x with an instance member, then we should know that after, o was not null. In general, should we consider scoping reasoning around throws to the nearest catch clause?