9

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?

Andi
  • 3,234
  • 4
  • 32
  • 37
  • 1
    did you try to initialize it? like this: `private PdfDoc doc = null;` – vasily.sib Mar 23 '20 at 11:23
  • Similar problem, just moved from constructor to declaration: "Cannot convert null literal to non-nullable reference type." – Andi Mar 23 '20 at 11:25
  • then maybe some-kind-of `private PdfDoc doc = PdfDoc.Empty;`, where `PdfDoc.Empty` is a `static readonly` field? – vasily.sib Mar 23 '20 at 11:29
  • First, I consider this to be a "hack", since in my understanding it is correct that the field is _not_ initialized at the beginning. Second, it is hard to do, since `PdfDoc` is from an external library and to create some "empty" document is another kind of "hack". – Andi Mar 23 '20 at 11:31
  • this is a little "hacky", true. The same way "hacky" as your attempt to make `nullable reference type` to pretend as `non-nullable reference type` – vasily.sib Mar 23 '20 at 11:35
  • 1
    No, it isn't. In my opinion, `null` is not the same as "not initialized". Walking through my code, there is no possibility to read the `pdfDoc` variable before it is initialized. I am _not_ violating the non-null rule, the compiler just seems to be too unable to prove this (yet). – Andi Mar 23 '20 at 11:37
  • _"there is no possibility to read the pdfDoc variable before it is initialized"_ - here you go: `typeof(PdfCreator).GetMethod("Method1", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(new PdfCreator(), new object[0])` – vasily.sib Mar 23 '20 at 11:45
  • 2
    Using reflection you can break all kinds of things, even put `null` values in non-nullable types. Code analysis can only handle regular control flow, and this is fine. – Andi Mar 23 '20 at 11:46
  • No, if you have null-checking in `Method1` (nullable reference type), or if you initialize `doc` in constructor (non-nullable reference type). – vasily.sib Mar 23 '20 at 11:48
  • Any reason why you didn't place the initialization of doc into constructors? It seems you have initialization methods instead of factory methods. Why not `var creator = new PdfCreator(stream);`? – Lasse V. Karlsen May 23 '20 at 16:23
  • Since you are asking for a "clean" solution in C#, there is really no good answer other than maybe Evsei's builder answer, which is basically telling you not to do this. In Kotlin, lateinit should be avoided as much as possible as it is an entirely unclean and unsafe concept, and thus asking for an equivalent to it is essentially asking for an unclean solution, which contradicts you asking for a clean solution. Even so, I recently found myself unable to find a cleaner solution than a lateinit like solution, since it helped me avoid a timing bug with asynchronous code. – still_dreaming_1 Jun 14 '20 at 15:23

5 Answers5

14

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.

still_dreaming_1
  • 8,661
  • 6
  • 39
  • 56
Andi
  • 3,234
  • 4
  • 32
  • 37
  • 3
    This tells the compiler _"let it be null, I understand all consequences and I'm just don't care"_ (not so "clean" approach) – vasily.sib Mar 23 '20 at 12:10
  • 1
    True. But only at the time of this declaration. After that I can not set it to `null` again (which is good). So this seems really to be the "equivalent" of Kotlin's "lateinit". – Andi Mar 23 '20 at 14:17
  • @Andi I think you should accept this as the answer. Thanks for this clever use of the null-forgiving operator. It helped me out just now when I was needing a lateinit like solution. – still_dreaming_1 Jun 14 '20 at 06:38
  • I also agree with @vasily.sib that this is not "clean" in the same sense that lateinit in Kotlin is not clean. In Kotlin, I always try to avoid having to use lateinit, and so far I have succeeded. But if you do need something like lateinit, this is not a bad solution, since the amount of code is so small. If you failed to initialize the member before using it, you are very likely to get a NullReferenceException upon the first attempt at accessing it (though not guaranteed I think), and this would make it clear enough it hasn't been initialized. – still_dreaming_1 Jun 14 '20 at 15:07
  • I also like Sean's solution since the code is more explicit and readable as to intent. Please see my comment under their answer for a way to tweak it to be even cleaner and safer at compile time. Regardless of which solution one chooses, it might be a good idea to somehow combine it with something similar to the builder pattern as discussed in one of the other answers. Ideally the PdfCreator constructor would be private, and a public static builder/factory method would only return a constructed instance after initializing all properties. – still_dreaming_1 Jun 14 '20 at 15:18
  • Please be aware that lateinit in Kotlin is specifically meant for dealing with third party frameworks that force you to use "unclean" patterns. Sometimes, frameworks force you to initialize things in Start or Init methods instead of within the constructor. The type system cannot know about the framework, but since the framework guarantees the call to these methods, this is safe and much better than working with nullables. – Alexander Weickmann Jul 18 '21 at 10:24
2

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.

Rikki Gibson
  • 4,136
  • 23
  • 34
Sean
  • 60,939
  • 11
  • 97
  • 136
  • To generalize this, I could move this check to a global method `T assertNotNull(T? obj)` and call: `var pdfDoc = assertNotNull(this.pdfDoc)`. But still it feels like unnecessary boilerplate code compared to Kotlin's simple `lateinit`. – Andi Mar 23 '20 at 11:44
  • I would use the `[NotNull]` [attribute](https://learn.microsoft.com/en-us/dotnet/csharp/nullable-attributes) to indicate the return value is never null. – Zer0 Mar 23 '20 at 11:50
  • 1
    Well, this should be obsolete using C# 8's new `nullable` compiler feature. – Andi Mar 23 '20 at 11:53
  • @Zer0 - If you're using the new nullable refernce types then it's implicitly not null. – Sean Mar 23 '20 at 11:55
  • Huh? The linked documentation is for C# 8 and nullable reference types. Maybe I should have been more clear. If the intent is `GetDocument` never returns null I would decorate it with `[NotNull]` – Zer0 Mar 23 '20 at 11:58
  • 1
    This would be even cleaner if you changed it to a property with a backing field. The getter would work the same as this GetDocument() method, with the advantage that the setter would prevent it from getting set to null at compile time. – still_dreaming_1 Jun 14 '20 at 15:10
  • Good answer. Let me add: This can be written shorter as `private PdfDocument GetDocument() => pdfDocument ?? throw new InvalidOperationException("not initialized");`. And if you want it to auto-instantiate rather than throwing an exception you could write it as `private PdfDocument GetDocument() => pdfDocument ?? (pdfDocument=new PdfDocument());`. Note how the variable is being updated on the fly if it contains null. You can even turn it into a readonly property like so: `private PdfDocument Document { get => pdfDocument ?? throw new InvalidOperationException("not initialized"); }` – Matt Jul 20 '21 at 08:28
  • @Matt - I'm not convinced that craming everything into an expression-bodied member always helps with readability.. – Sean Jul 20 '21 at 11:55
  • @Sean, you are probably right `int TheAnswerToLifeTheUniverseAndEverything { get => 42; }` ... ;-) – Matt Jul 20 '21 at 13:00
1

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
        }
    }
bugs-x64
  • 11
  • 2
  • I agree it looks like Andi is unknowingly grasping for a builder pattern like solution, or some kind of factory function. If one can use this to avoid the need for leaving a property temporarily uninitialized, that is much better. Even if this did not eliminate the need for a lateinit like solution, I think combining this solution with one of the other solutions would usually be the best option. The public static Builder() function could complete the initializing of PdfBuilder before returning it. – still_dreaming_1 Jun 14 '20 at 15:32
1

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();
   }
Rikki Gibson
  • 4,136
  • 23
  • 34
0

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.

Pharaz Fadaei
  • 1,605
  • 3
  • 17
  • 28
  • I find it doesn't work if `Initialize()` is virtual and overridden and is being set in a child `Initialize()`. – Raid May 13 '23 at 04:26