# Lazy Properties in .NET 10

In this post, I explore a couple of ways to create *lazy properties* in C# and .NET 10. What does a *lazy property* mean in the context of this post? It is an object instance property that gets initialized with a value the first time its getter is invoked. The getter of the property does not need to provide thread-safe initialization. Let's review a couple of solutions available before .NET 10:

All the examples below initialize a `string` property. The initializing method is *static* and extracted as a member of a separate class:

```csharp
public class Shared
{
    public static int _counter = 0;

    public static string Zeros()
    {
        Interlocked.Increment(ref _counter);
        return new string('0', _counter);
    }
}
```

The `Zeros()` method returns a new `string` object containing a number of `0` characters. The number of `0` characters corresponds to the number of times the method has been executed.

## Lazy

The first solution uses the [Lazy](https://learn.microsoft.com/en-us/dotnet/api/system.lazy-1?view=net-10.0) type to solve the problem:

```csharp
public class MyClass0
{
    private readonly Lazy<string> _value = new Lazy<string>(Shared.Zeros);
    public string Value => _value.Value;
}
```

In this case, a readonly `Lazy<T>` property is stored in a field, and the `Value` property returns the lazy field's `Value` property. The `Lazy<T>` has the initialization function referencing the shared *Zeros* method.

The benefit of this solution is that `Lazy<T>` provides thread-safe initialization. By default, only a single thread executes the *Zeros* method. The developer can customize this behavior by providing a `LazyThreadSafetyMode` parameter. This means that, by default, the initialized value is always the `"0"` string.

The downside of this solution is the memory overhead: an instance of `MyClass0` allocates 96+ bytes on the heap (x64 architecture) before even initializing its `Value` property.

> The 96 bytes would be higher in older runtimes, but the JIT got really good at object-stack allocations, reducing the overal heap allocations.

## Null-Coalescing Operator

The `??=` [operator](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator), or null-coalescing assignment operator, assigns the value on its right-hand operand to its left-hand operand when the left-hand operand is `null`. `MyClass1` uses this operator to lazily initialize the backing field for the `Value` property:

```csharp
public class MyClass1
{
    private string? _value;
    public string Value { get => _value ??= Shared.Zeros(); }
}
```

There is still a private backing field `_value` declared to store the initialized value. When the getter is invoked, it uses the `??=` operator to set the value returned by the `Zeros` method.

This solution is not thread-safe. Concurrent invocations of the `Value` property on the same instance of `MyClass1` might result in the `Zeros` method being executed multiple times. In the sample code above, multiple threads accessing the uninitialized property could result in setting a string like `00` or `000`, etc., to the `_value` field. As mentioned above, thread-safe behavior is not a requirement for the exercise in this blog post.

Notice that the `_value` field is not *readonly*. Another method in `MyClass1` could access or change its value after it was initialized. This could be a benefit (if the class needs to mutate the state), but more often, it is a design smell: if the `_value` field is set to `null`, the initializer would run again upon accessing the property, which is often unintended behavior.

The `??=` operator does not compile with (non-nullable) value types. In the above snippet, the type of the property is a reference type, `string`, but the backing field has the type `string?`, which means a string with a 'nullable-reference' annotation. The `_value` field is uninitialized (or null) by default.

The benefit of this solution is that an instance of `MyClass1` allocates only 24 bytes in memory.

## Double Checked Locking

The [Double Checked Locking](https://en.wikipedia.org/wiki/Double-checked_locking) design pattern takes the above solution and makes it thread-safe:

```csharp
public class MyClass2
{
    private string? _value;
    private readonly Lock _lock = new();
    public string Value
    {
        get
        {
            if (_value == null)
            {
                lock (_lock)
                {
                    _value ??= Shared.Zeros();
                }
            }
            return _value;
        }
    }
}
```

This code is significantly longer compared to `MyClass1`, and it also allocates more memory because of the additional field with `Lock` type.

## Incorrect Solutions

In code reviews, I often notice some incorrect patterns. Neither `MyClass3` nor `MyClass4` behaves according to the *lazy property* definition from above.

```csharp
public class MyClass3
{
    public string Value => Shared.Zeros();
}

public class MyClass4
{
    public string Value { get; } = Shared.Zeros();
}
```

`MyClass3` creates a new `string` instance every time the `Value` property is accessed. `MyClass4` only creates a single `string` value, but it is created during object initialization instead of being created on-demand.

## Field Keyword

The [field keyword](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/field) was introduced in C# 14. It can be used to access the compiler-synthesized backing field of a property. The solution in `MyClass5` uses it to lazily initialize the `Value` of a property:

```csharp
public class MyClass5
{
    public string Value { get => field ??= Shared.Zeros(); }
}
```

With `field` there is still a `string` backing field generated for the `Value` property. The next snippet shows the C# compiler generated [IL](https://learn.microsoft.com/en-us/dotnet/standard/managed-execution-process):


```IL
// Fields
.field private string '<Value>k__BackingField'
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
    01 00 00 00
)

// Methods
.method public hidebysig specialname 
    instance string get_Value () cil managed 
{
    // Method begins at RVA 0x209c
    // Header size: 12
    // Code size: 25 (0x19)
    .maxstack 3
    .locals init (
        [0] string
    )

    // Lazy initialization 👇 of the backing field.
    IL_0000: ldarg.0
    IL_0001: ldfld string MyClass5::'<Value>k__BackingField'
    IL_0006: dup
    IL_0007: brtrue.s IL_0018

    IL_0009: pop
    IL_000a: ldarg.0
    IL_000b: call string Shared::Zeros()
    IL_0010: dup
    IL_0011: stloc.0
    IL_0012: stfld string MyClass5::'<Value>k__BackingField'
    IL_0017: ldloc.0

    IL_0018: ret
} // end of method MyClass5::get_Value

.method public hidebysig specialname rtspecialname 
    instance void .ctor () cil managed 
{
    // Method begins at RVA 0x20c1
    // Header size: 1
    // Code size: 7 (0x7)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: call instance void [System.Runtime]System.Object::.ctor()
    IL_0006: ret
} // end of method MyClass5::.ctor

// Properties
.property instance string Value()
{
    .get instance string MyClass5::get_Value()
}
```

The code for the lazy initialization is marked with the *Lazy initialization* comment. It is identical to `MyClass1`'s initialization. That means the same thread-safety observations apply to it. The difference is the field declaration: `.field private string '<Value>k__BackingField'`. In the snippet above, it is a compiler generated field (see the `CompilerGeneratedAttribute`). In `MyClass1`, it is a regular field with nullable annotations:

```IL
.field private string _value
.custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( 01 00 02 00 00 )
```

Being compiler generated means that a developer cannot accidentally access and modify the field's value in regular C# code - without reflection or using `UnsafeAccessor` methods.

The size of an instance of `MyClass5` is 24 bytes: 8 bytes object header, 8 bytes MT, and 8 bytes for the `string` reference in the backing field on x64.

## Performance Comparison

The performance comparison of these solutions clearly show memory overhead of `MyClass0` and `MyClass2`. The `Mean` execution times are not directly comparable because of the different thread‑safety guarantees. However, the thread-safety 'overhead' is easy to notice:

```
| Method   | Mean      | Error     | StdDev    | Gen0   | Allocated |
|--------- |----------:|----------:|----------:|-------:|----------:|
| MyClass0 | 19.711 ns | 0.1748 ns | 0.1635 ns | 0.0153 |      96 B |
| MyClass1 |  3.749 ns | 0.0356 ns | 0.0316 ns | 0.0038 |      24 B |
| MyClass2 | 14.064 ns | 0.1523 ns | 0.1350 ns | 0.0102 |      64 B |
| MyClass5 |  3.753 ns | 0.0405 ns | 0.0379 ns | 0.0038 |      24 B |
```

Note that in the performance measurement above, each benchmark initiated new instance of MyClassX and invoked the getter that initiated a new `string` object. Normally this results an allocation of 48+ bytes on the heap. However, the JIT reduces the heap allocations with object-stack allocation optimization to 24 bytes.

## Conclusion

Lazy properties are a useful pattern in C# and .NET for deferring the initialization of a property until it is needed. This blog post explored several approaches to implementing lazy properties, each with its own trade-offs. The *Field keyword* approach is a modern and elegant solution introduced in C# 14, combining the simplicity of the null-coalescing operator with compiler-generated backing fields to prevent accidental modifications.

When choosing an approach for lazy fields, consider the specific requirements of your application, such as thread safety, memory constraints, and code maintainability. For most scenarios, the `Lazy<T>` type or the `field` keyword will provide a robust and clean solution.