如何实现基于数值的类型安全性?(类似泛型的类型依赖机制)
在C#中实现基于特定数值的强类型Level类(避免手动定义LevelXXX类型)
要实现你需要的「不同数值的Level类型互不兼容,且无需手动定义每个数值对应的单独类型」的功能,核心是利用C#的泛型类型系统区分不同数值标识,同时通过源代码生成器自动生成对应的标记类型,避免手动编写重复代码。
实现步骤
1. 定义标记接口与泛型Level类
首先创建一个标记接口,用于约束泛型参数;再定义泛型Level<T>类,确保不同T对应的Level类型完全独立:
// 标记接口,用于标识等级数值类型 public interface ILevelMarker { // 静态抽象成员,获取对应的等级数值(C# 11+支持) static abstract int LevelValue { get; } } // 泛型Level类,泛型参数必须是实现ILevelMarker的结构体 public class Level<T> where T : struct, ILevelMarker { // 可添加业务属性,比如玩家名称、经验值等 public string PlayerName { get; set; } // 可以通过静态成员获取当前Level对应的数值 public int CurrentLevelValue => T.LevelValue; }
2. 用源代码生成器自动生成标记结构体
为了避免手动编写Level1024、Level1000这类结构体,我们可以用源代码生成器自动生成对应数值的标记类型:
第一步:定义生成特性
创建一个特性,用于标记需要生成的等级数值:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)] public class GenerateLevelMarkerAttribute : Attribute { public int Value { get; } public GenerateLevelMarkerAttribute(int value) { Value = value; } }
第二步:编写源代码生成器
创建一个Source Generator项目,实现自动生成标记结构体的逻辑:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Immutable; [Generator] public class LevelMarkerGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { // 筛选带有GenerateLevelMarkerAttribute的类型 var provider = context.SyntaxProvider .CreateSyntaxProvider( (node, _) => node is ClassDeclarationSyntax || node is StructDeclarationSyntax, (ctx, _) => GetTargetType(ctx) ) .Where(t => t != null); // 合并结果并生成代码 context.RegisterSourceOutput(provider.Collect(), (spc, types) => { foreach (var type in types.Where(t => t != null)) { GenerateLevelMarkerCode(spc, type); } }); } private static INamedTypeSymbol? GetTargetType(GeneratorSyntaxContext context) { var symbol = context.SemanticModel.GetDeclaredSymbol(context.Node); if (symbol == null) return null; // 检查是否带有GenerateLevelMarkerAttribute var attr = symbol.GetAttributes() .FirstOrDefault(a => a.AttributeClass?.Name == nameof(GenerateLevelMarkerAttribute)); return attr != null ? symbol : null; } private static void GenerateLevelMarkerCode(SourceProductionContext context, INamedTypeSymbol type) { var attributes = type.GetAttributes() .Where(a => a.AttributeClass?.Name == nameof(GenerateLevelMarkerAttribute)); foreach (var attr in attributes) { if (attr.ConstructorArguments.First().Value is int levelValue) { var structName = $"Level{levelValue}"; // 生成代码 var code = $$""" using System; namespace YourNamespace; public readonly struct {{structName}} : ILevelMarker { public static int LevelValue => {{levelValue}}; } """; context.AddSource($"{structName}.g.cs", code); } } } }
3. 使用方式
在项目中添加一个类,标记需要生成的等级数值:
[GenerateLevelMarker(1024)] [GenerateLevelMarker(1000)] public static class LevelDefinitions { }
之后就可以直接使用泛型Level类,不同数值的类型会被编译器视为完全不同的类型,赋值时会报错:
Level<Level1024> topPlayerOf1K; Level<Level1000> Abe = new Level<Level1000> { PlayerName = "Abe" }; topPlayerOf1K = Abe; // 编译错误:无法将Level<Level1000>隐式转换为Level<Level1024>
替代方案(无源代码生成器)
如果暂时不想用源代码生成器,可以手动编写少量标记结构体,这种方式适合数值较少的场景:
public readonly struct Level1024 : ILevelMarker { public static int LevelValue => 1024; } public readonly struct Level1000 : ILevelMarker { public static int LevelValue => 1000; }
使用方式和上面一致,同样能实现类型不兼容的效果。
内容的提问来源于stack exchange,提问作者ispiro
相关产品推荐
相关产品推荐

