Championed: Non-defaultable value types · Issue #146 · dotnet/csharplang (original) (raw)

Ported from dotnet/roslyn#15108

Non-defaultable value types

Summary

Certain initialization is necessary for Method Contracts, especially Nullability checking. However, if T is a struct, default(T) makes all members 0 or null and skips certain initialization. Thus, flow analysis is needed for not only contracts and nullability checking for reference types but also "defaultability" checking for value types.

Motivation

Example 1: Thin Wrapper of Reference

From the viewpoint of performance, we sometimes need to create a thin wrapper struct which contains only few members of reference types, e. g. ImmutableArray. Now that performance is one of big themes in C#, needs of such a wrapper struct would increase more and more.

Given the code such as:

struct Wrapper where T : class { public T Value { get; } public Wrapper(T value) { Value = value ?? throw new ArgumentNullException(nameof(value)); } }

If Records and nullability checking would be introduced to C#, this could be shortly written as the following:

struct Wrapper(T Value) where T : class;

T is non-nullable from now, and use T? if null is required. The problem here is that an instance of this struct can contain null by using default(Wrapper<T>) although T Value is supposed to be non-nullable.

Example 2: Value-Constrained Structs

Suppose that you implement a type which value has some constraints: for instance, an integer type which is constrained to be positive:

struct PositiveInt { public int Value { get; } public PositiveInt(int value) { if (!Validate(value)) throw new InvalidOperationException(); Value = value; } public static bool Validate(int value) => value > 0 }

If Records and Method Contracts would be introduced to C#, this could be shortly written as the following:

struct PositiveInt(int Value) requires Value > 0;

This looks good at first glance, but, default(PositiveInt) makes 0 which is an invalid value.

Proposal

Nullability checking proposed in dotnet/roslyn#5032 is based on flow analysis. Defaultability checking is essentially the same analysis as the nullability checking and could be implemented by the same algorithm.

However, in most case, default(T) is a valid value of T. Therefore, some kind of annotation would be required. For instance, how about using a [NonDefault] attribute on structs.

[NonDefault] struct Wrapper(T Value) whereT : class;

[NonDefault] struct PositiveInt(int Value) requires Value > 0;

I call these types "non-defaultable value types". As with non-nullable reference types, defaultablity should be checked by using flow anlysis.

PositiveInt x = default(PositiveInt); // warning PositiveInt? y = default(PositiveInt); // OK PositiveInt z = y; // warning PositiveInt w = y ?? new PositiveInt(1); // OK

If T is a non-defaultable value type, T? doesn't require Nullable<T> because default(T) is an invalid value and can be used as null.

Moreover, ordinary value types should not be allowed to have members of non-nullable reference types. Only types that be allowed it would be reference types and non-defaultable value types.