When should I define a (explicit or implicit) conversion operator in C#? [closed]
Asked Answered
D

6

11

A somewhat little-known feature of C# is the possibility to create implicit or explicit user-defined type conversions. I have been writing C# code for 6 years now, and I have never used it. So, I'm afraid I might be missing good opportunities.

What are legitimate, good uses of user-defined conversions? Do you have examples where they are better than just defining a custom method?

--

Turns out, Microsoft has some design guidelines about conversions, the most relevant of which is:

Do not provide a conversion operator if such conversion is not clearly expected by the end users.

But when is a conversion "expected"? Outside of toy number classes, I can't figure out any real-world use case.


Here's a summary of the examples provided in the answers:

  • Radians/Degrees/double
  • Polar/Point2D
  • Kelvin/Farenheit/Celsius

The pattern seems to be: implicit conversions are mostly (only?) useful when defining numerical/value types, the conversion being defined by a formula. In retrospect this is kind of obvious. Still, I wonder if non-numerical classes could also benefit from implicit conversions..?

Devorahdevore answered 26/8, 2012 at 1:0 Comment(3)
One example in terms of practical applications: at my work we created distinct Degrees and Radians classes to manage angles. They have implicit conversions to/from each other and Double and have immensely simplified our usage of angles and all but eliminated cases of accidentally using degrees instead of radians (and vice-versa) in various trigonometric functions.Hurd
Very nice! It's like you are recreating F#'s Units of Measure with C#'s user-defined conversions.Devorahdevore
I avoid conversion operators because they're not discoverable via IntelliSense like methods and constructors are, which makes it easier to miss that a conversion is defined.Hegemony
H
6

As mentioned in the comments, degrees and rotations are a good example to avoid mixing up double values, especially between APIs.

I pulled out the Radians and Degrees classes we're currently using and here they are. Taking a look at them now (after so long) I want to clean them up (especially the comments/documentation) and make sure they're properly tested. Thankfully, I've managed to get time in the scheduling to do so. At any rate, use these at your own risk, I can't guarantee if all the math here is correct as I'm pretty sure we haven't actually used/tested all the functionality we wrote in.

Radians

/// <summary>
/// Defines an angle in Radians
/// </summary>
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

    /// <summary>
    /// Angle value
    /// </summary>
    public double Value;
    /// <summary>
    /// Finds the Cosine of the angle
    /// </summary>
    public double Cos
    {
        get
        {
            return System.Math.Cos(this);
        }
    }
    /// <summary>
    /// Finds the Sine of the angle
    /// </summary>
    public double Sin
    {
        get
        {
            return System.Math.Sin(this);
        }
    }

    #endregion

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="value">angle value in radians</param>
    public Radians(double value)
    {
        this.Value = value;
    }
    /// <summary>
    /// Gets the angle in degrees
    /// </summary>
    /// <returns>Returns the angle in degrees</returns>
    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

    /// <summary>
    /// Conversion of Degrees to Radians
    /// </summary>
    /// <param name="deg"></param>
    /// <returns></returns>
    public static implicit operator Radians(Degrees deg)
    {
        return new Radians(deg.Value * System.Math.PI / 180);
    }
    /// <summary>
    /// Conversion of integer to Radians
    /// </summary>
    /// <param name="i"></param>
    /// <returns></returns>
    public static implicit operator Radians(int i)
    {
        return new Radians((double)i);
    }
    /// <summary>
    /// Conversion of float to Radians
    /// </summary>
    /// <param name="f"></param>
    /// <returns></returns>
    public static implicit operator Radians(float f)
    {
        return new Radians((double)f);
    }
    /// <summary>
    /// Conversion of double to Radians
    /// </summary>
    /// <param name="dbl"></param>
    /// <returns></returns>
    public static implicit operator Radians(double dbl)
    {
        return new Radians(dbl);
    }
    /// <summary>
    /// Conversion of Radians to double
    /// </summary>
    /// <param name="rad"></param>
    /// <returns></returns>
    public static implicit operator double(Radians rad)
    {
        return rad.Value;
    }
    /// <summary>
    /// Add Radians and a double
    /// </summary>
    /// <param name="rad"></param>
    /// <param name="dbl"></param>
    /// <returns></returns>
    public static Radians operator +(Radians rad, double dbl)
    {
        return new Radians(rad.Value + dbl);
    }
    /// <summary>
    /// Add Radians to Radians
    /// </summary>
    /// <param name="rad1"></param>
    /// <param name="rad2"></param>
    /// <returns></returns>
    public static Radians operator +(Radians rad1, Radians rad2)
    {
        return new Radians(rad1.Value + rad2.Value);
    }
    /// <summary>
    /// Add Radians and Degrees
    /// </summary>
    /// <param name="rad"></param>
    /// <param name="deg"></param>
    /// <returns></returns>
    public static Radians operator +(Radians rad, Degrees deg)
    {
        return new Radians(rad.Value + deg.GetRadians().Value);
    }
    /// <summary>
    /// Sets Radians value negative
    /// </summary>
    /// <param name="rad"></param>
    /// <returns></returns>
    public static Radians operator -(Radians rad)
    {
        return new Radians(-rad.Value);
    }
    /// <summary>
    /// Subtracts a double from Radians
    /// </summary>
    /// <param name="rad"></param>
    /// <param name="dbl"></param>
    /// <returns></returns>
    public static Radians operator -(Radians rad, double dbl)
    {
        return new Radians(rad.Value - dbl);
    }
    /// <summary>
    /// Subtracts Radians from Radians
    /// </summary>
    /// <param name="rad1"></param>
    /// <param name="rad2"></param>
    /// <returns></returns>
    public static Radians operator -(Radians rad1, Radians rad2)
    {
        return new Radians(rad1.Value - rad2.Value);
    }
    /// <summary>
    /// Subtracts Degrees from Radians
    /// </summary>
    /// <param name="rad"></param>
    /// <param name="deg"></param>
    /// <returns></returns>
    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);
    }
}

Degrees

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);
    }
}

Some sample usage

These really benefit when being used an an API. While, internally, your organization might decide to strictly stick with degrees or radians to avoid mixups, at least with these classes you can use the type that makes the most sense. For example, publicly consumed APIs or GUI APIs can use Degrees whereas your heavy math/trig or internal usage might use Radians. Considering the following classes/print 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));
}

Yeah, the code is pretty contrived (and terribly ambiguous) but that's OK! Just goes to show how it can help reduce accidental mixups.

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

Then they can be really useful for implementing other mathematical concepts based on angles, such as polar coordinates:

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

Then finally, you could implement a Point2D class (or use the System.Windows.Point) with implicit conversions to/from Polar:

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

As I said, I want to take another pass at these classes, and probably eliminate the double implicit conversions to Radians to avoid a couple corner case mixups and compiler ambiguities that are possible. Those were actually there before we created the static ONE_PI, HALF_PI (and so on) fields and we were converting from some multiple of the Math.PI double.

EDIT: Here's the Polar class as a demonstration of additional implicit conversions. It takes advantage of the Radians class (and thus its implicit conversions) and the helper methods on it and the Point2D class. I haven't included it here, but the Polar class can easily implement operators interacting with the Point2D class but those aren't relevant for this discussion.

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);
    }
}
Hurd answered 1/9, 2012 at 22:40 Comment(8)
I think your Reduce() doesn't work as intended on negative angles.Devorahdevore
The Radians/Degrees/double and Polar/Point2D both are good examples of using implicit conversions to make code safer, so I'm marking this as answer. I wonder if non-numerical classes can also benefit from implicit conversions.Devorahdevore
@EldritchConundrum I wouldn't be surprised. It's an old method ported from an old, old Flash system we had. It's intended to convert any angle to an equivalent between 0 and 2pi (which means converting a negative angle to an equivalent positive). My tests do show it working for negative angles, can you provide an angle you used or is that the behaviour you got (which is intended)? At anyrate, that's an example of a method I want to rewrite to be more clear anyway.Hurd
@EldritchConundrum Some quick examples of non-numericals might be localization strings "en-us" to an custom object, or we have in the past added ones to convert Color to an equivalent SolidColorBrush (for convenience). It's definitely one of those tools that Microsoft added that, while not strictly necessary is intended to make code more readable and maintainable (if not abused!). Just remembered, I answered a question using implicit operators to denote "compatible types" that were compile-time checked: https://mcmap.net/q/1016162/-handling-unsupported-types/…Hurd
Your usage of implicit conversions in list initialization syntax is an interesting abuse, of the "using C# as a DSL" kind. I am more uncertain about conversions from Color to SolidColorBrush, they may be better left explicit. And I think your Reduce() on values less than -2*pi still return a negative number, but I don't mind since I don't intend to use it :)Devorahdevore
@EldritchConundrum Just did a quick test and the Reduce method does work fine for values less than -2pi, but that just goes to show why I want to rewrite it to be more understandable. I totally agree that the implicit conversions in list initialization for compatible types is abuse and probably wouldn't employ it myself. I similarly agree that conversions from Color to SolidColorBrush. I think they're cases of what makes sense for each usage and company. For our developers, the brush conversions actually saved us some headaches and made things easier (somehow).Hurd
let us continue this discussion in chatDevorahdevore
I like this example, particularly because a rotation struct can restrict/modulo its values between 0-360 degrees (or 2PI) in the constructor, meaning you can increase a Rotation value infinitely and never get an overflow.Matos
P
9

You can use a conversion operator when there is a natural and clear conversion to or from a different type.

Say for example that you have a data type for representing temperatures:

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);
  }

}

Using the implicit operator you can assign a double to a temperature variable, and it will automatically be used as Kelvin:

Temperature a = new Temperature(100, TemperatureScale.Celcius);
Temperature b = 373.15; // Kelvin is default
Princeling answered 26/8, 2012 at 1:28 Comment(3)
In general, any unit of measure is a good candidate for this.Subpoena
Shouldn't you actually implement this as 3 separate classes (Kelvin, Farenheit, Celsius) with implicit conversions between themselves and double?Devorahdevore
@EldritchConundrum: That depends on what you decide that the object describes. I would say that it describes essentially an energy level, and Kelvin/Farenheit/Celsius are only different ways to describe it. Compare how DateTime describes a point in time, and Local/Universal is different ways to describe it.Princeling
H
6

As mentioned in the comments, degrees and rotations are a good example to avoid mixing up double values, especially between APIs.

I pulled out the Radians and Degrees classes we're currently using and here they are. Taking a look at them now (after so long) I want to clean them up (especially the comments/documentation) and make sure they're properly tested. Thankfully, I've managed to get time in the scheduling to do so. At any rate, use these at your own risk, I can't guarantee if all the math here is correct as I'm pretty sure we haven't actually used/tested all the functionality we wrote in.

Radians

/// <summary>
/// Defines an angle in Radians
/// </summary>
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

    /// <summary>
    /// Angle value
    /// </summary>
    public double Value;
    /// <summary>
    /// Finds the Cosine of the angle
    /// </summary>
    public double Cos
    {
        get
        {
            return System.Math.Cos(this);
        }
    }
    /// <summary>
    /// Finds the Sine of the angle
    /// </summary>
    public double Sin
    {
        get
        {
            return System.Math.Sin(this);
        }
    }

    #endregion

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="value">angle value in radians</param>
    public Radians(double value)
    {
        this.Value = value;
    }
    /// <summary>
    /// Gets the angle in degrees
    /// </summary>
    /// <returns>Returns the angle in degrees</returns>
    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

    /// <summary>
    /// Conversion of Degrees to Radians
    /// </summary>
    /// <param name="deg"></param>
    /// <returns></returns>
    public static implicit operator Radians(Degrees deg)
    {
        return new Radians(deg.Value * System.Math.PI / 180);
    }
    /// <summary>
    /// Conversion of integer to Radians
    /// </summary>
    /// <param name="i"></param>
    /// <returns></returns>
    public static implicit operator Radians(int i)
    {
        return new Radians((double)i);
    }
    /// <summary>
    /// Conversion of float to Radians
    /// </summary>
    /// <param name="f"></param>
    /// <returns></returns>
    public static implicit operator Radians(float f)
    {
        return new Radians((double)f);
    }
    /// <summary>
    /// Conversion of double to Radians
    /// </summary>
    /// <param name="dbl"></param>
    /// <returns></returns>
    public static implicit operator Radians(double dbl)
    {
        return new Radians(dbl);
    }
    /// <summary>
    /// Conversion of Radians to double
    /// </summary>
    /// <param name="rad"></param>
    /// <returns></returns>
    public static implicit operator double(Radians rad)
    {
        return rad.Value;
    }
    /// <summary>
    /// Add Radians and a double
    /// </summary>
    /// <param name="rad"></param>
    /// <param name="dbl"></param>
    /// <returns></returns>
    public static Radians operator +(Radians rad, double dbl)
    {
        return new Radians(rad.Value + dbl);
    }
    /// <summary>
    /// Add Radians to Radians
    /// </summary>
    /// <param name="rad1"></param>
    /// <param name="rad2"></param>
    /// <returns></returns>
    public static Radians operator +(Radians rad1, Radians rad2)
    {
        return new Radians(rad1.Value + rad2.Value);
    }
    /// <summary>
    /// Add Radians and Degrees
    /// </summary>
    /// <param name="rad"></param>
    /// <param name="deg"></param>
    /// <returns></returns>
    public static Radians operator +(Radians rad, Degrees deg)
    {
        return new Radians(rad.Value + deg.GetRadians().Value);
    }
    /// <summary>
    /// Sets Radians value negative
    /// </summary>
    /// <param name="rad"></param>
    /// <returns></returns>
    public static Radians operator -(Radians rad)
    {
        return new Radians(-rad.Value);
    }
    /// <summary>
    /// Subtracts a double from Radians
    /// </summary>
    /// <param name="rad"></param>
    /// <param name="dbl"></param>
    /// <returns></returns>
    public static Radians operator -(Radians rad, double dbl)
    {
        return new Radians(rad.Value - dbl);
    }
    /// <summary>
    /// Subtracts Radians from Radians
    /// </summary>
    /// <param name="rad1"></param>
    /// <param name="rad2"></param>
    /// <returns></returns>
    public static Radians operator -(Radians rad1, Radians rad2)
    {
        return new Radians(rad1.Value - rad2.Value);
    }
    /// <summary>
    /// Subtracts Degrees from Radians
    /// </summary>
    /// <param name="rad"></param>
    /// <param name="deg"></param>
    /// <returns></returns>
    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);
    }
}

Degrees

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);
    }
}

Some sample usage

These really benefit when being used an an API. While, internally, your organization might decide to strictly stick with degrees or radians to avoid mixups, at least with these classes you can use the type that makes the most sense. For example, publicly consumed APIs or GUI APIs can use Degrees whereas your heavy math/trig or internal usage might use Radians. Considering the following classes/print 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));
}

Yeah, the code is pretty contrived (and terribly ambiguous) but that's OK! Just goes to show how it can help reduce accidental mixups.

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

Then they can be really useful for implementing other mathematical concepts based on angles, such as polar coordinates:

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

Then finally, you could implement a Point2D class (or use the System.Windows.Point) with implicit conversions to/from Polar:

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

As I said, I want to take another pass at these classes, and probably eliminate the double implicit conversions to Radians to avoid a couple corner case mixups and compiler ambiguities that are possible. Those were actually there before we created the static ONE_PI, HALF_PI (and so on) fields and we were converting from some multiple of the Math.PI double.

EDIT: Here's the Polar class as a demonstration of additional implicit conversions. It takes advantage of the Radians class (and thus its implicit conversions) and the helper methods on it and the Point2D class. I haven't included it here, but the Polar class can easily implement operators interacting with the Point2D class but those aren't relevant for this discussion.

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);
    }
}
Hurd answered 1/9, 2012 at 22:40 Comment(8)
I think your Reduce() doesn't work as intended on negative angles.Devorahdevore
The Radians/Degrees/double and Polar/Point2D both are good examples of using implicit conversions to make code safer, so I'm marking this as answer. I wonder if non-numerical classes can also benefit from implicit conversions.Devorahdevore
@EldritchConundrum I wouldn't be surprised. It's an old method ported from an old, old Flash system we had. It's intended to convert any angle to an equivalent between 0 and 2pi (which means converting a negative angle to an equivalent positive). My tests do show it working for negative angles, can you provide an angle you used or is that the behaviour you got (which is intended)? At anyrate, that's an example of a method I want to rewrite to be more clear anyway.Hurd
@EldritchConundrum Some quick examples of non-numericals might be localization strings "en-us" to an custom object, or we have in the past added ones to convert Color to an equivalent SolidColorBrush (for convenience). It's definitely one of those tools that Microsoft added that, while not strictly necessary is intended to make code more readable and maintainable (if not abused!). Just remembered, I answered a question using implicit operators to denote "compatible types" that were compile-time checked: https://mcmap.net/q/1016162/-handling-unsupported-types/…Hurd
Your usage of implicit conversions in list initialization syntax is an interesting abuse, of the "using C# as a DSL" kind. I am more uncertain about conversions from Color to SolidColorBrush, they may be better left explicit. And I think your Reduce() on values less than -2*pi still return a negative number, but I don't mind since I don't intend to use it :)Devorahdevore
@EldritchConundrum Just did a quick test and the Reduce method does work fine for values less than -2pi, but that just goes to show why I want to rewrite it to be more understandable. I totally agree that the implicit conversions in list initialization for compatible types is abuse and probably wouldn't employ it myself. I similarly agree that conversions from Color to SolidColorBrush. I think they're cases of what makes sense for each usage and company. For our developers, the brush conversions actually saved us some headaches and made things easier (somehow).Hurd
let us continue this discussion in chatDevorahdevore
I like this example, particularly because a rotation struct can restrict/modulo its values between 0-360 degrees (or 2PI) in the constructor, meaning you can increase a Rotation value infinitely and never get an overflow.Matos
H
2

I use it to have seamless conversion from DateTime to "yyyyMMdd" or to its corresponding int (yyyyMMdd) value.

For example:

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");
    }
}
Hoekstra answered 30/12, 2013 at 20:55 Comment(1)
Interesting! This enables you to define YearsFrom only once, giving it several possible return types. Normally I would write a short overload for each desired return type, but your approach scales better with the number of functions to define. Also it does not lose any compile-time type-error checking.Devorahdevore
E
1

Say you have a class for a product (for example a toy) that you are using for a shops application:

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

You can define an explicit cast that might do the following:

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.
}

That way when you do something like:

textBox1.Text = (string)myProduct;

It will format the output to what was in the explicit operator for the Product class.


Do not provide a conversion operator if such conversion is not clearly expected by the end users.

What Microsoft means by this is that if you do provide a conversion operator that you do not return un-expected results. Using the last example of our Product class, this is something that would return an un-expected result:

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

Obviously no one would actually do this, but if someone else were to use the Product class and use an explicit string conversion, they would have not expected it to return the price times 100.

Hope this helps!

Eckart answered 26/8, 2012 at 1:16 Comment(3)
There is already a ToString() method for that. How is a conversion better? This doesn't seem like a compelling use case. I think MS means you should refrain from defining a conversion for this class.Devorahdevore
I'm not a big fan of the (string) myProduct example, because one would expect that to be handled by ToString. A better example would be if you were instead converting strings to Products.Lignite
I just used the string conversion as an example; you can use any conversion type that you think your class could be converted to. For example for the Product class you could define an explicit int operator that would return the price.Eckart
R
0

Generally, if two things are logically convertible. I use them in such situations to provide more fluent code. I also sometimes use them to get around language features that don't quite work the way I expect them to.

Here's a very simple, contrived example that illustrates the last idea that is similar to something I've used in production...

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;
    }
}
Reconstruct answered 26/8, 2012 at 1:20 Comment(2)
Yes, it looks a bit contrived... And here, the implicit conversion also enables to write "int n = code1 + code2;" which makes no sense. I think just writing "switch (code1.Id)" is ok.Devorahdevore
You are correct. But that's a chance you have to take sometimes. No matter what you do, you can't defeat stupididty.Reconstruct
C
0

There is no general answer. I would use it carefully, and only if keeps the code understandable and straight-forward (i.e. it is showing expected behavior).

So I can give you an answer based on a practical example, if you follow it along you will understand when to use and when better not to use conversion operators:

I recently wanted to have a simpler way to deal with Guids. My design goals were: Simplify Syntax and initialization, simplify conversions and variable assignments.

As you know, if you need to create GUIDs the usage is a bit cumbersome:

Example 1:

Out of the box:

var guids = new Guid[] {
  new Guid("2f78c861-e0c3-4d83-a2d2-cac269fb87f1"), new Guid("2f78c861-e0c3-4d83-a2d2-cac269fb87f2"),
  new Guid("2f78c861-e0c3-4d83-a2d2-cac269fb87f3")
};

What if you just could implicitly convert the GUID string to a string, like:

var guids = new EasyGuid[] {
    "2f78c861-e0c3-4d83-a2d2-cac269fb87f1", "2f78c861-e0c3-4d83-a2d2-cac269fb87f2",
    "2f78c861-e0c3-4d83-a2d2-cac269fb87f3"
};

That would allow to paste a list of GUIDs directly from a JSON file into C# code.

Example 2:

To initialize an array, you need to do the following:

var guids = new Guid[30];
for (int i = 0; i < 30; i++)
{
    guids[i] = System.Guid.Empty; // Guid with 000...
}

Wouldn't it be easier to just Guids like:

var guids = new EasyGuid[30]; // create array with 30 Guids (value null)

The guids from both examples could then be used like

foreach (Guid g in guids)
{
        g.Dump();
}

In other words, they can be implicitly converted into a "normal" Guid just when they need to be used. And in the 2nd example, if they are null, an Empty Guid is implicitly assigned on the fly.

How can you do that? You can't inherit from System.Guid. But you can use implicit conversions. Take a look at this class, I called it EasyGuid, it makes the declarations above possible:

/// <summary>
/// Easy GUID creation
/// Written by Matt, 2020
/// </summary>
public class EasyGuid
{
    // in case you want to replace GUID generation 
    // by RT.Comb, call Provider.PostgreSql.Create()
    private static System.Guid NewGuid => System.Guid.NewGuid();

    private System.Guid _guid = EasyGuid.NewGuid;

    public EasyGuid()
    {
        _guid = NewGuid;
    }

    public EasyGuid(string s)
    {
        _guid = new System.Guid(s); // convert string to Guid
    }

    // converts string to Guid
    public static implicit operator EasyGuid(string s) => new EasyGuid(s);

    // converts EasyGuid to Guid, create empty guid (Guid with 0) if null
    public static implicit operator System.Guid(EasyGuid g)
                                    => (g == null) ? System.Guid.Empty : g.ToGuid();

    // converts EasyGuid to Guid?, null will be passed through
    public static implicit operator System.Guid?(EasyGuid g)
                                    => (g == null) ? null : (Guid?)g.ToGuid();
                                    
    public override string ToString() => _guid.ToString();
    public System.Guid ToGuid() => _guid;
}

You can see that EasyGuid can implicitly convert a string into an EasyGuid and can convert an EasyGuid into a Guid - either implicitly or explicitly by calling ToGuid(). Also it can be printed as string because I have overridden .ToString().

Finally, I wanted to be able to easily generate new GUIDs on the fly. I achieved this by writing.

    // converts EasyGuid to Guid, create empty guid (Guid with 0) if null
    public static implicit operator System.Guid(EasyGuid g)
                                    => (g == null) ? EasyGuid.NewGuid : g.ToGuid();
                                    

Which would have the effect that

var guids = new EasyGuid[30]; 

would generate new GUIDs on the fly as soon as they are converted to GUID. But I got the feedback from @OskarBerggren that this approach - while it is easy to implement, would cause confusion - the code wouldn't be obvious any more for others reading it (thank you, Oskar for this hint!). It could also lead to unexpected issues (bugs). Remember what Microsoft said:

Do not provide a conversion operator if such conversion is not clearly expected by the end users.

So instead I implemented this not via implicit conversions, but with extension methods as follows:

public static class Extensions
{
    public static System.Guid[] ToGuids(this EasyGuid[] guidArray, bool replaceNullByNewGuid = false)
        => guidArray.ToList().ToGuids(replaceNullByNewGuid).ToArray();
        
    public static List<System.Guid> ToGuids(this List<EasyGuid> easyGuidList, bool replaceNullByNewGuid = false)
    {
        var guidList = new List<Guid>();
        foreach (var g in easyGuidList)
        {
            Guid result = (g!=null) ? g : ((replaceNullByNewGuid) ? new EasyGuid().ToGuid() : System.Guid.Empty);
            guidList.Add(result);
        }
        return guidList;
    }
}

This is more straightforward, since now you have the choice:

// shorter: .ToGuids(true)
var guids = new EasyGuid[30].ToGuids(replaceNullByNewGuid: true);

And if you just want to create an array with empty Guids:

var guids = new EasyGuid[30].ToGuids();

does it (same for list of GUIDS).

This example is showing that implict conversion operators can easily cause confusion by unexpected behavior. Sometimes, it is better to use extension methods (as shown here).

I think this example shows that there are occasions where the conversion operators can make your life easier, and others where you should stop and think and find more obvious ways of implementation.


For completeness: Other cases are:

var eg1 = new EasyGuid();  // simple case: new Guid
Guid g = eg1; g.Dump();    // straight-forward conversion
    
EasyGuid eg2 = null;       // null-handling
Guid g2 = eg2; g2.Dump();  // converted to 00000000-0000-0000-0000-000000000000
Guid? g3 = eg2; g3.Dump(); // will be null
Crosseye answered 4/8, 2020 at 8:32 Comment(4)
Changing the behavior of the default constructor of a type, using a wrapper type that implicitly converts to the wrapped type... Yes, it works. But I don't think it's a good idea to hide the conversions, it adds complexity... Unless if you're somehow using C# as a DSL, maybe.Devorahdevore
@Eldritch: As I wrote, I would use it carefully. And there are not many use cases where you really need the conversion operator. Here, it is also not really needed, it just simplifies the syntax. Whether to use it or not, is a matter of personal preferences. Sometimes, it is good to have an example, to know how it looks like - which allows you to estimate if you want to go that route or not.Crosseye
Specifically for array initialization, the expectation in C# is that allocating an array of some size will make all elements of the array have whatever is the default value for the type - often 0. It feels like a trap to have a type that produces a random value for each element instead.Clubwoman
@OskarBerggren - agreed, I have changed the code, now it assigns an empty guid in the array example.Crosseye

© 2022 - 2024 — McMap. All rights reserved.