Creating a Value Type with Validated Construction
In C#, creating immutable value types with validated construction can be challenging due to the shallow nature of immutability in the language. Exposing arrays on public surfaces can lead to unintended modifications.
One approach involves using a record
class and exposing the data property as an immutable type, such as IReadOnlyList<byte>
. This prevents direct modification of the array's contents, but it still allows for scenarios where the array reference can be mutated.
public record CanFrame { public int Id { get; init; } public IReadOnlyList<byte> Data { get; init; } public CanFrame(int id, byte[] data) { if (data.Length > 8) throw new ArgumentOutOfRangeException(); Id = id; Data = data; } }
To further restrict mutability, properties can be set as get-only, as seen in the following example:
public record CanFrame { public int Id { get; } public IReadOnlyList<byte> Data { get; } public CanFrame(int id, byte[] data) { if (data.Length > 8) throw new ArgumentOutOfRangeException(); Id = id; Data = data; } }
This approach provides immutability, but it lacks the value type semantics and the associated default parameterless constructor.
An alternative approach is to represent the data in a different manner, utilizing an indexer for byte-level access and keeping the data internally as a ulong
. This approach ensures complete immutability and provides an efficient representation in memory.
public struct CanFrame { private readonly ulong data; public int Id { get; } public int Length { get; } public byte this[int index] { get { /* implementation */ } } public void CopyTo(Span<byte> buffer) { // Implementation } public CanFrame(int id, byte[] data) { // Length validation and conversion of the byte array // into a ulong via shifting } // Potentially other constructors, maybe accepting a ReadOnlySpan<byte> // or even a ulong and length }
This approach provides a fully immutable value type with a reasonable default value and efficient memory usage.
Ultimately, the choice of approach depends on the specific requirements and trade-offs for the given scenario.