@@ -11,10 +11,11 @@ internal static class ClassCloneBodyGenerator
1111{
1212 /// <summary>
1313 /// Checks if FormatterServices using statement is needed for the given type.
14+ /// Records don't need FormatterServices since they have copy constructors.
1415 /// </summary>
1516 public static bool NeedsFormatterServices ( TypeModel model )
1617 {
17- return ! model . HasParameterlessConstructor && ! model . IsStruct ;
18+ return ! model . HasParameterlessConstructor && ! model . IsStruct && ! model . IsRecord ;
1819 }
1920
2021 /// <summary>
@@ -32,25 +33,6 @@ public static bool NeedsFormatterServices(IEnumerable<TypeModel> types)
3233 return false ;
3334 }
3435
35- /// <summary>
36- /// Writes the code to instantiate a class, handling both parameterless and non-parameterless constructors.
37- /// </summary>
38- /// <param name="sb">StringBuilder to write to</param>
39- /// <param name="typeName">Name of the type to instantiate</param>
40- /// <param name="hasParameterlessConstructor">Whether the type has a parameterless constructor</param>
41- public static void WriteInstanceCreation ( StringBuilder sb , string typeName , bool hasParameterlessConstructor )
42- {
43- if ( hasParameterlessConstructor )
44- {
45- sb . AppendLine ( $ " var result = new { typeName } ();") ;
46- }
47- else
48- {
49- // Use FormatterServices.GetUninitializedObject to create instance without calling constructor
50- sb . AppendLine ( $ " var result = ({ typeName } )FormatterServices.GetUninitializedObject(typeof({ typeName } ));") ;
51- }
52- }
53-
5436 /// <summary>
5537 /// Writes the complete class clone body code.
5638 /// </summary>
@@ -59,31 +41,41 @@ public static void WriteInstanceCreation(StringBuilder sb, string typeName, bool
5941 /// <param name="useState">Whether to use state tracking for circular references</param>
6042 /// <param name="stateVarName">Name of the state variable (null if not using state)</param>
6143 /// <param name="useNullConditional">Whether to use null-conditional operator (?.) when calling AddKnownRef</param>
44+ /// <param name="sourceVarName">Name of the source variable (usually "source" for classes, "src" for structs after null check)</param>
6245 public static void WriteClassCloneBody (
6346 CloneGeneratorContext ctx ,
6447 string typeName ,
6548 bool useState ,
6649 string ? stateVarName = null ,
67- bool useNullConditional = false )
50+ bool useNullConditional = false ,
51+ string sourceVarName = "source" )
6852 {
6953 var sb = ctx . Source ;
7054 var hasParameterlessConstructor = ctx . Model . HasParameterlessConstructor ;
55+ var isRecord = ctx . Model . IsRecord ;
56+
57+ // For records without circular references, use the idiomatic 'with' expression
58+ if ( isRecord && ! useState )
59+ {
60+ WriteRecordCloneBody ( ctx , typeName , sourceVarName ) ;
61+ return ;
62+ }
7163
7264 if ( useState )
7365 {
7466 // When tracking circular references, we must register the instance BEFORE cloning members
7567 // to avoid infinite recursion (StackOverflowException) in case of cycles.
7668 // This requires us to instantiate first, then register, then assign members.
77- WriteInstanceCreation ( sb , typeName , hasParameterlessConstructor ) ;
69+ WriteInstanceCreation ( sb , typeName , hasParameterlessConstructor , isRecord , sourceVarName ) ;
7870
7971 var stateVarForAdd = stateVarName ?? "state" ;
8072 var nullConditional = useNullConditional ? "?" : "" ;
81- sb . AppendLine ( $ " { stateVarForAdd } { nullConditional } .AddKnownRef(source , result);") ;
73+ sb . AppendLine ( $ " { stateVarForAdd } { nullConditional } .AddKnownRef({ sourceVarName } , result);") ;
8274 sb . AppendLine ( ) ;
8375
8476 foreach ( var member in ctx . Model . Members )
8577 {
86- MemberCloneGenerator . WriteMemberCloning ( ctx , member , "result" , "source" , stateVarForAdd ) ;
78+ MemberCloneGenerator . WriteMemberCloning ( ctx , member , "result" , sourceVarName , stateVarForAdd ) ;
8779 }
8880
8981 sb . AppendLine ( ) ;
@@ -100,7 +92,7 @@ public static void WriteClassCloneBody(
10092 var memberAssignments = new List < string > ( ) ;
10193 foreach ( var member in ctx . Model . Members )
10294 {
103- var assignment = MemberCloneGenerator . GetMemberAssignment ( ctx , member , "source" , "null" ) ;
95+ var assignment = MemberCloneGenerator . GetMemberAssignment ( ctx , member , sourceVarName , "null" ) ;
10496 if ( ! string . IsNullOrEmpty ( assignment ) )
10597 {
10698 memberAssignments . Add ( $ " { assignment } ") ;
@@ -117,18 +109,84 @@ public static void WriteClassCloneBody(
117109 else
118110 {
119111 // Use FormatterServices.GetUninitializedObject to create instance without calling constructor
120- WriteInstanceCreation ( sb , typeName , hasParameterlessConstructor ) ;
112+ WriteInstanceCreation ( sb , typeName , hasParameterlessConstructor , isRecord , sourceVarName ) ;
121113
122114 // Then assign members individually (no state needed for non-circular types)
123115 foreach ( var member in ctx . Model . Members )
124116 {
125- MemberCloneGenerator . WriteMemberCloning ( ctx , member , "result" , "source" , "null" ) ;
117+ MemberCloneGenerator . WriteMemberCloning ( ctx , member , "result" , sourceVarName , "null" ) ;
126118 }
127119 }
128120
129121 sb . AppendLine ( ) ;
130122 sb . AppendLine ( " return result;" ) ;
131123 }
132124 }
125+
126+ /// <summary>
127+ /// Writes the code to instantiate a class, handling both parameterless and non-parameterless constructors.
128+ /// For records, uses the 'with' expression for shallow copy.
129+ /// </summary>
130+ /// <param name="sourceVarName">The name of the source variable (usually "source" for classes, "src" for structs after null check)</param>
131+ private static void WriteInstanceCreation ( StringBuilder sb , string typeName , bool hasParameterlessConstructor , bool isRecord , string sourceVarName = "source" )
132+ {
133+ if ( isRecord )
134+ {
135+ // Records use 'with' expression for shallow copy (modifiable later)
136+ sb . AppendLine ( $ " var result = { sourceVarName } with {{ }};") ;
137+ }
138+ else if ( hasParameterlessConstructor )
139+ {
140+ sb . AppendLine ( $ " var result = new { typeName } ();") ;
141+ }
142+ else
143+ {
144+ // Use FormatterServices.GetUninitializedObject to create instance without calling constructor
145+ sb . AppendLine ( $ " var result = ({ typeName } )FormatterServices.GetUninitializedObject(typeof({ typeName } ));") ;
146+ }
147+ }
148+
149+ /// <summary>
150+ /// Writes the clone body for records using the 'with' expression.
151+ /// Only includes members that need deep cloning in the 'with' expression.
152+ /// </summary>
153+ /// <param name="sourceVarName">The name of the source variable (usually "source" for classes, "src" for structs after null check)</param>
154+ private static void WriteRecordCloneBody ( CloneGeneratorContext ctx , string typeName , string sourceVarName = "source" )
155+ {
156+ var sb = ctx . Source ;
157+
158+ // Collect members that need deep cloning (not safe types)
159+ var deepCloneAssignments = new List < string > ( ) ;
160+ foreach ( var member in ctx . Model . Members )
161+ {
162+ // Skip safe types - they're already shallow copied by 'with'
163+ if ( member . TypeKind == MemberTypeKind . Safe )
164+ continue ;
165+
166+ // Skip read-only members
167+ if ( member . IsReadOnly )
168+ continue ;
169+
170+ var assignment = MemberCloneGenerator . GetMemberAssignment ( ctx , member , sourceVarName , "null" ) ;
171+ if ( ! string . IsNullOrEmpty ( assignment ) )
172+ {
173+ deepCloneAssignments . Add ( $ " { assignment } ") ;
174+ }
175+ }
176+
177+ if ( deepCloneAssignments . Count == 0 )
178+ {
179+ // All members are safe - simple shallow copy
180+ sb . AppendLine ( $ " return { sourceVarName } with {{ }};") ;
181+ }
182+ else
183+ {
184+ // Use 'with' expression with only the members that need deep cloning
185+ sb . AppendLine ( $ " return { sourceVarName } with") ;
186+ sb . AppendLine ( " {" ) ;
187+ sb . AppendLine ( string . Join ( ",\n " , deepCloneAssignments ) ) ;
188+ sb . AppendLine ( " };" ) ;
189+ }
190+ }
133191}
134192
0 commit comments