如何限制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
相关产品推荐
相关产品推荐

