Когда я должен определить (явный или неявный) оператор преобразования в 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;
}
}