C
C#2mo ago
Crater

Mutable-by-copy pattern?

Curious if anyone has any clever solutions for this, or can point out if I'm missing some knowledge. Basically I've set up some data that's totally immutable (I hope, at least - if I missed something or there's a better approach, feel free to point it out). This is a simplified example similar to what I've got:
public readonly record struct ImmutableThing
{
public readonly string Name;
public readonly int Number;
public readonly ImmutableArray<byte> Stuff;
public ImmutableThing(string name, int number, params byte[] stuff)
{
Name = name;
Number = number;
Stuff = stuff.AsReadOnly();
}
}
public readonly record struct ImmutableThing
{
public readonly string Name;
public readonly int Number;
public readonly ImmutableArray<byte> Stuff;
public ImmutableThing(string name, int number, params byte[] stuff)
{
Name = name;
Number = number;
Stuff = stuff.AsReadOnly();
}
}
But if I wanted to have a mutable version of this data then I'd need to copy it, naturally. The obvious answer is just to have a record struct that's identical to this except not readonly, with all single value types losing their readonly as well, and ImmutableArray becomes an array, etc. Is there any way around that, though? Some way to copy between immutable and mutable without needing essentially a duplicate of the entire type? Maybe some sort of wrapping/boxing that still allows reads, but not modifying the values (or collection members?) without copying? A duplicate wouldn't be that cumbersome for the example given obviously, but with a much larger data structure (and a more complicated and layered construction process, and many different types that would ideally follow this pattern) it starts to seem like an unattractive option. Any ideas/alternatives? Not married to the types/primitives (e.g. record struct) in question either if there's something that might be more fitting, but the idea is generally to keep it in the realm of "fast and solid value-typed data" I guess.
5 Replies
Anton
Anton2mo ago
No there's no way You could not make any fields immutable though And make the whole thing an immutable field where you use it Or pass it with in when used as a parameter
Crater
CraterOP2mo ago
Wouldn't this part still mean mutability on the object's fields? e.g.
public struct NewStruct
{
public readonly Thing NewThing; // previously ImmutableThing in the first snippet
}
public struct NewStruct
{
public readonly Thing NewThing; // previously ImmutableThing in the first snippet
}
would allow this kind of thing:
newStructVar.NewThing.Stuff[0] = 255;
newStructVar.NewThing.Stuff[0] = 255;
I think?
Anton
Anton2mo ago
No unless it's a reference type Or at any point in the chain is there a reference type Otherwise it's the same block of memory You may need to do a second implementation anyway for other purposes If there are many different functions that only work with an uninitialized object to initialize it in a way (builders) you will want to have the separation
MODiX
MODiX2mo ago
Zeth
REPL Result: Success
using JsonSerializer = System.Text.Json.JsonSerializer;

byte[] a = [ 234, 14, 64, 74 ];
var i = new ImmutableThing
{
Name = "abc",
Number = 12,
Stuff = a.ToImmutableArray(),
};

var n = i with
{
Name = "st",
};

Console.WriteLine(JsonSerializer.Serialize(i));
Console.WriteLine(JsonSerializer.Serialize(n));

public readonly record struct ImmutableThing
{
public required string Name { get; init; }
public required int Number { get; init; }
public required ImmutableArray<byte> Stuff { get; init; }
}
using JsonSerializer = System.Text.Json.JsonSerializer;

byte[] a = [ 234, 14, 64, 74 ];
var i = new ImmutableThing
{
Name = "abc",
Number = 12,
Stuff = a.ToImmutableArray(),
};

var n = i with
{
Name = "st",
};

Console.WriteLine(JsonSerializer.Serialize(i));
Console.WriteLine(JsonSerializer.Serialize(n));

public readonly record struct ImmutableThing
{
public required string Name { get; init; }
public required int Number { get; init; }
public required ImmutableArray<byte> Stuff { get; init; }
}
Console Output
{"Name":"abc","Number":12,"Stuff":[234,14,64,74]}
{"Name":"st","Number":12,"Stuff":[234,14,64,74]}
{"Name":"abc","Number":12,"Stuff":[234,14,64,74]}
{"Name":"st","Number":12,"Stuff":[234,14,64,74]}
Compile: 578.385ms | Execution: 96.380ms | React with ❌ to remove this embed.
Crater
CraterOP2mo ago
Kinda, yeah actually. I totally forgot with was a thing. I think its limitations and relatively small scope probably make it not totally useful for what I had in mind necessarily, but still interesting to think about in relation to the data. I guess the challenge with it is just that you don't get a truly mutable copy, you get a short window of mutability on a copy before it becomes read-only again. Granted in some scenarios that's enough, but it's basically just shorthand for feeding the data into its own constructor, I feel like. More elegant for short-term uses, but just as locked down I think. Cool reminder of a lesser-used keyword in any case though, and I'll probably find some use for it while working with this stuff.

Did you find this page helpful?