Quando dovrei definire un operatore di conversione (esplicito o implicito) in C #?

Una caratteristica un po ‘poco conosciuta di C # è la possibilità di creare conversioni di tipo definite implicite o esplicite. Ho scritto codice C # per 6 anni e non l’ho mai usato. Quindi, temo che mi manchino le buone opportunità.

Quali sono gli usi legittimi e validi delle conversioni definite dall’utente? Avete esempi in cui sono migliori di una semplice definizione di un metodo personalizzato?

Si scopre che Microsoft ha alcune linee guida sulla progettazione delle conversioni, la più rilevante delle quali è:

Non fornire un operatore di conversione se tale conversione non è chiaramente prevista dagli utenti finali.

Ma quando è prevista una conversione? Al di fuori delle classi numeriche dei giocattoli, non riesco a capire nessun caso d’uso nel mondo reale.


Ecco un riepilogo degli esempi forniti nelle risposte:

  • Radianti / gradi / doppia
  • Polar / Point2D
  • Kelvin / Farenheit / Celsius

Il modello sembra essere: le conversioni implicite sono principalmente (solo?) Utili quando si definiscono tipi numerici / di valore, la conversione essendo definita da una formula. In retrospettiva, questo è ovvio. Tuttavia, mi chiedo se le classi non numeriche potrebbero anche beneficiare di conversioni implicite ..?

Come menzionato nei commenti, i gradi e le rotazioni sono un buon esempio per evitare di mescolare valori doppi, in particolare tra le API.

Ho tirato fuori le classi Radians and Degrees che stiamo usando attualmente ed Radians qui. Dando un’occhiata a loro ora (dopo tanto tempo) voglio pulirli (specialmente i commenti / documentazione) e assicurarmi che siano testati correttamente. Per fortuna, sono riuscito a ottenere il tempo necessario per farlo. Ad ogni modo, usali a tuo rischio e pericolo, non posso garantire se tutti i calcoli qui sono corretti in quanto sono abbastanza sicuro di non aver effettivamente usato / testato tutte le funzionalità in cui abbiamo scritto.

radianti

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

Gradi

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

Alcuni esempi di utilizzo

Questi sono davvero utili quando si utilizza un’API. Mentre, internamente, la tua organizzazione potrebbe decidere di attenersi rigorosamente ai gradi o ai radianti per evitare confusioni, almeno con queste classi puoi usare il tipo che ha più senso. Ad esempio, le API o le API della GUI consumate pubblicamente possono utilizzare Degrees mentre il tuo uso pesante / trig o l’uso interno potrebbero utilizzare i Radians . Considerando le seguenti classi / funzione di stampa:

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

Sì, il codice è abbastanza elaborato (e terribilmente ambiguo) ma va bene! Basta mostrare come può aiutare a ridurre i mix accidentali.

 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 

Quindi possono essere davvero utili per implementare altri concetti matematici basati su angoli, come le coordinate polari:

 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 

Infine, è ansible implementare una class Point2D (o utilizzare System.Windows.Point) con conversioni implicite in / da Polar :

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

Come ho detto, voglio fare un altro passo in queste classi e probabilmente eliminare le double conversioni implicite ai Radians per evitare un mix di casi in coppia e ambiguità del compilatore possibili. Quelli erano effettivamente lì prima che abbiamo creato i ONE_PI statici ONE_PI , HALF_PI (e così via) e stavamo convertendo da qualche multiplo del doppio Math.PI

EDIT: Ecco la class Polar come dimostrazione di ulteriori conversioni implicite. Sfrutta la class Radians (e quindi le sue conversioni implicite) e i metodi helper su di esso e la class Point2D . Non l’ho incluso qui, ma la class Polar può facilmente implementare operatori che interagiscono con la class Point2D ma quelli non sono rilevanti per questa discussione.

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

Puoi utilizzare un operatore di conversione quando esiste una conversione naturale e chiara ao da un tipo diverso.

Supponiamo ad esempio di avere un tipo di dati per rappresentare le temperature:

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

Usando l’operatore implicito puoi assegnare un doppio a una variabile di temperatura, e verrà automaticamente usato come Kelvin:

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

Lo uso per ottenere una conversione senza "yyyyMMdd" da DateTime a "yyyyMMdd" o al valore corrispondente int (yyyyMMdd).

Per esempio:

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

Supponi di avere un corso per un prodotto (ad esempio un giocattolo) che stai utilizzando per un’applicazione di un negozio:

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

È ansible definire un cast esplicito che potrebbe eseguire le seguenti operazioni:

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

In questo modo quando fai qualcosa come:

 textBox1.Text = (string)myProduct; 

Formatterà l’output su ciò che era nell’operatore esplicito per la class Product .


Non fornire un operatore di conversione se tale conversione non è chiaramente prevista dagli utenti finali.

Ciò che Microsoft intende con questo è che se si fornisce un operatore di conversione che non si restituiscono risultati non previsti. Utilizzando l’ultimo esempio della nostra class Product , questo è qualcosa che restituirebbe un risultato inatteso:

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

Ovviamente nessuno lo farebbe in realtà, ma se qualcun altro dovesse utilizzare la class Product e utilizzare una conversione stringa esplicita, non si sarebbero aspettati che restituisse i tempi di prezzo 100.

Spero che questo ti aiuti!

In generale, se due cose sono logicamente convertibili. Li uso in tali situazioni per fornire un codice più fluido. A volte li uso anche per aggirare caratteristiche linguistiche che non funzionano nel modo in cui mi aspetto che facciano.

Ecco un esempio molto semplice e forzato che illustra l’ultima idea simile a qualcosa che ho usato nella produzione …

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