C#如何为类的{get;set;}属性实现隐式赋值转换
我希望创建一种表现与double值一致、同时可附加额外功能的类类型。我了解隐式转换实现方案,但该方案无法为已存在的类实例直接赋值:
public interface IUnit { double ToScaled(double v); double ToSI(double v); } public class ScalableNumber { public IUnit Unit { get; set; } double val; public double SI { get => val; set => val = value; } public double Scaled { get => Unit?.ToScaled(val)??val; set => val = (Unit == null)? value : Unit.ToSI(value); } public ScalableNumber(IUnit u) { Unit = u; } // 隐式转double,实现类似get的隐式行为 public static implicit operator double(ScalableNumber x) => x.val; // 想要实现隐式set,直接把double赋值给val // 从double隐式构造ScalableNumber的方案不可行,会丢失原有单位 public static implicit operator ScalableNumber(double x) => new ScalableNumber(null) { val=x}; // 索引器方案不可行,隐式转换得到的是单值 public double this[int i] { get => val; set => val = value; } } public class Unit:IUnit { public double scale = 1; public double ToScaled(double v) => v/scale; public double ToSI(double v) => scale * v; } public class MyExample { IUnit kilometer, hour, angstroemPerWeek; public ScalableNumber way; public ScalableNumber time; public ScalableNumber speed; public MyExample() { kilometer = new Unit() { scale = 1000 }; hour = new Unit() { scale = 3600 }; angstroemPerWeek = new Unit() { scale = 1e-10 / (60 * 60 * 24 * 7) }; way = new ScalableNumber(kilometer); time = new ScalableNumber(hour); speed = new ScalableNumber(angstroemPerWeek); } public static void ExecuteMyExample() { var myExample = new MyExample(); myExample.way.Scaled = 100; myExample.time.Scaled = 1; Console.WriteLine(myExample.Speed); } public void Calculate() { // 期望得到单位为埃/周的速度 // 当前实现得到的是米每秒(无缩放)的速度值 speed = way / time; } public string Speed { get { if (time <= 0) return string.Empty; Calculate(); return speed.Scaled.ToString(); } } }
当然,我可以在Calculate()方法中编写如下代码完成赋值:
speed.SI = way / time;
但如果可以省略.SI直接完成赋值操作,我就能够复用大量目前针对double类型属性编写的方法。请问C#中是否存在可行方案,无需指定属性名即可将赋值操作直接定向到类的对应内部属性?
首先明确C#的核心语法规则:C#不支持自定义重载赋值运算符=。
对于引用类型(你当前定义的ScalableNumber是class),赋值操作的行为永远是修改变量的引用指向,让变量指向等号右侧的新实例,不可能修改原有实例的内部字段/属性。你现在写的speed = way / time实际执行逻辑是:
- 因为没有自定义
ScalableNumber之间的除法运算符,编译器自动调用两个ScalableNumber到double的隐式转换,做完除法得到double类型的米/秒速度值 - 调用你写的double到
ScalableNumber的隐式转换,生成一个Unit为null的全新ScalableNumber实例 - 把
speed变量的引用,从一开始构造的、绑定了埃/周单位的实例,替换成这个新生成的无单位实例,原有实例直接被丢弃
这就是你单位丢失、结果不符合预期的根本原因,这个行为是语法层面固定的,没有任何特性可以绕开。
如果要实现你要的、和double行为一致又带单位的效果,有两个可行的落地方案,不需要每次写.SI:
方案1:将ScalableNumber改为值类型(struct)+ 重载所有算术运算符
值类型的赋值是值拷贝,不存在引用替换的问题。你需要做两个改动:
- 把
ScalableNumber从class改为readonly struct,保留你现有的隐式转double逻辑 - 重载所有你需要用到的算术运算符(加减乘除、比较等),让
ScalableNumber之间的运算直接返回携带正确单位的ScalableNumber结果,不要让运算降级到double类型。比如长度除以时间的运算符重载里,直接返回绑定了对应速度单位的结果实例,赋值时就不会丢失单位。
这个方案的好处是类型行为和double完全一致,所有原来接收double的方法都可以直接传ScalableNumber,不需要改原有代码逻辑。唯一需要注意的是值类型的拷贝开销,对于包装double的小结构体来说这个开销可以忽略。
方案2:保留class引用类型,通过属性封装拦截赋值
如果你一定要用引用类型(比如要支持继承、要做引用传递),可以不要把ScalableNumber类型的字段直接暴露为public字段,而是封装成属性,在属性的set块里自动把传入的double/ScalableNumber值写到原有实例的内部字段,而不是替换引用:
private ScalableNumber _speed = new ScalableNumber(angstroemPerWeek); public ScalableNumber speed { get => _speed; set { // 赋值时不替换引用,直接把值写到原有实例的SI属性 _speed.SI = value; } }
这个方案的问题是,所有用到ScalableNumber的地方都要写这种封装属性,没法直接用public字段,而且如果等号右侧是新的带其他单位的ScalableNumber,你还要自己处理单位转换逻辑。
不存在任何"黑科技"语法可以让你在直接给class类型的public字段赋值double时,自动修改原有实例内容,所有声称能实现这个效果的方案本质都是上面两种思路的变种。如果你不想改动类型定义、也不想写属性封装,那老老实实写.SI =是成本最低、可读性最好的选择。
内容的提问来源于stack exchange,提问作者SDG

