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

如何限制C#泛型仅适用于record以使用with表达式修改Name?

问题描述

我正在开发一个类似Fluent Validator的对象修改类库,普通类可正常运行,但处理C# record时遇到难题。规则定义与Fluent Validator一致:

RuleFor(x => x.Name).ForceUppercaseFormat();

ForceUppercaseFormat方法定义在扩展类中:

public static ImmutableLogicRuleBuilder<TModel,string> ForceUppercaseFormat<TModel>(this ImmutableLogicRuleBuilder<TModel,string> builder)
    where TModel : INamed
{
    builder.Apply(new StringToUpperCase(), (model, changedName) =>
    {
        model = model with { Name = changedName };
        return model;
    });

    return builder;
}

参考接口:

public interface INamed
{
    string Name { get; }
}

最初Name无init访问器,为兼顾类和record未添加,后续针对record添加后仍无效,Rider提示TModel不是有效record,无法使用with表达式。

请问是否能将泛型约束为仅适用于record?目前替代方案是为方法添加两个重载,但不够优雅且冗余:

// 针对类的重载
RuleFor(x => x.Name).ForceUppercaseFormat((x, newName) => x.Name = newName);

// 针对record的重载
RuleFor(x => x.Name).ForceUppercaseFormat((x, newName) => x with {Name = newName});
解决方案

1. 无法直接通过泛型约束限定仅为record

C#目前没有内置泛型约束可以直接指定TModel必须是record类型。record本质是编译器生成的语法糖,底层是带有特定重写方法(如Clone、Equals)的类/结构体,但没有统一的标记接口或约束能直接识别它。

2. 优化方案:利用重载优先级区分类和record

可以保留无参数的ForceUppercaseFormat,通过泛型重载的匹配优先级让编译器自动区分record和普通类,调用者无需感知差异:

// 针对record的重载(record会隐式实现IEquatable<T>,编译器优先匹配此重载)
public static ImmutableLogicRuleBuilder<TModel, string> ForceUppercaseFormat<TModel>(this ImmutableLogicRuleBuilder<TModel, string> builder)
    where TModel : INamed, IEquatable<TModel>
{
    builder.Apply(new StringToUpperCase(), (model, changedName) => model with { Name = changedName });
    return builder;
}

// 针对普通类的重载(需实现带setter的接口)
public static ImmutableLogicRuleBuilder<TModel, string> ForceUppercaseFormat<TModel>(this ImmutableLogicRuleBuilder<TModel, string> builder)
    where TModel : INamed, INameMutable
{
    builder.Apply(new StringToUpperCase(), (model, changedName) =>
    {
        model.Name = changedName;
        return model;
    });
    return builder;
}

// 新增用于普通类的可变接口
public interface INameMutable : INamed
{
    new string Name { get; set; }
}

当TModel是record时,会优先匹配第一个重载;普通类实现INameMutable后自动匹配第二个重载,调用者依然只需写RuleFor(x => x.Name).ForceUppercaseFormat()。

3. 更透明的方案:用表达式树动态生成修改逻辑

如果不想额外定义接口,可以在方法内部通过表达式树分析TModel类型,自动生成适配类或record的修改逻辑:

public static ImmutableLogicRuleBuilder<TModel, string> ForceUppercaseFormat<TModel>(this ImmutableLogicRuleBuilder<TModel, string> builder)
    where TModel : INamed
{
    Func<TModel, string, TModel> modifyFunc;
    var modelParam = Expression.Parameter(typeof(TModel), "model");
    var newValueParam = Expression.Parameter(typeof(string), "newValue");

    // 判断是否为record:检查是否存在编译器生成的非公开Clone方法
    var isRecord = typeof(TModel).GetMethod("Clone", BindingFlags.Instance | BindingFlags.NonPublic) != null;

    if (isRecord)
    {
        // 构建record的with表达式逻辑
        var nameProp = typeof(TModel).GetProperty(nameof(INamed.Name))!;
        var initExpr = Expression.MemberInit(
            Expression.Call(modelParam, typeof(TModel).GetMethod("Clone", BindingFlags.Instance | BindingFlags.NonPublic)!),
            Expression.Bind(nameProp, newValueParam)
        );
        modifyFunc = Expression.Lambda<Func<TModel, string, TModel>>(initExpr, modelParam, newValueParam).Compile();
    }
    else
    {
        // 构建普通类的setter赋值逻辑
        var nameProp = typeof(TModel).GetProperty(nameof(INamed.Name), BindingFlags.Instance | BindingFlags.Public)!;
        var assignExpr = Expression.Assign(Expression.Property(modelParam, nameProp), newValueParam);
        var blockExpr = Expression.Block(assignExpr, modelParam);
        modifyFunc = Expression.Lambda<Func<TModel, string, TModel>>(blockExpr, modelParam, newValueParam).Compile();
    }

    builder.Apply(new StringToUpperCase(), modifyFunc);
    return builder;
}

这种方式对调用者完全透明,无需区分类和record,内部自动适配修改逻辑。

内容的提问来源于stack exchange,提问作者Marek Wdowiak

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 21:13:11