C
C#10mo ago
Stroniax

Builder Pattern as Ref Struct

Hey all, I'm trying to make a source-generated builder which I want to be a struct. I'm running into a problem when trying to chain the entire build, but if I capture the builder in a variable first I can chain it as much as I like. Wondering if there's any language feature/etc that would help me out here? What I'd like is to use the same builder struct instance and not have to copy it with each chained method.
C#
public struct MyBuilder {
public required int Id { get; set; }
public string? Name { get; set; }
public My Build() => new(this);
}
public static class MyBuilderExtensions {
public ref MyBuilder WithId(this ref MyBuilder b, int id) {
b.Id = id;
return ref b;
}
public ref MyBuilder WithName(this ref MyBuilder b, string? name) {
b.Name = name;
return ref b;
}
}
public static class Usage {
public static My Works() {
var builder = new MyBuilder() { Id = 1 };
return builder.WithName("Test").Build();
}
public static My DoesNotWork() {
// CS1510: A ref or out value must be an assignable variable
return new MyBuilder() { Id = 1 }.WithName("Test").Build();
}
}
public partial class My
{
public required int Id { get => throw null; init => throw null; }
public string Name { get => throw null; init => throw null; }
// only if set by builder/init
public bool TryGetName(out string name) => throw null;;
[SetsRequiredMembers]
public My(int id) => throw null;
[SetsRequiredMembers]
public My(MyBuilder b) => throw null;
}
C#
public struct MyBuilder {
public required int Id { get; set; }
public string? Name { get; set; }
public My Build() => new(this);
}
public static class MyBuilderExtensions {
public ref MyBuilder WithId(this ref MyBuilder b, int id) {
b.Id = id;
return ref b;
}
public ref MyBuilder WithName(this ref MyBuilder b, string? name) {
b.Name = name;
return ref b;
}
}
public static class Usage {
public static My Works() {
var builder = new MyBuilder() { Id = 1 };
return builder.WithName("Test").Build();
}
public static My DoesNotWork() {
// CS1510: A ref or out value must be an assignable variable
return new MyBuilder() { Id = 1 }.WithName("Test").Build();
}
}
public partial class My
{
public required int Id { get => throw null; init => throw null; }
public string Name { get => throw null; init => throw null; }
// only if set by builder/init
public bool TryGetName(out string name) => throw null;;
[SetsRequiredMembers]
public My(int id) => throw null;
[SetsRequiredMembers]
public My(MyBuilder b) => throw null;
}
EDIT: The problem occurs because the extension method operates on a ref of the instance, so I must have a field/variable declared beforehand of the type. This means I can't just call new().Extension(), as the result of new() must be assigned to something before it can be used. That is what I am trying to resolve.
32 Replies
Joschi
Joschi10mo ago
May I ask, why are you using a ref struct?
Stroniax
StroniaxOP10mo ago
It’s a builder. Why would it ever need boxed? Specifically it’s only for setting properties of an immutable type.
jcotton42
jcotton4210mo ago
ref structs cannot be used in async methods @Stroniax
Stroniax
StroniaxOP10mo ago
It's... not async? Update: the ref struct part is unimportant really. It could be a regular struct. Its the ref this part I'm having trouble with. OP edited to clarify.
Mkp
Mkp10mo ago
Shouldnt the return just be return b; since b is already a ref
jcotton42
jcotton4210mo ago
no, it'll convert to a value return
Mkp
Mkp10mo ago
interesting Apparently chatgpt says that by avoiding using the constructor directly you can fix this issue, in which MyBuilder Create() => new(); "The issue you're encountering stems from the fact that when you chain methods directly on the result of a constructor, the compiler treats that instance as a temporary value, not as an assignable variable. To resolve this, you can introduce a method that creates and returns a builder instance, allowing you to chain methods on it without running into the assignment error."
Stroniax
StroniaxOP10mo ago
@MKP unfortunately I've tried that as well, to no avail. What's weird is that I don't need to assign intermediate stages.
Mkp
Mkp10mo ago
That "to" in that sentence took me through a run
Stroniax
StroniaxOP10mo ago
The factory is not working for me
No description
Mkp
Mkp10mo ago
Well yeah you have to take it out to a variable thats what a ref return does Please ensure mwe's actually build Like whats this My type
Stroniax
StroniaxOP10mo ago
The point is that this won't build, I'm trying to avoid having to assign to a field/variable first while mutating and returning the same struct instance, that's my primary goal. I've added a My definition if necessary, but the point is that the builder just builds into it.
Mkp
Mkp10mo ago
Well if you want others to triage they should be able to run the code
Stroniax
StroniaxOP10mo ago
If the code ran, I wouldn't be needing help here 🙂 Let me see if I can come up with a more simple example
C#
public struct Example {
public int First;
// for simplicity, I have one member. This is source-generated: imagine the example with twenty members and equally many builder extensions
}
public static class ExampleExtensions {
// I do not want to accept a copy of this instance, but a reference to the instance itself
public static Example WithFirstByCopy(this Example e, int first) {
e.First = first;
// I do not want to return a copy of this instance, but the ref instance I would like to receive
return e;
}
public static ref Example WithFirstByRef(ref this Example e, int first) {
e.First = first;
return ref e;
}
// This does not compile so it does not suit my needs
public static Example DoesNotCompile() {
return new Example().WithFirstByRef(1);
}
// This copies 3? times so it does not suit my needs
public static Example CopiesBuilder() {
return new Example().WithFirstByCopy(1);
}
}
C#
public struct Example {
public int First;
// for simplicity, I have one member. This is source-generated: imagine the example with twenty members and equally many builder extensions
}
public static class ExampleExtensions {
// I do not want to accept a copy of this instance, but a reference to the instance itself
public static Example WithFirstByCopy(this Example e, int first) {
e.First = first;
// I do not want to return a copy of this instance, but the ref instance I would like to receive
return e;
}
public static ref Example WithFirstByRef(ref this Example e, int first) {
e.First = first;
return ref e;
}
// This does not compile so it does not suit my needs
public static Example DoesNotCompile() {
return new Example().WithFirstByRef(1);
}
// This copies 3? times so it does not suit my needs
public static Example CopiesBuilder() {
return new Example().WithFirstByCopy(1);
}
}
@MKP New example that I believe is more concise. Does this help?
Mkp
Mkp10mo ago
I was able to do something horrible to fix this
Stroniax
StroniaxOP10mo ago
Oh shoot. I'm OK with that if it's safe to do? Wait, I didn't see the static self. That's not at all thread safe, is it?
Mkp
Mkp10mo ago
Its not thread safe no And dangerous to use Cuz if you continue any other builder then it explodes
Stroniax
StroniaxOP10mo ago
👍 that is what I was afraid of, thanks for confirming I think I will just document and instruct users to create the builder in a separate line from the build chain. Not as great UX but it's what works.
Mkp
Mkp10mo ago
Yeah you cant return a local ref I mean you could probably force something to happen with memory allocations But at what cost I rather you just create a class instead
Stroniax
StroniaxOP10mo ago
Or what if I do Unsafe.AsRef(in builder) inside the create method? I mean it's... unsafe, I'm sure there's a reason for that which I simply don't understand of why not to use that method here.
Mkp
Mkp10mo ago
Because that ref will get garbage collected You are essentially accessing memory unsafely from a zombie object You cant explicitly force a GC cycle in C# sadge But in theory it will explode randomly if that memory then gets used for something else
Stroniax
StroniaxOP10mo ago
Gotcha. Good reason to avoid it then. ✅
Joschi
Joschi10mo ago
Remember that this builder then will not be able to be used in any async method. Because you simply cannot have a ref struct variable in async code.
Stroniax
StroniaxOP10mo ago
Like Span. It's nbd to call it through a sync method.
C#
public async Task DoSomethingAsync() {
var dto = await GetSomeDto();
var my = Build(dto);
await DoSomethingWith(my);
}
private My Build(Dto someDto) => new MyBuilder().WithId(someDto.Id).Build();
C#
public async Task DoSomethingAsync() {
var dto = await GetSomeDto();
var my = Build(dto);
await DoSomethingWith(my);
}
private My Build(Dto someDto) => new MyBuilder().WithId(someDto.Id).Build();
Mkp
Mkp10mo ago
I dont like your example code
Stroniax
StroniaxOP10mo ago
C#
public record Dto(int Id);
C#
public record Dto(int Id);
@MKP does this help? Based on the example code in OP.
Mkp
Mkp10mo ago
Records are just classes with configured a certain way There are record structs, but thats the same way
Stroniax
StroniaxOP10mo ago
Yes, I'm not sure what you disliked about my example code
Mkp
Mkp10mo ago
Oh. I much prefer you using FooBar Because "My" isnt a noun
Stroniax
StroniaxOP10mo ago
Oh gotcha. I will try to use more common nouns.
Want results from more Discord servers?
Add your server