Skip to content

Commit affae44

Browse files
committed
[Ad hoc Union] Reference type members share the same backing field + new config "UseSingleBackingField"
... to reduce memory footprint
1 parent d1138c5 commit affae44

17 files changed

+2527
-39
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<Copyright>(c) $([System.DateTime]::Now.Year), Pawel Gerr. All rights reserved.</Copyright>
5-
<VersionPrefix>9.5.3</VersionPrefix>
5+
<VersionPrefix>9.6.0</VersionPrefix>
66
<Authors>Pawel Gerr</Authors>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
<PackageProjectUrl>https://github.com/PawelGerr/Thinktecture.Runtime.Extensions</PackageProjectUrl>

Thinktecture.Runtime.Extensions.slnx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22
<Folder Name="/assets/">
33
<File Path=".editorconfig" />
44
<File Path=".gitattributes" />
5-
<File Path=".github/copilot-instructions.md" />
6-
<File Path=".github/workflows/ci.ps1" />
7-
<File Path=".github/workflows/codeql-analysis.yml" />
8-
<File Path=".github/workflows/main.yml" />
95
<File Path=".gitignore" />
106
<File Path="context7.json" />
117
<File Path="Directory.Build.props" />
@@ -19,6 +15,7 @@
1915
<File Path=".github\workflows\claude.yml" />
2016
<File Path=".github\workflows\claude-code-review.yml" />
2117
<File Path=".github\workflows\ci.ps1" />
18+
<File Path=".github/copilot-instructions.md" />
2219
<File Path=".github\workflows\mcp-config.json" />
2320
<File Path=".github\workflows\main.yml" />
2421
</Folder>

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionCodeGenerator.cs

Lines changed: 134 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ public class AdHocUnionCodeGenerator : CodeGeneratorBase
99

1010
private readonly AdHocUnionSourceGenState _state;
1111
private readonly StringBuilder _sb;
12+
private readonly bool _useSharedObjectForRefTypes;
1213

1314
public AdHocUnionCodeGenerator(
1415
AdHocUnionSourceGenState state,
1516
StringBuilder sb)
1617
{
1718
_state = state;
1819
_sb = sb;
20+
_useSharedObjectForRefTypes = state.Settings.UseSingleBackingField
21+
|| _state.MemberTypes.Where(t => t.IsReferenceType && t.TypeDuplicateCounter <= 1).Select(t => t.TypeFullyQualified).Count() >= 2;
1922
}
2023

2124
public override void Generate(CancellationToken cancellationToken)
@@ -247,7 +250,7 @@ private void GenerateToString()
247250
var memberType = _state.MemberTypes[i];
248251

249252
_sb.Append(@"
250-
").Append(i + 1).Append(" => this.").Append(memberType.BackingFieldName);
253+
").Append(i + 1).Append(" => ").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType);
251254

252255
if (memberType.SpecialType != SpecialType.System_String)
253256
{
@@ -289,7 +292,7 @@ public override int GetHashCode()
289292
_sb.Append(@"
290293
").Append(i + 1).Append(" => ");
291294

292-
_sb.Append("global::System.HashCode.Combine(").AppendTypeFullyQualified(_state).Append("._typeHashCode, this.").Append(memberType.BackingFieldName);
295+
_sb.Append("global::System.HashCode.Combine(").AppendTypeFullyQualified(_state).Append("._typeHashCode, ").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType);
293296

294297
if (memberType.IsReferenceType)
295298
_sb.Append("?");
@@ -366,14 +369,22 @@ public bool Equals(").AppendTypeFullyQualifiedNullAnnotated(_state).Append(@" ot
366369
for (var i = 0; i < _state.MemberTypes.Count; i++)
367370
{
368371
var memberType = _state.MemberTypes[i];
372+
var useSharedObjectBackingField = _state.UseSharedObjectBackingField(_useSharedObjectForRefTypes, memberType);
369373

370374
_sb.Append(@"
371375
").Append(i + 1).Append(" => ");
372376

373377
if (memberType.IsReferenceType)
374-
_sb.Append("this.").Append(memberType.BackingFieldName).Append(" is null ? other.").Append(memberType.BackingFieldName).Append(" is null : ");
378+
{
379+
_sb.Append("this.").AppendBackingFieldName(useSharedObjectBackingField, memberType).Append(" is null ? other.").AppendBackingFieldName(_state, _useSharedObjectForRefTypes, memberType).Append(" is null : ");
380+
}
375381

376-
_sb.Append("this.").Append(memberType.BackingFieldName).Append(".Equals(other.").Append(memberType.BackingFieldName);
382+
if (useSharedObjectBackingField)
383+
{
384+
_sb.Append("this._valueIndex == other._valueIndex && ");
385+
}
386+
387+
_sb.AppendBackingFieldAccess(useSharedObjectBackingField, memberType, nullAnnotated: false, suppressed: true).Append(".Equals(").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType, qualifier: "other");
377388

378389
if (memberType.SpecialType == SpecialType.System_String)
379390
_sb.Append(", global::System.StringComparison.").Append(Enum.GetName(typeof(StringComparison), _state.Settings.DefaultStringComparison));
@@ -523,7 +534,7 @@ private void GenerateIndexBasedActionSwitchBody(bool withState, bool isPartially
523534
if (withState)
524535
_sb.AppendEscaped(_state.Settings.SwitchMapStateParameterName).Append(", ");
525536

526-
_sb.Append("this.").Append(memberType.BackingFieldName).Append(memberType.IsReferenceType && memberType.NullableAnnotation != NullableAnnotation.Annotated ? "!" : null).Append(@");
537+
_sb.AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType).Append(memberType.IsReferenceType && memberType.NullableAnnotation != NullableAnnotation.Annotated ? "!" : null).Append(@");
527538
return;");
528539
}
529540

@@ -684,7 +695,7 @@ private void GenerateIndexBasedFuncSwitchBody(bool withState, bool isPartially)
684695
if (withState)
685696
_sb.AppendEscaped(_state.Settings.SwitchMapStateParameterName).Append(", ");
686697

687-
_sb.Append("this.").Append(memberType.BackingFieldName).Append(memberType is { IsReferenceType: true, Setting.IsNullableReferenceType: false } ? "!" : null).Append(");");
698+
_sb.AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType).Append(memberType is { IsReferenceType: true, Setting.IsNullableReferenceType: false } ? "!" : null).Append(");");
688699
}
689700

690701
_sb.Append(@"
@@ -866,7 +877,7 @@ private void GenerateConstructors()
866877

867878
_sb.Append(@")
868879
{
869-
this.").Append(memberType.BackingFieldName).Append(" = ").AppendEscaped(argName).Append(@";
880+
").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType, false).Append(" = ").AppendEscaped(argName).Append(@";
870881
this._valueIndex = ");
871882

872883
if (hasDuplicates)
@@ -910,15 +921,32 @@ private void GenerateFactoriesForTypeDuplicates()
910921

911922
private void GenerateMemberTypeFieldsAndProps()
912923
{
924+
var objBackingFieldWritten = false;
925+
913926
for (var i = 0; i < _state.MemberTypes.Count; i++)
914927
{
915928
var memberType = _state.MemberTypes[i];
916929

917930
if (memberType.TypeDuplicateCounter > 1)
918931
continue;
919932

920-
_sb.Append(@"
933+
var useSharedObjectBackingField = _state.UseSharedObjectBackingField(_useSharedObjectForRefTypes, memberType);
934+
935+
if (useSharedObjectBackingField)
936+
{
937+
if (objBackingFieldWritten)
938+
continue;
939+
940+
objBackingFieldWritten = true;
941+
942+
_sb.Append(@"
943+
private readonly object? _obj;");
944+
}
945+
else
946+
{
947+
_sb.Append(@"
921948
private readonly ").AppendTypeFullyQualifiedNullAnnotated(memberType).Append(" ").Append(memberType.BackingFieldName).Append(";");
949+
}
922950
}
923951

924952
for (var i = 0; i < _state.MemberTypes.Count; i++)
@@ -942,7 +970,7 @@ private void GenerateMemberTypeFieldsAndProps()
942970
/// </summary>
943971
/// <exception cref=""global::System.InvalidOperationException"">If the current value is not of type ").AppendTypeForXmlComment(memberType).Append(@".</exception>
944972
public ").AppendTypeFullyQualified(memberType).Append(" As").Append(memberType.Name).Append(" => Is").Append(memberType.Name)
945-
.Append(" ? this.").Append(memberType.BackingFieldName).Append(memberType.IsReferenceType && memberType.NullableAnnotation != NullableAnnotation.Annotated ? "!" : null)
973+
.Append(" ? ").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType).Append(memberType.IsReferenceType && memberType.NullableAnnotation != NullableAnnotation.Annotated ? "!" : null)
946974
.Append(" : throw new global::System.InvalidOperationException($\"'{nameof(").AppendTypeFullyQualified(_state).Append(")}' is not of type '").AppendTypeMinimallyQualified(memberType).Append("'.\");");
947975
}
948976
}
@@ -964,7 +992,15 @@ private void GenerateRawValueGetter()
964992
}
965993

966994
_sb.Append(@"
967-
public object").Append(hasNullableTypes ? "?" : null).Append(@" Value => this._valueIndex switch
995+
public object").Append(hasNullableTypes ? "?" : null).Append(" Value => ");
996+
997+
if (_state.Settings.UseSingleBackingField)
998+
{
999+
_sb.Append("this._obj!;");
1000+
return;
1001+
}
1002+
1003+
_sb.Append(@"this._valueIndex switch
9681004
{");
9691005

9701006
if (!_state.IsReferenceType)
@@ -978,7 +1014,7 @@ private void GenerateRawValueGetter()
9781014
var memberType = _state.MemberTypes[i];
9791015

9801016
_sb.Append(@"
981-
").Append(i + 1).Append(" => this.").Append(memberType.BackingFieldName).Append(memberType.IsReferenceType && !hasNullableTypes ? "!" : null).Append(",");
1017+
").Append(i + 1).Append(" => ").AppendBackingFieldAccess(_state, _useSharedObjectForRefTypes, memberType, withCast: false, nullAnnotated: false, suppressed: false).Append(memberType.IsReferenceType && !hasNullableTypes ? "!" : null).Append(",");
9821018
}
9831019

9841020
_sb.Append(@"
@@ -1004,4 +1040,91 @@ public static StringBuilder AppendMemberTypes(this StringBuilder sb, IReadOnlyLi
10041040

10051041
return sb;
10061042
}
1043+
1044+
public static bool UseSharedObjectBackingField(
1045+
this AdHocUnionSourceGenState state,
1046+
bool useSharedObjectForRefTypes,
1047+
AdHocUnionMemberTypeState memberType)
1048+
{
1049+
return state.Settings.UseSingleBackingField || (useSharedObjectForRefTypes && memberType.IsReferenceType);
1050+
}
1051+
1052+
public static StringBuilder AppendBackingFieldAccess(
1053+
this StringBuilder sb,
1054+
AdHocUnionSourceGenState state,
1055+
bool useSharedObjectForRefTypes,
1056+
AdHocUnionMemberTypeState memberType,
1057+
bool withCast = true,
1058+
bool nullAnnotated = true,
1059+
bool suppressed = false,
1060+
string qualifier = "this")
1061+
{
1062+
var useSharedObjectBackingField = state.UseSharedObjectBackingField(useSharedObjectForRefTypes, memberType);
1063+
1064+
return AppendBackingFieldAccess(sb, useSharedObjectBackingField, memberType, withCast, nullAnnotated, suppressed, qualifier);
1065+
}
1066+
1067+
public static StringBuilder AppendBackingFieldAccess(
1068+
this StringBuilder sb,
1069+
bool useSharedObjectBackingField,
1070+
AdHocUnionMemberTypeState memberType,
1071+
bool withCast = true,
1072+
bool nullAnnotated = true,
1073+
bool suppressed = false,
1074+
string qualifier = "this")
1075+
{
1076+
if (useSharedObjectBackingField)
1077+
{
1078+
if (withCast)
1079+
{
1080+
sb.Append("((");
1081+
1082+
if (nullAnnotated)
1083+
{
1084+
sb.AppendTypeFullyQualifiedNullAnnotated(memberType);
1085+
}
1086+
else
1087+
{
1088+
sb.AppendTypeFullyQualified(memberType);
1089+
}
1090+
1091+
sb.Append(")");
1092+
}
1093+
1094+
sb.Append(qualifier).Append("._obj");
1095+
1096+
if (withCast)
1097+
{
1098+
if (suppressed || memberType is { IsReferenceType: false, IsNullableStruct: false })
1099+
sb.Append("!");
1100+
1101+
sb.Append(")");
1102+
}
1103+
}
1104+
else
1105+
{
1106+
sb.Append(qualifier).Append(".").Append(memberType.BackingFieldName);
1107+
}
1108+
1109+
return sb;
1110+
}
1111+
1112+
public static StringBuilder AppendBackingFieldName(
1113+
this StringBuilder sb,
1114+
AdHocUnionSourceGenState state,
1115+
bool useSharedObjectForRefTypes,
1116+
AdHocUnionMemberTypeState memberType)
1117+
{
1118+
var useSharedObjectBackingField = state.UseSharedObjectBackingField(useSharedObjectForRefTypes, memberType);
1119+
1120+
return AppendBackingFieldName(sb, useSharedObjectBackingField, memberType);
1121+
}
1122+
1123+
public static StringBuilder AppendBackingFieldName(
1124+
this StringBuilder sb,
1125+
bool useSharedObjectBackingField,
1126+
AdHocUnionMemberTypeState memberType)
1127+
{
1128+
return sb.Append(useSharedObjectBackingField ? "_obj" : memberType.BackingFieldName);
1129+
}
10071130
}

src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/AdHocUnions/AdHocUnionSettings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public sealed class AdHocUnionSettings : IEquatable<AdHocUnionSettings>
1212
public ConversionOperatorsGeneration ConversionToValue { get; }
1313
public string SwitchMapStateParameterName { get; }
1414
public SerializationFrameworks SerializationFrameworks { get; }
15+
public bool UseSingleBackingField { get; }
1516

1617
public AdHocUnionSettings(
1718
AttributeData attribute,
@@ -26,6 +27,7 @@ public AdHocUnionSettings(
2627
ConversionToValue = attribute.FindConversionToValue() ?? ConversionOperatorsGeneration.Explicit;
2728
SwitchMapStateParameterName = attribute.FindSwitchMapStateParameterName();
2829
SerializationFrameworks = attribute.FindSerializationFrameworks();
30+
UseSingleBackingField = attribute.FindUseSingleBackingField() ?? false;
2931

3032
var memberTypeSettings = new AdHocUnionMemberTypeSetting[numberOfMemberTypes];
3133
MemberTypeSettings = memberTypeSettings;
@@ -58,6 +60,7 @@ public bool Equals(AdHocUnionSettings? other)
5860
&& ConversionToValue == other.ConversionToValue
5961
&& SwitchMapStateParameterName == other.SwitchMapStateParameterName
6062
&& SerializationFrameworks == other.SerializationFrameworks
63+
&& UseSingleBackingField == other.UseSingleBackingField
6164
&& MemberTypeSettings.SequenceEqual(other.MemberTypeSettings);
6265
}
6366

@@ -74,6 +77,7 @@ public override int GetHashCode()
7477
hashCode = (hashCode * 397) ^ (int)ConversionToValue;
7578
hashCode = (hashCode * 397) ^ SwitchMapStateParameterName.GetHashCode();
7679
hashCode = (hashCode * 397) ^ (int)SerializationFrameworks;
80+
hashCode = (hashCode * 397) ^ UseSingleBackingField.GetHashCode();
7781
hashCode = (hashCode * 397) ^ MemberTypeSettings.ComputeHashCode();
7882

7983
return hashCode;

src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,11 @@ public static UnionConstructorAccessModifier FindUnionConstructorAccessModifier(
231231
?? UnionConstructorAccessModifier.Public;
232232
}
233233

234+
public static bool? FindUseSingleBackingField(this AttributeData attributeData)
235+
{
236+
return GetBooleanParameterValue(attributeData, "UseSingleBackingField");
237+
}
238+
234239
public static JsonIgnoreCondition? FindJsonIgnoreCondition(this AttributeData attributeData)
235240
{
236241
return (JsonIgnoreCondition?)GetIntegerParameterValue(attributeData, "Condition");

src/Thinktecture.Runtime.Extensions/UnionAttributeBase.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ public abstract class UnionAttributeBase : Attribute
5353
/// </summary>
5454
public string? SwitchMapStateParameterName { get; set; }
5555

56+
/// <summary>
57+
/// Indication whether the generator should use a single backing field of type <see cref="object"/> for all members, even for structs.
58+
/// Default is <c>false</c>.
59+
/// </summary>
60+
public bool UseSingleBackingField { get; set; }
61+
5662
/// <summary>
5763
/// Initializes a new instance of <see cref="UnionAttributeBase"/>.
5864
/// </summary>

0 commit comments

Comments
 (0)