TL;DR: If you remove the initialized argument, then EnsureInitialized() will guarantee that the returned object is [NotNull]—even if you pass in an unitialized MyClass? reference—and thus there won't be a need to use the null-forgiving operator (!).
Complete Answer
This is a good question. The answer to your specific example is really easy, but I also want to take the opportunity to address your general question regarding how to handle ref arguments with C# 8.0's nullable reference types. In doing so, that will not only help address other scenarios like this, but also offer an explanation of why the solution to your specific example works.
Specific Example
While the EnsureInitialized() documentation doesn't make this entirely clear, the particular overload you are calling is for situations where you potentially want a null target. Namely, if you were to instead pass a value of true for the initialized parameter, then it would return null. That condition is why it must return a nullable type.
Since you don't want to permit null values—and aren't working with a value type—you simply need to remove the initialized argument. You'll still need to declare your _initializationTarget as nullable, since it isn't being initialized in your constructor. This overload, however, guarantees to the compiler that the target parameter will no longer be null after EnsureInitialized() has run:
private static object _initializationLock = new object();
private static MyClass? _initializationTarget; //marked as nullable
public MyClass GetInstance() =>
LazyInitializer.EnsureInitialized(
ref _initializationTarget,
ref _initializationLock,
() => new MyClass()
);
Notice that while it still passes in a null(able) MyClass?, it confidently returns a MyClass without needing to resort to the null-forgiving operator (!).
General Question
As you dive further into C# 8.0's nullable reference types, you'll discover a number of gaps in Roslyn's static flow analysis which cannot be disambiguated by simply using the ? and ! operators alone. Fortunately, Microsoft foresaw this problem and provided us with a variety of attributes which can be used to provide compiler hints.
Example
Here's a basic example to illustrate the general problem:
public void EnsureNotNull(ref Object? input) => input ??= new Object();
If you call this method with the following code, you'll get a CS8602 warning:
Object? object = null;
EnsureNotNull(ref object);
_ = object.ToString(); //CS8602; Dereference of a possibly null reference
You can mitigate this, however, by applying the [NotNull] hint to the input parameter:
public void EnsureNotNull([NotNull]ref Object? input) => input ??= new Object();
Now, any references to object after EnsureNotNull() is called will be known (by the compiler) to not be null.
EnsureInitialized() overloads
If you evaluate the source code for LazyInitializer with the above in mind, the answer to your specific problem becomes a lot more clear. The overload you were calling marks target as [AllowNull], which is the equivalent to returning MyClass?:
public static T EnsureInitialized<T>([AllowNull] ref T target, ref bool initialized, [NotNull] ref object? syncLock, Func<T> valueFactory) => …
By contrast, the overload that I've recommended implements the [NotNull] attribute discussed above, which is the equivalent of returning MyClass:
public static T EnsureInitialized<T>([NotNull] ref T? target, [NotNull] ref object? syncLock, Func<T> valueFactory) where T : class => …
If you evaluate the actual logic, you'll see they're basically the same, except the former includes an escape clause for the scenario where initialized is true—and, therefore, allows target to potentially remain null.
Since you know that you're working with a class and don't want a null value, however, the latter overload is the best choice, and implements the exact practice outlined in my answer to your general question.