C# non-nullable field: Lateinit?

6.9k Views Asked by At

I am wondering how to use late-initialized class fields in C# with nullable reference types. Imagine the following class:

public class PdfCreator { 

   private PdfDoc doc;

   public void Create(FileInfo outputFile) {
       doc = new PdfWriter(outputFile);
       Start();
   }

   public void Create(MemoryStream stream) {
       doc = new PdfWriter(stream);
       Start();
   }

   private void Start() {
      Method1();
      // ...
      MethodN();
   }

   private void Method1() {
      // Work with doc
   }

   // ...

   private void MethodN() {
      // Work with doc
   }
}

The above code is very simplified. My real class uses many more fields like doc and also has some constructors with some arguments.

Using the above code, I get a compiler warning on the constructor, that doc is not initialized, which is correct. I could solve this by setting the type of doc to PdfDoc?, but then I have to use ?. or !. everywhere it is used, which is nasty.

I could also pass doc as a parameter to each method, but remember that I have some fields like this and this violates the clean code principle in my eyes.

I am looking for a way to tell the compiler, that I will initialize doc before using it (actually I do it, there is no possibility for the caller to get a null reference exception!). I think Kotlin has the lateinit modifier exactly for this purpose.

How would you solve this problem in "clean" C# code?

5

There are 5 best solutions below

6
On BEST ANSWER

Best solution I found so far is this one:

private PdfDoc doc = null!;

This removes all compiler warnings by using the null-forgiving operator introduced in C# 8. It allows you to use a value as if it were not null. Therefore, one way it can be used is when you need something similar to Kotlin's "lateinit". Unlike Kotlin's lateinit, it will actually get initialized to null here, which will be allowed by both the compiler and runtime. If you later use this variable where null is not expected, you can get a NullReferenceException, and the compiler will not warn you that it could be null, as it will think it is not null. Kotlin's lateinit has a subtle difference where if you accessed a lateinit property before it was initialized, it throws a special exception that clearly identifies the property being accessed and the fact that it hasn't been initialized.

9
On

Late initialization can be tricksy with nullable reference types

One option is to make the member variable nullable and add a function to wrap the access:

private PdfDocument? pdfDocument = null;

private PdfDocument GetDocument()
{
  if(pdfDocument == null) throw new InvalidOperationException("not initialized");

  return pdfDocument;
}

Note that the compiler doesn't warn on this method, because it recognizes that the method will only return if pdfDocument is not null.

With this is place you can now change your methods to this:

private void Method1() 
{
  var doc = GetDocument();

  // doc is never null
}

Now your code more accurately models the intent. pdfDocument can be null, even if just for a short time, and your methods can access the document knowing they will never get back null.

1
On

your code seems like a builder patter, read more about it

    public class PdfBuilder
    {
        private PdfDoc _doc;

        private PdfBuilder(PdfDoc doc)
        {
            _doc = doc;
        }

        public static PdfBuilder Builder(FileInfo outputFile)
        {
            var writer = new PdfWriter(outputFile);
            return new PdfBuilder(writer.ReadPdfDoc());
        }

        public void Build() 
        {
            Stage1();
            StageN();
        }

        private void Stage1() 
        {
            // Work with doc
        }

        // ...

        private void StageN() 
        {
            // Work with doc
        }
    }
0
On

It sounds like what you want is a way to add nullability preconditions onto your methods (i.e. if I call this instance method when fields X, Y, or Z might be null, warn me). The language doesn't have that at this point. You are welcome to put in a language feature request at https://github.com/dotnet/csharplang.

Depending on exactly how initialization of your type works, there will be different patterns that would work as alternatives. It sounds like you have the following phases:

  1. The constructor is called with some arguments and the arguments are saved to fields.
  2. An overload of Create() is called and "late initialized" fields are populated.
  3. Create() calls Start(), which does pretty much everything else.

In this case I would consider extracting the methods which use the late initialized fields to another type:

public class PdfCreator {

    public void Create(FileInfo outputFile) {
        var context = new PdfCreatorContext(new PdfWriter(outputFile));
        context.Start();
    }

    public void Create(MemoryStream stream) {
        var context = new PdfCreatorContext(new PdfWriter(stream));
        context.Start();
    }

    private struct PdfCreatorContext
    {
        private PdfDoc doc;
        internal PdfCreatorContext(PdfDoc doc)
        {
            this.doc = doc;
        }

        internal void Start() {
            Method1();
            // ...
            MethodN();
        }

        internal void Method1() {
            // Work with doc
            doc.ToString();
        }

        // ...

        internal void MethodN() {
            // Work with doc
        }
    }
}

It's possible the usage of the class is more complex than this, or issues like asynchrony and mutation make it impractical to use a struct. In this case you could at least require your methods to check their own preconditions before the compiler lets them use the nullable fields:

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

public class PdfCreator { 
   PdfDoc? doc;

   [Conditional("DEBUG"), MemberNotNull(nameof(doc))]
   private void AssertInitialized()
   {
      Debug.Assert(doc != null);
      // since the real thing has many nullable fields, we check them all
      // in here, and reference them all in the MemberNotNull attribute.
   }

   private void Method1() {
      AssertInitialized();
      // Work with doc with the assumption it is not-null.
      // In the case that any method is called with an unexpected
      // null field in debug builds, we crash as early as possible.
      doc.ToString();
   }

   private void Method2() {
      // oops! we didn't AssertInitialized, so we get a warning.
      doc.ToString(); 
   }
}

Note that [MemberNotNull] is currently only available in the .NET 5 preview. In .NET Core 3, you could write a Debug.Assert that checks all the nullable fields you need at the call site.

   private void Method1() {
      Debug.Assert(doc != null);
      doc.ToString();
   }
1
On

It might not directly solve the OP's problem but as searching for 'late init' brought me here, I am going to post.

Although you can use the null! technique explained in other answers, if you initialize your non-null class members in the constructor not directly but through some helper methods, there is a more elegant way to declare this. You need to use MemberNotNull(nameof(Member)) attribute on your helper method.

public class TestClass
{
    private string name;

    public TestClass()
    {
        Initialize();
    }

    [MemberNotNull(nameof(name))]
    private void Initialize()
    {
        name = "Initialized";
    }
}

This way, the compiler will no longer argue about non-nullable name not being set after exiting the constructor, as it knows calling Initialize ensures that the name field is initialized with a non-null value.