我应该何时在C#中定义(显式或隐式)转换运算符?

C#的一个鲜为人知的特性是可以创建隐式或显式的用户定义类型转换 。 我已经写了6年的C#代码了,我从来没有用过它。 所以,我担心我可能错过了好机会。

什么是用户定义转换的合法,良好用途? 您是否有比仅定义自定义方法更好的示例?

事实certificate,微软有一些关于转换的设计指南 ,其中最相关的是:

如果最终用户未明确预期此类转换,请勿提供转换运算符。

但什么时候转换“预期”? 在玩具编号课程之外,我无法弄清楚任何真实世界的用例。


以下是答案中提供的示例摘要:

  • 弧度/度/双
  • 极地/的Point2D
  • 开尔文/华氏/摄氏度

模式似乎是:隐式转换大多数(仅?)在定义数值/值类型时很有用,转换由公式定义。 回想起来,这很明显。 不过,我想知道非数字类是否也可以从隐式转换中受益..?

正如评论中所提到的,度和旋转是避免混合双值的一个很好的例子,尤其是在API之间。

我拿出了我们目前正在使用的Radians and Degrees课程,他们在这里。 现在看看它们(经过这么长时间)我想要清理它们(特别是注释/文档)并确保它们经过适当的测试。 值得庆幸的是,我已经设法有时间安排这样做。 无论如何,使用这些都需要您自担风险,我不能保证这里的所有数学是否正确,因为我很确定我们没有实际使用/测试过我们写的所有function。

弧度

 ///  /// Defines an angle in Radians ///  public struct Radians { public static readonly Radians ZERO_PI = 0; public static readonly Radians ONE_PI = System.Math.PI; public static readonly Radians TWO_PI = ONE_PI * 2; public static readonly Radians HALF_PI = ONE_PI * 0.5; public static readonly Radians QUARTER_PI = ONE_PI * 0.25; #region Public Members ///  /// Angle value ///  public double Value; ///  /// Finds the Cosine of the angle ///  public double Cos { get { return System.Math.Cos(this); } } ///  /// Finds the Sine of the angle ///  public double Sin { get { return System.Math.Sin(this); } } #endregion ///  /// Constructor ///  /// angle value in radians public Radians(double value) { this.Value = value; } ///  /// Gets the angle in degrees ///  /// Returns the angle in degrees public Degrees GetDegrees() { return this; } public Radians Reduce() { double radian = this.Value; bool IsNegative = radian < 0; radian = System.Math.Abs(radian); while (radian >= System.Math.PI * 2) { radian -= System.Math.PI * 2; } if (IsNegative && radian != 0) { radian = System.Math.PI * 2 - radian; } return radian; } #region operator overloading ///  /// Conversion of Degrees to Radians ///  ///  ///  public static implicit operator Radians(Degrees deg) { return new Radians(deg.Value * System.Math.PI / 180); } ///  /// Conversion of integer to Radians ///  ///  ///  public static implicit operator Radians(int i) { return new Radians((double)i); } ///  /// Conversion of float to Radians ///  ///  ///  public static implicit operator Radians(float f) { return new Radians((double)f); } ///  /// Conversion of double to Radians ///  ///  ///  public static implicit operator Radians(double dbl) { return new Radians(dbl); } ///  /// Conversion of Radians to double ///  ///  ///  public static implicit operator double(Radians rad) { return rad.Value; } ///  /// Add Radians and a double ///  ///  ///  ///  public static Radians operator +(Radians rad, double dbl) { return new Radians(rad.Value + dbl); } ///  /// Add Radians to Radians ///  ///  ///  ///  public static Radians operator +(Radians rad1, Radians rad2) { return new Radians(rad1.Value + rad2.Value); } ///  /// Add Radians and Degrees ///  ///  ///  ///  public static Radians operator +(Radians rad, Degrees deg) { return new Radians(rad.Value + deg.GetRadians().Value); } ///  /// Sets Radians value negative ///  ///  ///  public static Radians operator -(Radians rad) { return new Radians(-rad.Value); } ///  /// Subtracts a double from Radians ///  ///  ///  ///  public static Radians operator -(Radians rad, double dbl) { return new Radians(rad.Value - dbl); } ///  /// Subtracts Radians from Radians ///  ///  ///  ///  public static Radians operator -(Radians rad1, Radians rad2) { return new Radians(rad1.Value - rad2.Value); } ///  /// Subtracts Degrees from Radians ///  ///  ///  ///  public static Radians operator -(Radians rad, Degrees deg) { return new Radians(rad.Value - deg.GetRadians().Value); } #endregion public override string ToString() { return String.Format("{0}", this.Value); } public static Radians Convert(object value) { if (value is Radians) return (Radians)value; if (value is Degrees) return (Degrees)value; return System.Convert.ToDouble(value); } } 

学位

 public struct Degrees { public double Value; public Degrees(double value) { this.Value = value; } public Radians GetRadians() { return this; } public Degrees Reduce() { return this.GetRadians().Reduce(); } public double Cos { get { return System.Math.Cos(this.GetRadians()); } } public double Sin { get { return System.Math.Sin(this.GetRadians()); } } #region operator overloading public static implicit operator Degrees(Radians rad) { return new Degrees(rad.Value * 180 / System.Math.PI); } public static implicit operator Degrees(int i) { return new Degrees((double)i); } public static implicit operator Degrees(float f) { return new Degrees((double)f); } public static implicit operator Degrees(double d) { return new Degrees(d); } public static implicit operator double(Degrees deg) { return deg.Value; } public static Degrees operator +(Degrees deg, int i) { return new Degrees(deg.Value + i); } public static Degrees operator +(Degrees deg, double dbl) { return new Degrees(deg.Value + dbl); } public static Degrees operator +(Degrees deg1, Degrees deg2) { return new Degrees(deg1.Value + deg2.Value); } public static Degrees operator +(Degrees deg, Radians rad) { return new Degrees(deg.Value + rad.GetDegrees().Value); } public static Degrees operator -(Degrees deg) { return new Degrees(-deg.Value); } public static Degrees operator -(Degrees deg, int i) { return new Degrees(deg.Value - i); } public static Degrees operator -(Degrees deg, double dbl) { return new Degrees(deg.Value - dbl); } public static Degrees operator -(Degrees deg1, Degrees deg2) { return new Degrees(deg1.Value - deg2.Value); } public static Degrees operator -(Degrees deg, Radians rad) { return new Degrees(deg.Value - rad.GetDegrees().Value); } #endregion public override string ToString() { return String.Format("{0}", this.Value); } public static Degrees Convert(object value) { if (value is Degrees) return (Degrees)value; if (value is Radians) return (Radians)value; return System.Convert.ToDouble(value); } } 

一些示例用法

这些在使用API​​时确实很有用。 虽然在内部,您的组织可能决定严格遵守学位弧度以避免混淆,但至少对于这些类,您可以使用最有意义的类型。 例如,公共使用的API或GUI API可以使用Degrees而您的重数学/三角或内部使用可能使用Radians 。 考虑以下类/打印function:

 public class MyRadiansShape { public Radians Rotation { get; set; } } public class MyDegreesShape { public Degrees Rotation { get; set; } } public static void PrintRotation(Degrees degrees, Radians radians) { Console.WriteLine(String.Format("Degrees: {0}, Radians: {1}", degrees.Value, radians.Value)); } 

是的,代码非常人为(而且非常模糊)但是没关系! 只是展示它如何帮助减少意外混淆。

 var radiansShape = new MyRadiansShape() { Rotation = Math.PI / 2}; //prefer "Radians.HALF_PI" instead, but just as an example var degreesShape = new MyDegreesShape() { Rotation = 90 }; PrintRotation(radiansShape.Rotation, radiansShape.Rotation); PrintRotation(degreesShape.Rotation, degreesShape.Rotation); PrintRotation(radiansShape.Rotation + degreesShape.Rotation, radiansShape.Rotation + degreesShape.Rotation); //Degrees: 90, Radians: 1.5707963267949 //Degrees: 90, Radians: 1.5707963267949 //Degrees: 180, Radians: 3.14159265358979 

然后它们对于实现基于角度的其他数学概念非常有用,例如极坐标:

 double distance = 5; Polar polarCoordinate = new Polar(distance, (degreesShape.Rotation - radiansShape.Rotation) + Radians.QUARTER_PI); Console.WriteLine("Polar Coordinate Angle: " + (Degrees)polarCoordinate.Angle); //because it's easier to read degrees! //Polar Coordinate Angle: 45 

最后,您可以实现Point2D类(或使用System.Windows.Point)与Polar隐式转换:

 Point2D cartesianCoordinate = polarCoordinate; Console.WriteLine(cartesianCoordinate.X + ", " + cartesianCoordinate.Y); //3.53553390593274, 3.53553390593274 

正如我所说,我想在这些类中进行另一次传递,并且可能消除对Radiansdouble隐式转换,以避免可能的几个极端情况混淆和编译器歧义。 在我们创建静态ONE_PIHALF_PI (等等)字段之前,我们实际上就是那里,我们从Math.PI double的多个转换。

编辑:这是Polar类,作为其他隐式转换的演示。 它利用了Radians类(以及它的隐式转换)以及它上面的辅助方法和Point2D类。 我没有把它包含在这里,但Polar类可以很容易地实现与Point2D类交互的操作符,但这些与本讨论无关。

 public struct Polar { public double Radius; public Radians Angle; public double X { get { return Radius * Angle.Cos; } } public double Y { get { return Radius * Angle.Sin; } } public Polar(double radius, Radians angle) { this.Radius = radius; this.Angle = angle; } public Polar(Point2D point) : this(point.Magnitude(), point.GetAngleFromOrigin()) { } public Polar(Point2D point, double radius) : this(radius, point.GetAngleFromOrigin()) { } public Polar(Point2D point, Point2D origin) : this(point - origin) { } public Point2D ToCartesian() { return new Point2D(X, Y); } public static implicit operator Point2D(Polar polar) { return polar.ToCartesian(); } public static implicit operator Polar(Point2D vector) { return new Polar(vector); } } 

当与其他类型进行自然而明确的转换时,您可以使用转换运算符。

比如说你有一个表示温度的数据类型:

 public enum TemperatureScale { Kelvin, Farenheit, Celsius } public struct Temperature { private TemperatureScale _scale; private double _temp; public Temperature(double temp, TemperatureScale scale) { _scale = scale; _temp = temp; } public static implicit operator Temperature(double temp) { return new Temperature(temp, TemperatureScale.Kelvin); } } 

使用隐式运算符,您可以为温度变量赋值,它将自动用作开尔文:

 Temperature a = new Temperature(100, TemperatureScale.Celcius); Temperature b = 373.15; // Kelvin is default 

我使用它来从DateTime"yyyyMMdd"或其对应的int (yyyyMMdd)值进行无缝转换。

例如:

 void f1(int yyyyMMdd); void f2(string yyyyMMdd); ... f1(30.YearsFrom(DateTime.Today)); f2(30.YearsFrom(DateTime.Today)); ... public static DateAsYyyyMmDd YearsFrom(this int y, DateTime d) { return new DateAsYyyyMmDd(d.AddYears(y)); } ... public class DateAsYyyyMmDd { private readonly DateTime date; public DateAsYyyyMmDd(DateTime date) { this.date = date; } public static implicit operator int(DateOrYyyyMmDd d) { return Convert.ToInt32(d.date.ToString("yyyyMMdd")); } public static implicit operator string(DateOrYyyyMmDd d) { return d.date.ToString("yyyyMMdd"); } } 

假设您有一个用于商店应用程序的产品(例如玩具)的类:

 class Product { string name; decimal price; string maker; //etc... } 

您可以定义可能执行以下操作的显式强制转换:

 public static explicit operator string(Product p) { return "Product Name: " + p.name + " Price: " + p.price.ToString("C") + " Maker: " + p.maker; // Or you might just want to return the name. } 

这样你做的事情如下:

 textBox1.Text = (string)myProduct; 

它会将输出格式化为Product类的显式运算符。


如果最终用户未明确预期此类转换,请勿提供转换运算符。

微软的意思是,如果您确实提供了转换运算符,则不会返回不期望的结果。 使用我们的Product类的最后一个示例,这将返回一个不期望的结果:

 public static explicit operator string(Product p) { return (p.price * 100).ToString(); //... } 

显然没有人会这样做,但如果其他人使用Product类并使用显式字符串转换,他们就不会期望它返回100的价格。

希望这可以帮助!

通常,如果两件事物在逻辑上可兑换。 我在这种情况下使用它们来提供更流畅的代码。 我有时也会使用它们来解决那些不像我期望的那样工作的语言function。

这是一个非常简单,人为的例子,它说明了最后一个与我在生产中使用的东西相似的想法……

 class Program {. static void Main(string[] args) { Code code1 = new Code { Id = 1, Description = "Hi" }; Code code2 = new Code { Id = 2, Description = "There" }; switch (code1) { case 23: // do some stuff break; // other cases... } } } public class Code { private int id; private string description; public int Id { get; set; } public string Description { get; set; } public static implicit operator int(Code code) { return code.Id; } }