Using an empty collection initializer on
ImmutableArray creates a new, distinct instance with default equality semantics, whereas
ImmutableArray.Empty is a cached singleton. In C# 12, relying on the initializer for empty checks can lead to unexpected reference inequality failures because each initialization allocates a separate object in memory rather than reusing the global empty state.
The Core Distinction Between Singleton and Instance
The primary difference lies in how these two constructs handle memory allocation and identity.
ImmutableArray<T>.Empty is a static readonly field that provides a pre-allocated, read-only array instance. This singleton pattern ensures that every time you access
Empty, you get the exact same object reference back. This is highly efficient for memory usage and allows for fast reference equality checks using
ReferenceEquals.
In contrast, when you use the collection initializer syntax like
new ImmutableArray<int>() { }, the compiler generates code that calls the constructor and potentially sets up internal fields to represent an empty state. While the resulting array contains zero elements, it is a newly allocated object. Each time this syntax appears in your code, a new heap allocation occurs. This means that two variables initialized this way will not be reference-equal, even though they both logically represent an empty collection.
This distinction is crucial in performance-sensitive applications or when dealing with large numbers of empty collections. The overhead of allocating many small objects can contribute to garbage collection pressure. By using the singleton
Empty property, you eliminate this allocation entirely. It is important to note that while the values inside are identical (both are empty), their identities are fundamentally different. One is a shared resource, and the other is a unique instance.
Behavior of Equality Checks
When working with
ImmutableArray, developers often rely on equality operators to determine if two arrays contain the same data. Understanding the behavior of these operators helps avoid subtle bugs. The
== operator for
ImmutableArray performs a structural comparison by default. This means it checks if the lengths are equal and if all corresponding elements are equal. Since both an empty initializer and
Empty have a length of zero, the structural equality check returns true.
However, reference equality behaves differently. If you use
ReferenceEquals or check identity in a context where object identity matters, the results will diverge.
ReferenceEquals(ImmutableArray<int>.Empty, ImmutableArray<int>.Empty) returns true because it is the same object. Conversely, comparing two instances created via initializers will return false. This can cause issues in hash-based collections if you inadvertently use the array itself as a key without proper hashing overrides, although
ImmutableArray typically handles hashing based on content.
It is also worth noting that
ImmutableArray implements
IEquatable<ImmutableArray<T>>. The
Equals method follows the same structural logic as the
== operator. Therefore, for most logical comparisons, both approaches yield the same result regarding content. The danger arises when code assumes that an empty collection must be the specific singleton instance, perhaps for optimization purposes or strict identity checks.
Performance Implications in C# 12
C# 12 introduces various improvements to language features, but the underlying behavior of
ImmutableArray initialization remains consistent with previous versions. The performance gap between using the singleton and the initializer becomes significant only when dealing with high-frequency allocations. In typical application logic, the difference might be negligible. However, in tight loops or systems processing millions of records, the allocation cost adds up.
Using
ImmutableArray.Empty is generally recommended when you know the collection will be empty. It avoids the overhead of constructor calls and memory allocation. If you are building a collection dynamically and start empty, consider initializing it with
Empty and then using methods like
Add or creating a new immutable array from a list. This approach leverages the immutability model effectively while minimizing unnecessary object creation.
For scenarios where you need a mutable builder,
ImmutableArray.CreateBuilder is the appropriate choice. It allows you to add items efficiently before calling
ToImmutable(). If the builder ends up empty, calling
ToImmutable() on an empty builder may still return a new instance depending on the implementation details, so checking against
Empty explicitly can be a good practice for normalization.
Best Practices for Developers
To maintain clean and efficient code, always prefer
ImmutableArray<T>.Empty when you intend to represent a known empty collection. Reserve the collection initializer syntax for cases where you are populating the array with specific values at declaration time. For example,
new ImmutableArray<int>() { 1, 2 } is perfectly valid and necessary for inline definition. But for an empty state,
new ImmutableArray<int>() { } is redundant and wasteful.
Additionally, be cautious when writing unit tests that rely on reference equality for empty collections. Ensure your test assertions account for structural equality unless you are specifically testing for the singleton pattern. If you are designing APIs that accept
ImmutableArray, document whether the caller should pass
Empty or if any empty structure is acceptable. This clarity prevents misuse and ensures consistent behavior across the system.
For more information on managing state and memory in C#, you might find insights in
Would a static variable in an instance class cause a memory leak in C?, which discusses similar patterns of static caching and reference management.
Frequently Asked Questions
Does using the collection initializer cause a memory leak?
No, it does not cause a memory leak. The allocated objects are short-lived and will be collected by the garbage collector. However, it increases garbage collection pressure due to the higher number of objects being created compared to using the singleton.
Can I use ImmutableArray.Empty with generic types?
Yes,
ImmutableArray.Empty is generic. You can assign it to any
ImmutableArray<T> variable, such as
ImmutableArray<string>.Empty. The type parameter is inferred or specified explicitly, ensuring type safety without runtime errors.
Is there a performance benefit to using Empty in loops?
Yes, in tight loops where empty collections are frequently created, using
Empty eliminates allocation overhead. This reduces CPU cycles spent on memory management and improves overall throughput, especially in high-performance computing scenarios.
What happens if I mix Empty and initializers in a list?
They coexist normally. A list containing both
ImmutableArray.Empty and an empty initializer instance will have two distinct items. Structural operations like sorting or searching based on content will treat them as equivalent, but reference-based operations will see them as different objects.
Should I always check for null instead of Empty?
ImmutableArray is a value type and cannot be null. You should check for emptiness using the
Length property or the
IsEmpty property. Comparing against
Empty is valid for structural equality but not required for correctness unless you need reference identity.