在Visual Studio 2022中为C#创建带编译时转换检查的自定义值类型
在C#中实现编译期值约束的自定义类型
C#本身没有原生支持编译期值约束的自定义值类型,但可以通过限制构造访问+静态工厂方法结合Roslyn自定义分析器的方式,实现类似uint的编写阶段错误检查效果。以下是具体方案:
1. 基础改进:避免隐式转换,限制实例创建
首先修改你的结构体,将构造函数设为私有,通过静态工厂方法创建实例,并禁用隐式转换,减少意外的非法值注入:
public readonly record struct Liters { public double Value { get; } // 私有构造函数,强制通过工厂方法创建实例 private Liters(double value) { if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "升数不能为负数"); Value = value; } // 静态工厂方法,明确实例创建入口 public static Liters Create(double value) => new Liters(value); // 仅保留显式转换为基础类型 public static explicit operator double(Liters liters) => liters.Value; }
这个版本能在运行时拦截非法值,但还做不到编译期检查,需要配合Roslyn分析器实现。
2. 编译期检查核心:自定义Roslyn分析器
通过编写Roslyn分析器,你可以在编译阶段(VS编写代码时即时提示)检查传递给Create方法的参数是否符合约束。以下是分析器的核心逻辑示例:
[DiagnosticAnalyzer(LanguageNames.CSharp)] public class LitersValueAnalyzer : DiagnosticAnalyzer { public const string DiagnosticId = "Liters001"; private static readonly LocalizableString Title = "无效的升数值"; private static readonly LocalizableString MessageFormat = "升数值不能小于0"; private static readonly LocalizableString Description = "升作为体积单位,不能为负数"; private const string Category = "Usage"; private static readonly DiagnosticDescriptor Rule = new( DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); // 监听方法调用节点 context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); } private void AnalyzeInvocation(SyntaxNodeAnalysisContext context) { var invocation = (InvocationExpressionSyntax)context.Node; // 检查是否是Liters.Create方法调用 var symbol = context.SemanticModel.GetSymbolInfo(invocation.Expression).Symbol as IMethodSymbol; if (symbol == null || symbol.Name != "Create" || symbol.ContainingType.Name != "Liters") return; // 获取第一个参数(值参数) if (invocation.ArgumentList.Arguments.Count == 0) return; var arg = invocation.ArgumentList.Arguments[0]; // 尝试获取参数的常量值 var constantValue = context.SemanticModel.GetConstantValue(arg.Expression); if (!constantValue.HasValue) return; // 检查是否是负数 if (constantValue.Value is double value && value < 0) { var diagnostic = Diagnostic.Create(Rule, arg.GetLocation()); context.ReportDiagnostic(diagnostic); } } }
将这个分析器打包成VS扩展后,当你编写Liters.Create(-1)时,VS会在编译前直接报错,和uint的错误提示逻辑一致。
3. 扩展到其他类型(如pH值)
同样的逻辑可以复制到pH值类型,只需修改约束条件(pH范围0-14):
public readonly record struct PhValue { public double Value { get; } private PhValue(double value) { if (value < 0 || value > 14) throw new ArgumentOutOfRangeException(nameof(value), "pH值必须在0-14之间"); Value = value; } public static PhValue Create(double value) => new PhValue(value); public static explicit operator double(PhValue ph) => ph.Value; }
对应的分析器只需修改检查逻辑,判断值是否在0-14之外即可。
总结
- 单纯靠结构体无法实现编译期值约束,必须结合Roslyn分析器
- 限制构造函数访问+静态工厂方法是基础,确保实例创建的可控性
- 自定义分析器可以针对常量值做编译期检查,对于变量的检查可以通过更复杂的数据流分析实现
内容的提问来源于stack exchange,提问作者Thomas Youssef
相关产品推荐
相关产品推荐

