You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 02:15:42