csharplang/meetings/2019/LDM-2019-04-15.md
2019-04-18 15:09:09 -07:00

5.3 KiB

C# Language Design Notes for Apr. 15, 2019

Agenda

  1. CancellationToken in iterators
  2. Implied nullable constraints in nullable disabled code
  3. Inheriting constraints in nullable disabled code
  4. Declarations with constraints declared in #nullable disabled code
  5. Result type of ??= expression
  6. Use annotation context to compute the annotations?
  7. Follow-up decisions for pattern-based Index/Range

Discussion

Proposal for CancellationToken in iterator method

The proposal is that we allow a parameter of type CancellationToken to an iterator to be decorated with an attribute that specifies that the parameter is to be used in the state machine for cancellation if none is provided.

Conclusion

Approved. We will also allow the token in WithCancellation to override the token provided to the iterator. We will mitigate potentially surprising behavior by making the attribute named DefaultCancellation or similar.

Implied constraints of type parameters in #nullable disabled code

Consider the following declaration:

#nullable disable
interface I
{
    T M<T>(T arg);
}

If you provide an implementation,

#nullable enable
class A : I
{
    public T M<T>(T arg) where T : object
    {
    }
}

this will provide a warning because currently the implied constraint on I.M<T> is object?. The proposal is that the implicit constraint should instead be "oblivous object", meaning that an implicit interface implementation will not provide a warning, and an explicit interface implementation will inherit the oblivious constraint.

Conclusion

Yes, the implied constraint in a disabled context is oblivious. We previously did not consider context when deciding the implicit constraint, but seeing this example, we have decided differently for disabled context.

Inheriting constraints in #nullable disabled code

#nullable enable
public class A
{
    public virtual void F2<T>(T y) where T : class?
    {
    }
}

class B : A
{
#nullable disable
    public override void F2<U>(U y)
    {
        ...
    }
}

What is the implied constraint for the type parameter U?

Conclusion

The constraint is inherited from the definition, which is class?. Since you cannot write a new constraint in the override, there's no point in doing anything but inheriting.

Declarations with constraints declared in #nullable disabled code

#nullable disable
class A<T1, T2, T3> where T2 : class where T3 : object
{
    #nullable enable
    void M2()
    {
        T1 x2 = default; // warning?
        T2 y2 = default; // warning?
        T3 z2 = default; // warning?
    }
}

Proposal 1: The declarations using the type parameters are treated as unconstrained. All lines require a warning by previous decisions.

Proposal 2: The declarations using the type parameters are treated as oblivious. None of the lines produce warnings.

Conclusion

Proposal 2 seems to fit with the philosophy of oblivious, where we do not provide warnings and trust the user to know the correct nullability of their types. Proposal 2 accepted.

Result type of ??= expression

Right now the result type of ??= is always the type of the LHS of the assignment. This is consistent with all other assignment operators in the language. However, there's been some desire for a closer analogy to the ?? instead of to the assignment operators.

That change would mean the following:

int? b = null;
var c = b ??= 5;
// b is int?
// c is int

Conclusion

We will accept the design change to be closer to the behavior of ??. This means that we will no longer maintain the principle that an assignment can be replaced with the variable being assigned in an expression context. While that is unfortunate, we think the use cases are good enough to justify the drawback. The result of the ??= expression will be the result of an implicit conversion from the RHS to the underlying type of the LHS, if one exists. Spec note: if possible it would be cleaner to specify this in terms of ??.

Use annotation context to compute the annotations?

The current LDM decisions have a tentative decision to prohibit inferring oblivious in type inference, as a part of the principle that type inference does not infer "unspeakable" types.

The proposal is that we change the spec to remove this requirement. We do not seem to have any definitive examples of where this rule would reduce confusion or provide a lot of value, and it seems to be fairly complicated both to implement and also to spec the results of all of the situations in which oblivious states can enter into type inference.

Conclusion

While the principle is valuable, it doesn't seem to carry over to this area completely. It's agreed that we will remove this part of the spec and keep our current implementation.

Follow-up decisions for pattern-based Index/Range

A small note that there were the following changes or refinements for pattern-based Index/Range:

  • All members in the pattern must be instance members
  • If a Length method is found but it has the wrong return type, continue looking for Count
  • The indexer used for the Index pattern must have exactly one int parameter
  • The Slice method used for the Range pattern must have exactly two int parameters - When looking for the pattern members, we look for original definitions, not constructed members