Когда я должен определить (явный или неявный) оператор преобразования в C#?

несколько малоизвестной особенностью C# является возможность создания неявного или явного пользовательские преобразования типа. Я пишу код C# уже 6 лет, и я никогда не использовал его. Боюсь, я упускаю хорошие возможности.

Что такое законное, хорошее использование пользовательских преобразований? У вас есть примеры, где они лучше, чем просто определение пользовательского метода?

--

оказывается, у Microsoft есть некоторые правила разработки о конверсиях, наиболее релевантным из которых является:

не предоставляйте оператор преобразования, если такое преобразование не ясно ожидается конечными пользователями.

но когда преобразование "ожидается"? За исключением классов игрушек, я не могу понять ни одного реального случая использования.


вот краткое изложение примеров, приведенных в ответах:

  • радианы/Градусы/двойной
  • Полярный/Point2D
  • Келвин/По Фаренгейту/По Цельсию

шаблон выглядит так: неявные преобразования в основном (только?) полезно при определении числовых типов / значений, преобразование определяется формулой. Оглядываясь назад, это очевидно. Тем не менее, мне интересно, могут ли нечисловые классы также извлечь выгоду из неявных преобразований..?

5 ответов


как упоминалось в комментариях, Градусы и вращения являются хорошим примером, чтобы избежать смешивания двойных значений, особенно между API.

я вытащил Radians и Degrees классы, которые мы сейчас используем, и вот они. Взглянув на них сейчас (после стольких лет), я хочу очистить их (особенно комментарии/документацию) и убедиться, что они правильно протестированы. К счастью, мне удалось получить время в расписании, чтобы сделать так. Во всяком случае, используйте их по своему усмотрению риск, я не могу гарантировать, если все математика здесь правильная, так как я уверен, что мы на самом деле не используется/испытана все функции, которые мы писали в.

Радианс

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

градусов

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. В то время как внутренне ваша организация может решить строго придерживаться степеней или радианс, чтобы избежать mixups, по крайней мере, с этими классами вы можете использовать тип, который имеет наибольший смысл. Например, публично используемые API или API-интерфейсы GUI могут использовать Degrees в то время как ваш тяжелый math/trig или внутреннее использование может использовать Radians. Учитывая следующие классы/функции печати:

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 class (или использовать систему.Окна.Point) с неявными преобразованиями в/из Polar:

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

как я уже сказал, Я хочу пройти еще один проход на этих занятиях и, вероятно, исключить double неявные преобразования в Radians чтобы избежать пары угловых ошибок и неоднозначностей компилятора, которые вероятный. Они были на самом деле там, прежде чем мы создали static ONE_PI, HALF_PI (и так далее) поля, и мы преобразовывали из некоторого кратного Math.PI двойной.

EDIT: вот 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 to "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 класса.


не предоставляйте оператор преобразования, если такое преобразование явно не ожидается конечный пользователь.

что Microsoft имеет в виду под этим, что если вы do укажите оператор преобразования, который не возвращает ожидаемые результаты. Используя последний пример нашего Product класс, это то, что вернет неожиданный результат:

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

очевидно, никто на самом деле не сделал бы этого, но если бы кто-то другой использовал Product class и использовать явное преобразование строк, они не ожидали бы, что он вернет цену раз 100.

надеюсь, что это помогает!


обычно, если две вещи логически конвертируются. Я использую их в таких ситуациях, чтобы обеспечить более свободный код. Я также иногда использую их, чтобы обойти языковые функции, которые не совсем работают так, как я ожидаю.

вот очень простой, надуманный пример, который иллюстрирует последнюю идею, похожую на то, что я использовал в производстве...

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