Skip to content

Commit 8b969c0

Browse files
committed
follow up to last
1 parent db0a279 commit 8b969c0

File tree

4 files changed

+163
-8
lines changed

4 files changed

+163
-8
lines changed

EnumSourceGenerator/EnumGenerator.cs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3131

3232
// Extract enum member names from the All property of the class
3333
var structName = typeArgument.Name;
34+
var uniqueStruct = typeArgument.GetAttributes()
35+
.Any(attr => attr.AttributeClass?.Name == "UniqueAttribute");
36+
var structReadonly = typeArgument.IsReadOnly;
37+
3438
var enumMembers = ExtractEnumMembers(ctx, classSymbol).ToImmutableArray();
3539

3640
var source = GenerateEnumSource(classSymbol.Name, enumMembers);
3741
ctx.AddSource($"{classSymbol.Name}TypeEnum.g.cs", SourceText.From(source, Encoding.UTF8));
3842

3943
var helper = GeneratePartialHelper(classSymbol.Name, structName,
4044
classSymbol.ContainingNamespace.ToDisplayString(),
41-
typeArgument.ToDisplayString(), enumMembers);
45+
typeArgument.ToDisplayString(), enumMembers, uniqueStruct, structReadonly);
4246
ctx.AddSource($"{classSymbol.Name}Helper.g.cs", SourceText.From(helper, Encoding.UTF8));
4347
}
4448
});
@@ -197,19 +201,58 @@ public enum {className}Type
197201
}}";
198202
}
199203

200-
private static string GeneratePartialHelper(string className, string structName, string fullNamespace, string typeArgument, ImmutableArray<string> enumMembers)
204+
private static string GeneratePartialHelper(
205+
string className,
206+
string structName,
207+
string fullNamespace,
208+
string typeArgument,
209+
ImmutableArray<string> enumMembers,
210+
bool uniqueStruct,
211+
bool structReadonly)
201212
{
202213
var membersSource = string.Join(";\n", enumMembers.Select((m, i) => $" public {typeArgument} {m} => All[{i}]"));
214+
var structSource = string.Empty;
215+
if (uniqueStruct)
216+
{
217+
// Generate index-based comparison using sanitized names
218+
var nameToIndexMapping = string.Join(",\n",
219+
enumMembers.Select((name, index) => $" [\"{name}\"] = {index}"));
220+
structSource = structReadonly
221+
? $@"
222+
public readonly partial record struct {structName}
223+
{{
224+
private static readonly Dictionary<string, int> _nameToIndexMap = new Dictionary<string, int>
225+
{{
226+
{nameToIndexMapping}
227+
}};
228+
229+
private readonly int Index => _nameToIndexMap.TryGetValue(Name ?? string.Empty, out var idx) ? idx : -1;
230+
231+
public bool Equals({structName} other) => Index == other.Index;
232+
public override int GetHashCode() => Index;
233+
}}"
234+
: $@"
235+
public partial record struct {structName}
236+
{{
237+
private static readonly Dictionary<string, int> _nameToIndexMap = new Dictionary<string, int>
238+
{{
239+
{nameToIndexMapping}
240+
}};
241+
242+
private int? _index;
243+
private int Index => _index ??= _nameToIndexMap.TryGetValue(Name ?? string.Empty, out var idx) ? idx : -1;
244+
245+
public bool Equals({structName} other) => Index == other.Index;
246+
public override int GetHashCode() => Index;
247+
}}";
248+
}
249+
203250
return $@"// Auto-generated code
204251
using ContentEnums;
205252
206253
namespace {fullNamespace}
207254
{{
208-
public partial record struct {structName}
209-
{{
210-
public bool Equals({structName} other) => string.Equals(Name, other.Name);
211-
public override int GetHashCode() => Name?.GetHashCode() ?? 0;
212-
}}
255+
{structSource}
213256
public partial class {className}
214257
{{
215258
public {typeArgument} Get({className}Type type) => All[(int)type];

SimpleInjection.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleInjection", "SimpleIn
77
EndProject
88
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumSourceGenerator", "EnumSourceGenerator\EnumSourceGenerator.csproj", "{2CC0B800-ECBE-4DE5-DF71-BADEFC9E17F2}"
99
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestMe", "TestMe\TestMe.csproj", "{46458B09-F479-402A-864A-F40D1CED5DD6}"
11+
EndProject
1012
Global
1113
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1214
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
2123
{2CC0B800-ECBE-4DE5-DF71-BADEFC9E17F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
2224
{2CC0B800-ECBE-4DE5-DF71-BADEFC9E17F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
2325
{2CC0B800-ECBE-4DE5-DF71-BADEFC9E17F2}.Release|Any CPU.Build.0 = Release|Any CPU
26+
{46458B09-F479-402A-864A-F40D1CED5DD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27+
{46458B09-F479-402A-864A-F40D1CED5DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
28+
{46458B09-F479-402A-864A-F40D1CED5DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{46458B09-F479-402A-864A-F40D1CED5DD6}.Release|Any CPU.Build.0 = Release|Any CPU
2430
EndGlobalSection
2531
GlobalSection(SolutionProperties) = preSolution
2632
HideSolutionNode = FALSE
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
namespace SimpleInjection.Generator;
2+
3+
/// <summary>
4+
/// Marks a struct as unique for content generation. When applied to structs used in <see cref="IContent{T}"/> implementations,
5+
/// the source generator will override the record struct's default equality behavior to use array index-based comparison
6+
/// instead of comparing all properties.
7+
/// </summary>
8+
/// <remarks>
9+
/// <para>
10+
/// The <see cref="UniqueAttribute"/> should be applied to record structs that represent content items in arrays
11+
/// where position determines identity. This is particularly useful for static content resources where:
12+
/// </para>
13+
/// <list type="bullet">
14+
/// <item><description>The struct's position in the content array uniquely identifies it</description></item>
15+
/// <item><description>Performance-critical equality comparisons are needed (O(1) instead of O(n))</description></item>
16+
/// <item><description>The struct is used as a key in collections or lookups</description></item>
17+
/// <item><description>The struct is used as a key in <see cref="ISubContent{TKey, TValue}"/> implementations</description></item>
18+
/// </list>
19+
/// <para>
20+
/// When this attribute is present, the source generator creates:
21+
/// </para>
22+
/// <list type="number">
23+
/// <item><description>A static dictionary mapping struct names to their array indices</description></item>
24+
/// <item><description>Custom <see cref="object.Equals(object)"/> and <see cref="object.GetHashCode"/> implementations that use the array index</description></item>
25+
/// <item><description>Optimized implementation based on whether the struct is declared as readonly</description></item>
26+
/// </list>
27+
/// <para>
28+
/// <strong>Performance Benefits:</strong>
29+
/// </para>
30+
/// <list type="bullet">
31+
/// <item><description>Equality comparison: O(1) integer comparison vs O(n) property comparison</description></item>
32+
/// <item><description>Hash code generation: O(1) integer return vs O(n) property hashing</description></item>
33+
/// <item><description>Dictionary/HashSet operations: Significantly faster lookups and insertions</description></item>
34+
/// <item><description><strong>ISubContent optimization:</strong> When used as keys in <see cref="ISubContent{TKey, TValue}.ByKey"/> dictionaries, provides dramatically improved performance for dictionary operations due to optimized hashing and equality</description></item>
35+
/// </list>
36+
/// <para>
37+
/// <strong>Readonly vs Non-Readonly Optimization:</strong>
38+
/// </para>
39+
/// <list type="bullet">
40+
/// <item><description><strong>Readonly structs:</strong> Direct dictionary lookup on each access, eliminates defensive copying</description></item>
41+
/// <item><description><strong>Non-readonly structs:</strong> Cached index lookup for repeated operations, potential defensive copying</description></item>
42+
/// </list>
43+
/// <para>
44+
/// <strong>ISubContent Performance Impact:</strong>
45+
/// </para>
46+
/// <para>
47+
/// When structs marked with <see cref="UniqueAttribute"/> are used as keys in <see cref="ISubContent{TKey, TValue}"/>
48+
/// implementations, the performance improvement is substantial:
49+
/// </para>
50+
/// <list type="bullet">
51+
/// <item><description><strong>Dictionary.TryGetValue():</strong> Much faster key lookup due to optimized hash codes</description></item>
52+
/// <item><description><strong>Dictionary.Add():</strong> Faster insertion with reduced hash collisions</description></item>
53+
/// <item><description><strong>Dictionary enumeration:</strong> More predictable performance due to consistent hashing</description></item>
54+
/// <item><description><strong>Memory efficiency:</strong> Reduced memory allocation from fewer hash collisions and bucket redistributions</description></item>
55+
/// </list>
56+
/// <para>
57+
/// <strong>Usage Requirements:</strong>
58+
/// </para>
59+
/// <list type="bullet">
60+
/// <item><description>Must be applied to record structs that implement or are used with content interfaces</description></item>
61+
/// <item><description>The struct must have a <c>Name</c> property that matches the content item's identifier</description></item>
62+
/// <item><description>Content items should be defined in static arrays where order is meaningful</description></item>
63+
/// <item><description>Declaring the struct as <c>readonly</c> is optional but recommended for optimal performance</description></item>
64+
/// <item><description>Particularly beneficial when the struct implements <see cref="INamed"/> and is used as a key type in <see cref="ISubContent{TKey, TValue}"/></description></item>
65+
/// </list>
66+
/// </remarks>
67+
/// <example>
68+
/// <code>
69+
/// // Readonly version (recommended for best performance)
70+
/// [Unique]
71+
/// public readonly partial record struct Material(string Name, int Durability, Color Color) : INamed;
72+
///
73+
/// // Non-readonly version (still optimized with caching)
74+
/// [Unique]
75+
/// public partial record struct Material(string Name, int Durability, Color Color) : INamed;
76+
///
77+
/// public partial class Materials : IContent&lt;Material&gt;
78+
/// {
79+
/// public static Material[] All = [
80+
/// new("Iron", 100, Color.Gray),
81+
/// new("Gold", 50, Color.Yellow),
82+
/// new("Diamond", 500, Color.White)
83+
/// ];
84+
/// }
85+
///
86+
/// // Usage with ISubContent for high-performance lookups:
87+
/// public class MaterialProperties : ISubContent&lt;Material, PropertyData&gt;
88+
/// {
89+
/// public Dictionary&lt;Material, PropertyData&gt; ByKey { get; } = new()
90+
/// {
91+
/// [Materials.Iron] = new PropertyData(/* ... */),
92+
/// [Materials.Gold] = new PropertyData(/* ... */),
93+
/// // Fast dictionary operations due to optimized Material equality/hashing
94+
/// };
95+
///
96+
/// public PropertyData this[Material key] => ByKey[key]; // O(1) lookup
97+
/// }
98+
///
99+
/// // Generated code enables fast index-based equality:
100+
/// var iron1 = materials.Iron;
101+
/// var iron2 = materials.Get(MaterialsType.Iron);
102+
/// var areEqual = iron1.Equals(iron2); // Uses index comparison, not all properties
103+
/// </code>
104+
/// </example>
105+
[AttributeUsage(AttributeTargets.Struct)]
106+
public class UniqueAttribute : Attribute;

SimpleInjection/SimpleInjection.csproj

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

99
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1010
<PackageId>SimpleInjection</PackageId>
11-
<Version>0.9.6.7</Version>
11+
<Version>0.9.7</Version>
1212
<Authors>Derek Gooding</Authors>
1313
<Company>Derek Gooding</Company>
1414
<Description>

0 commit comments

Comments
 (0)