Back to Csharplang

C# Language Design Meeting for July 30th, 2025

meetings/2025/LDM-2025-07-30.md

latest10.0 KB
Original Source

C# Language Design Meeting for July 30th, 2025

Agenda

Quote(s) of the Day

  • (Attendee lifting their cat) "is having their Lion King moment!"

Discussion

'/main' and top-level statements

Pull request: https://github.com/dotnet/csharplang/pull/9546

Should we allow specification of an entry point with /main even when there are top-level statements? This would help some scenarios using the new single-file app.cs capability.

The core concern is that the top-level program cannot then be called programmatically from other entry points, because it is generated with an unspeakable name. Arguably, this should be paired with a proposal to make top-level programs callable.

Conclusion

We're ok adopting the proposal now, so that scenarios can start lighting up, and then likely embracing a design for callable top-level programs in the future.

Union proposals overview

As a PSA, the Union proposals overview is a useful document to follow the many union-related proposals currently being investigated and adopted.

Add type parameter restriction to closed hierarchies

Pull request: https://github.com/dotnet/csharplang/pull/9560

This attempts to plug a hole in the current Closed hierarchies proposal where "fresh" type parameters on a class deriving from a closed class can hamper exhaustiveness in switches:

csharp
closed class C<T> { ... }
class D<T> : C<T> { ... }
class E<T, U> : C<T> { ... }

C<int> c = ...;
_ = c switch
{
    D<int> d => ...,
    E<int, ???> e => ..., // Can't put anything as '???' to exhaust all instantiations of `E`
}

While there only exists one instantiation of D<...> that is implicitly convertible to C<int> (namely D<int>), there's an infinite number of instantiations of E<...> (anything where the first type argument is int) that are. So even though C is closed, it is not possible to write an exhaustive switch without a fallback clause, undermining the point of declaring C closed in the first place.

We would like to catch this problem at the declaration of E<T, U>. The proposal suggests doing this by requiring all type parameters of a class deriving from a closed class to be used in the base class specification. E.g. in class D<T> : C<T> all type parameters (T) are being used in C<T>, whereas in E<T, U> : C<T> only T is used in C<T>, not U.

We think the proposed rule goes a long way to plug the hole, but are not entirely convinced it is enough. We discussed adding stronger rules, e.g. requiring type parameters to be used as, not just in, type arguments to the base class, or preventing derived classes from being explicitly generic altogether (they would have to be nested).

We also touched on generic variance. Class type parameters cannot be variant today, but in the future we might extend the closed hierarchies feature to interfaces, or even allow classes to express variance. If we were to do either we would need to reevaluate whether we have the right rules in place here: With variance, more than one generic instantiation of a derived type could still be implicitly convertible to the base type; however there might be one that is the base type of all the other instantiations and could be used in exhaustive switches.

Conclusion

We adopted the proposal as an improvement; however, we need to keep thinking about this; in particular, a constructive description of how to get from a generic instantiation of the closed class to at most one of each derived class would be useful.

We think the proposed tighter restrictions are too draconian and affect real scenarios. As for variance, we can cross that bridge when we get there; as a fallback we can always forbid variant type parameters in a closed type.

Standard unions

Specification: https://github.com/dotnet/csharplang/blob/5736dbf3d4008498acb120669a4ede96dd69e104/meetings/working-groups/discriminated-unions/standard-unions.md

The proposal is to offer standard generic Union<...> types in the BCL, similar to e.g. Action and Func types:

csharp
public union Union<T1, T2>(T1, T2);
public union Union<T1, T2, T3>(T1, T2, T3);
... // etc

Unlike e.g. ValueTuple<...> the current proposal does not bother with a "nesting" scheme, but just caps the number of case types offered in this way.

The benefit is similar to e.g. Func: Smaller scenarios, where a named abstraction isn't really helpful, can just grab a standard union type "off the shelf".

There is a small concern that it won't be intuitive that these don't unify across different orders of the case types; e.g., Union<string, bool> vs Union<bool, string>. However, since the types are not special language syntax but just library types, hopefully it will be unsurprising that they act as such.

Conclusion

Adopted as proposed, with the understanding that the .NET library team has final say in the shape of these types.

Union interfaces

Specification: https://github.com/dotnet/csharplang/blob/5736dbf3d4008498acb120669a4ede96dd69e104/meetings/working-groups/discriminated-unions/union-interfaces.md

Non-generic 'IUnion' interface

The proposal is to add an IUnion interface that simply enshrines the Value property of unions:

csharp
public interface IUnion
{
    object? Value { get; }
}

Union declarations in C# would automatically add this interface to the lowered type.

This allows code to dynamically test if something is of a union type, and if so, get at its contained value. It also can be used as a marker for e.g. the compiler to treat a type as a union type; see Custom unions below.

In some ways this is analogous to ITuple which allows tuple values to be dynamically discovered and interacted with.

Conclusion

Adopted as is.

Generic 'IUnion' interface

The proposed interface declares a static method to test whether an incoming value could be contained in the implementing union type, and if so creates such a union value from it:

csharp
public interface IUnion<TUnion> : IUnion where TUnion : IUnion<TUnion>
{
    static abstract bool TryCreate<TValue>(TValue value, [NotNullWhen(true)] out TUnion union);
}

Again union declarations in C# would automatically add this interface to the lowered type.

We noted that the method is a "GVM" - a generic virtual method, which is bad for AOT compilation and generally avoided, especially in widely-used library types. The type is generic only so that it can avoid boxing when the incoming value is of a value type. We could instead declare the method non-generically as:

csharp
public interface IUnion<TUnion> : IUnion where TUnion : IUnion<TUnion>
{
    static abstract bool TryCreate(object? value, [NotNullWhen(true)] out TUnion union);
}
Conclusion

We're on board with the value of the proposal, but want to go with the non-GVM version of the TryCreate method.

'IUnionUnboxed' interface

The proposed interface would allow a general means of querying union types for content of a given type, without causing boxing of the value. This is as an alternative to asking though the IUnion.Value property described above, which would cause boxing of value types.

Conclusion

We summarily dismissed this proposal, as it relies fundamentally on a GVM. As with the previous interface we want to start from a position of preferring potential boxing to the risks of GVMs.

Custom unions

Specification: https://github.com/dotnet/csharplang/blob/5736dbf3d4008498acb120669a4ede96dd69e104/meetings/working-groups/discriminated-unions/custom-unions.md

The overall proposal is to allow developers to write their own union types "manually" instead of having them generated by the new union syntax. When following the proposed pattern (implement the non-generic IUnion interface and offer a public constructor for each case type), the compiler would allow the type to be consumed equally to a compiler-generated union type, in terms of:

  • Implicit conversion from each case type to the union type,
  • Pattern matching that implicitly indirects through the Value property, and
  • Exhaustiveness in switch expressions.

This allows existing types that are already effectively union types to be imbued with first-class language behavior, as well as new union types that for various reasons use other strategies for e.g. content storage.

Conclusion

Adopted. It's possible the specific pattern will evolve slightly as we build the feature, but this is a great place to start from.

Non-boxing access pattern

Specification: https://github.com/dotnet/csharplang/blob/5736dbf3d4008498acb120669a4ede96dd69e104/meetings/working-groups/discriminated-unions/non-boxing-access-pattern.md

For efficiency, some custom unions may store their contents in a non-boxing manner. However, such efficiency gains are thwarted if the value can only be accessed through the Value property, which would just box them anyway.

The proposal is for the compiler to recognize an optional alternative access pattern on unions that avoids boxing. With this pattern, the custom union type offers a HasValue property for null checking, and a TryGetValue method for each case type, e.g.:

csharp
public struct MyUnion : IUnion
{
    public bool HasValue => ...;
    public bool TryGetValue(out Case1 value) {...}
    public bool TryGetValue(out Case2 value) {...}
    
    object? IUnion.Value => ...;
}

Pattern matching will then be implemented in terms of these members rather than the Value property when possible.

Conclusion

Adopted.