csharplang/meetings/2020/LDM-2020-01-08.md
2020-01-13 14:06:20 -08:00

6.4 KiB

C# Language Design Meeting for Jan. 8, 2020

Agenda

  1. Unconstrained type parameter annotation
  2. Covariant returns

Discussion

Unconstrained type parameter annotation T??

You can only use T? when is constrained to be a reference type or a value type. On the other hand, you can only use T?? when T is unconstrained.

The question is what to use for the following:

abstract class A<T>
{
    internal abstract void F<U>(ref U?? u) where U : T;
}
class B1 : A<string>
{
    internal override void F<U>(ref U?? u) => default; // Is ?? allowed or required?
}
class B2 : A<int>
{
    internal override void F<U>(ref U?? u) => default; // Is ?? allowed or required?
}
class B3 : A<int?>
{
    internal override void F<U>(ref U?? u) => default; // Is ?? allowed or required?
}

Our understanding is that this would be:

abstract class A<T>
{
    internal abstract void F<U>(ref U?? u) where U : T;
}
class B1 : A<string>
{
    internal override void F<U>(ref U?? u) => default; 
    // We think the correct annotation is
    //  void F<U>(ref U? u) where U : class
    // because the type parameter is no longer unconstrained. The `where U : class`
    // constraint is required, as U? would otherwise mean U : struct
}
// We may want to allow U?? even in the above case, so
class B1 : A<string>
{
    internal override void F<U>(ref U?? u) => default; // allowed?
    // This would not require the `where U : class` constraint because `U??` cannot
    // be confused with `Nullable<U>`
}
class B2 : A<int>
{
    internal override void F<U>(ref U?? u) => default;
    // The correct annotation is
    //  void F<U>(ref U u)
    // We could allow
    //  void F<U>(ref U?? u)
    // although it would be redundant
}
class B3 : A<int?>
{
    internal override void F<U>(ref U?? u) => default;
    // The correct annotation is
    //  void F<U>(ref U u)
    // We could allow
    //  void F<U>(ref U?? u)
    // although it would be redundant

In the above, we're wondering whether we should allow ?? without warning even in cases where there's existing syntax to represent the semantics. One benefit is that you could write

abstract class A<T>
{
    internal abstract F<U>(ref U?? u) where U : T;
}
class B1 : A<string>
{
    internal override F<U>(ref U?? u) { }
}

Since the override cannot be confused with U?, there's no need for the where U : class. On the other hand, the benefit seems marginal and it's not needed to represent the semantics. It could also be added later.

Conclusion

We like the syntax ?? to represent the "maybe default" semantics. We think that ?? should be allowed in the cases where we have other syntax to represent the semantics. A warning will be provided and hopefully a code fix to move the code to the "more canonical" form. The syntax ?? is only legal on type parameters in a nullable-enabled context.

We considered using the ? syntax the represent the same semantics, but ruled it out for a couple reasons:

  1. It's technically difficult to achieve. There are two technical limitations in the compiler. The first is design history where a type parameter ending in ? is assumed to be a struct. This has been true in the compiler all the way until nullable reference types in the last release. The second problem is that many pieces of C# binding are very sensitive to being asked if a type is a value or reference type and asking the question can often lead to cycles if asked before the answer is absolutely necessary. However, T? means different things if T is a struct or unconstrained, so finding the safe place to ask is difficult.

  2. Unresolved design problems. In overriding T? means where T : struct, going back to the beginning of Nullable<T>. This already caused problems with T? where T : class in C# 8, which is why we introduced a feature where you could specify T? where T : class in an override, contrary to past C# design where constraints are not allowed to be re-specified in overrides. To accommodate overloads for unconstrained T? we would have to introduce a new type of explicit constraint meaning unconstrained. We don't have a syntax for that, and don't particularly want one.

  3. Confusion with a different feature. If we use the T? syntax, the following would be legal:

class C
{
    public T? M<T>() where T : notnull { ... }
}

what you may think is that M returns a nullable reference type if T is a reference type, and a nullable value type if T is a value type (i.e., Nullable<T>). However, that's not what this feature is. This feature is essentially maybe default, meaning that T? may contain the value default(T). For a reference type this would be null, but for a value type this would be the zero value, not null.

Moreover, this seems like a useful feature, that would be ruled out if we used the syntax for something else. Consider a method similar to FirstOrDefault, FirstOrNull:

public static T? FirstOrNull<T>(this IEnumerable<T> e) where T : notnull

The benefit is that there is a single sentinel value, null, and that the full set of values in a struct could be represented. In FirstOrDefault, if the input is IEnumerable<int> there is no way to distinguish a non-empty enumerable with first value of 0, or an empty enumerable. With FirstOrNull you can distinguish these cases in a single call, as long as null is not a valid value in the type.

Due to CLR restrictions it is not possible to implement this feature in the obvious way, so this feature may never be implemented, but we would like to prevent confusion and keep the syntax available in case we ever figure out how we'd like to implement it.

Covariant returns

We looked at the proposal in #2844.

There's a proposal that for the following

interface I1
{
    object M();
}
interface I2 : I1
{
    string M() => null;
}

I2.M would be illegal as it is, because this is an interface implementation, not an override. Interface implementation would not change return types, while overriding would. Thus we would allow

interface I1
{
    object M();
}
interface I2 : I1
{
    override string M() => null;
}

which would allow the return type to change. However, it would override all Ms, since explicit implementation is not allowed.

Conclusion

We like it in principle and would like to move forward. There may be some details around interface implementation vs. overriding to work out.