如何避免强命名数值结构体重复的运算符重载样板代码?
我完全懂你这种被重复样板代码淹没的痛苦——30多个readonly record struct,每个都要写几乎一模一样的加减乘除、比较运算符,维护起来简直是噩梦,而且还不能自己写源生成器,确实头疼。结合你的场景,我给你两个实用的解决方案:
方案一:用T4文本模板自动生成代码(最省心的重复代码消除方式)
既然你不想自己维护源生成器,那Visual Studio自带的T4模板是绝佳替代——它不需要复杂配置,只要写一个模板文件,就能自动帮你生成所有30个结构体的完整代码,包括你需要的特定创建方法、属性,以及通用的运算符重载。
举个简单的T4模板示例,你可以根据自己的需求扩展:
<#@ template language="C#" #> <#@ output extension=".cs" #> <# // 定义所有需要生成的强类型,每个类型包含:结构体名称、创建方法前缀、单位属性名称 var types = new[] { new { StructName = "Potential", FactoryPrefix = "From", UnitProperty = "InVolt" }, new { StructName = "Duration", FactoryPrefix = "From", UnitProperty = "InSeconds" }, new { StructName = "PotentialRate", FactoryPrefix = "From", UnitProperty = "InVoltsPerSecond" } // 剩下的27个类型都加在这里 }; #> namespace YourNamespace { <# foreach (var type in types) { #> public readonly record struct <#= type.StructName #> : IComparable<<#= type.StructName #>> { private readonly double _internalValue; private <#= type.StructName #>(double value) { _internalValue = value; } public static <#= type.StructName #> <#= type.FactoryPrefix #><#= type.UnitProperty.Substring(2) #>(double value) => new(value); public double <#= type.UnitProperty #> => _internalValue; // 通用运算符重载,自动生成 public static <#= type.StructName #> operator +(<#= type.StructName #> left, <#= type.StructName #> right) => new(left._internalValue + right._internalValue); public static <#= type.StructName #> operator -(<#= type.StructName #> left, <#= type.StructName #> right) => new(left._internalValue - right._internalValue); public static <#= type.StructName #> operator *(<#= type.StructName #> value, double factor) => new(value._internalValue * factor); public static double operator /(<#= type.StructName #> left, <#= type.StructName #> right) => left._internalValue / right._internalValue; public static bool operator >(<#= type.StructName #> left, <#= type.StructName #> right) => left._internalValue > right._internalValue; public static bool operator >=(<#= type.StructName #> left, <#= type.StructName #> right) => left._internalValue >= right._internalValue; public static bool operator <(<#= type.StructName #> left, <#= type.StructName #> right) => left._internalValue < right._internalValue; public static bool operator <=(<#= type.StructName #> left, <#= type.StructName #> right) => left._internalValue <= right._internalValue; public int CompareTo(<#= type.StructName #> other) => _internalValue.CompareTo(other._internalValue); // 这里可以添加每个类型特有的逻辑,比如类型转换运算符 <# if (type.StructName == "PotentialRate") { #> public static implicit operator <#= type.StructName #>(Potential potential) => new(potential.InVolt / 1); // 示例转换逻辑,按需修改 <# } #> } <# } #> }
只要维护这个模板里的类型列表和特有的逻辑,就能自动生成所有结构体的代码,完全避免重复编写样板代码,而且比源生成器更容易维护。
方案二:用静态工具类封装通用逻辑(减少每个结构体的重复代码)
如果不想用代码生成,那可以把通用的运算符逻辑封装到一个静态类里,每个结构体只需要一行代码调用这些逻辑,大大减少重复:
首先定义一个接口来约束所有强类型:
public interface IStrongDouble<TSelf> where TSelf : IStrongDouble<TSelf> { double InternalValue { get; } static abstract TSelf Create(double value); }
然后写一个静态工具类,封装所有通用运算:
public static class StrongDoubleOperations { public static TSelf Add<TSelf>(TSelf left, TSelf right) where TSelf : IStrongDouble<TSelf> => TSelf.Create(left.InternalValue + right.InternalValue); public static TSelf Subtract<TSelf>(TSelf left, TSelf right) where TSelf : IStrongDouble<TSelf> => TSelf.Create(left.InternalValue - right.InternalValue); public static TSelf MultiplyByFactor<TSelf>(TSelf value, double factor) where TSelf : IStrongDouble<TSelf> => TSelf.Create(value.InternalValue * factor); public static double Divide<TSelf>(TSelf left, TSelf right) where TSelf : IStrongDouble<TSelf> => left.InternalValue / right.InternalValue; public static bool GreaterThan<TSelf>(TSelf left, TSelf right) where TSelf : IStrongDouble<TSelf> => left.InternalValue > right.InternalValue; // 其他比较逻辑同理 }
最后,每个结构体只需要实现接口,并调用这些静态方法来实现运算符:
public readonly record struct Potential : IStrongDouble<Potential> { private readonly double _internalValue; private Potential(double valueInVolt) => _internalValue = valueInVolt; public static Potential FromVolt(double valueInVolt) => new(valueInVolt); public double InVolt => _internalValue; // 运算符只需要一行代码,调用工具类 public static Potential operator +(Potential left, Potential right) => StrongDoubleOperations.Add(left, right); public static Potential operator -(Potential left, Potential right) => StrongDoubleOperations.Subtract(left, right); // 其他运算符同理 public int CompareTo(Potential other) => _internalValue.CompareTo(other._internalValue); // 特有的类型转换逻辑 public static implicit operator PotentialRate(Potential potential) => PotentialRate.FromVoltsPerSecond(potential.InVolt / 60); }
这种方式虽然还是要每个结构体写运算符,但每个运算符只有一行代码,大大减少了样板代码的维护量。
关于你之前尝试的接口运算符问题
你遇到的CS0563错误,本质是C#的运算符重载规则限制:二元运算符的两个参数中必须有一个是定义运算符的类型。你在接口里定义operator +(TSelf left, TSelf right)时,TSelf是泛型参数,编译器无法将它识别为“包含类型”,所以报错。就算你把参数改成IStrongDouble<TSelf>,编译器也无法自动将结构体实例匹配到这个运算符,因为C#不会隐式把结构体转成接口来匹配运算符——这就是为什么你必须强制转换才能使用,体验很差。
所以接口默认实现的方式在这个场景下确实走不通,上面的两个方案会更实用。
内容来源于stack exchange

