Skip to content

Proposal: custom nullable structs #1981

Closed
@YairHalberstadt

Description

@YairHalberstadt

Motivation

Structs are often more performant then classes, as they improve data locality, and reduce stress on the garbage collector. Indeed in high performance code it is advised to use structs wherever possible.

In general I try to replace classes with structs, if the struct would be small enough that copying wont be an issue.

The biggest issue with doing so is that you cannot define, or prevent the default constructor. Neither can you provide default values for field or properties. As such it is impossible to make sure that a struct is initialised to a valid value before it is used, which is a major limitation for many of the cases where I try to replace a class with a struct. Indeed I would say it is the single most common reason why I do not replace a class with a struct.

There is a good reason for this. When default(T) is called, where T is a type of struct, a zeroed out struct is allocated. This is necessary for performance reasons, as the garbage collector automatically zeroes out segments it acquires.

It would be very strange for default(T) to return a valid struct which is different to the struct returned by a call to new().

Proposed Solution

This isn't an issue for classes, since a call to default(T) where T is a class returns null, and it is understood that you have to check a class for null before accessing it.

We also have the Nullable struct type, where you have to check if they are valid before using them. However you can't specify that a given struct type should be nullable.

So I propose we allow defining custom nullable structs with the following feature:

For a custom nullable struct type T

  1. T has an extra hidden bool field which indicates if the instance is null.
  2. a call to default(T) returns a null instance of T
  3. it is possible to define a default constructor for T
  4. It is possible to define default values for properties and fields of T
  5. A call on a member of a null instance of T results in a null reference exception
  6. A T[] is initialised with null Ts by default

Suggested Syntax and Example

public struct ArrayWrapper<T>?
{
    private readonly T[] _array = new T[1]; // can define default value for this field
    public T this[int index] {get => _array[index]; set => _array[index] = value;}
}

// or equivalently:

public struct ArrayWrapper<T>?
{
    private readonly T[] _array;
    public T this[int index] {get => _array[index]; set => _array[index] = value;}

    public ArrayWrapper<T>?() => _array = new T[1]; //can define default constructor
}

...

var arrayWrapper = default(ArrayWrapper<int>?);

Console.WriteLine(arrayWrapper[0]); //throws nullReferenceException. Should also be a compiler warning 

arrayWrapper = new ArrayWrapper<int>?();
Console.WriteLine(arrayWrapper[0]); //prints 0

var array = new ArrayWrapper<int>?[0];
arrayWrapper = array[0];

Console.WriteLine(arrayWrapper[0]); //throws nullReferenceException. Should also be a compiler warning 

Motivating use case

As just one example of a real life use case, consider this code I was trying to write:

public class Class
{
    public int PropertyOne {get; private set;}
    public string PropertyTwo {get; private set;}

    private Class(){}

    public struct Builder
    {
        private readonly Class _class;
        public void Initialise() => _class = new Class();
        public int PropertyOne{get => _class.PropertyOne; set => _class.PropertyOne = value;}
        public string PropertyTwo{get => _class.PropertyTwo; set => _class.PropertyTwo = value;}
        public Class Build()
        {
            var @class = _class;
            _class = new Class();
            return @class;
        }
    }
}

The problem with this pattern is that you are required to Initialise the Builder before usage, which is prone to errors. This also means you can't use object initialisers with the builder.

The alternative would be using a class, but this adds a new object to the heap for no reason, and doubles the cost of creating a new Class.

With this proposal we could write:

public class Class
{
    public int PropertyOne {get; private set;}
    public string PropertyTwo {get; private set;}

    private Class(){}

    public struct Builder?
    {
        private readonly Class _class;
        public int PropertyOne{get => _class.PropertyOne; set => _class.PropertyOne = value;}
        public string PropertyTwo{get => _class.PropertyTwo; set => _class.PropertyTwo = value;}
        public Class Build()
        {
            var @class = _class;
            _class = new Class();
            return @class;
        }
    }
}

...

var @class = new Class.Builder?
{
    PropertyOne = 17,
    PropertyTwo = "this is a string",
}.Build();

semantics relating to nullable reference types and Nullable

#1865 discusses adding nullable reference type features to nullable value types. Presumable similiar features called be added to custom nullable value types.

Open question would include whether you could refer to the custom nullable type without the ? ending when you know the value is not null.

So for example in the example given above could you declare a new Class.Builder() or only a new Class.Builder?(). Could you say Class.Builder builder = new Class.Builder?() or only Class.Builder? builder = new Class.Builder?().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions