C#的一个鲜为人知的特性是创建隐式或显式user-defined type conversions的可能性。 我已经写了6年的C#代码了,我从来没有用过它。所以,我担心我可能错过了很好的机会。
用户定义转换的合法用途是什么?您是否有比仅定义自定义方法更好的示例?
-
事实证明,微软有一些design guidelines转换,其中最相关的是:
如果此类转换不明确,请勿提供转换运算符 期待最终用户。
但什么时候转换是“预期的”?在玩具编号课程之外,我无法弄清楚任何真实世界的用例。
以下是答案中提供的示例摘要:
模式似乎是:隐式转换大多数(仅?)在定义数值/值类型时很有用,转换由公式定义。回想起来,这是显而易见的。不过,我想知道非数字类是否也可以从隐式转换中受益..?
答案 0 :(得分:9)
当与其他类型进行自然而明确的转换时,您可以使用转换运算符。
例如,假设您有一种表示温度的数据类型:
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
答案 1 :(得分:6)
正如评论中所提到的,度和旋转是避免混淆双值的一个很好的例子,尤其是在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时,这些确实很有用。虽然在内部,您的组织可能决定严格遵守度或弧度以避免混淆,但至少对于这些类,您可以使用最有意义的类型。例如,公共使用的API或GUI API可以使用Degrees
,而您的重数学/触发或内部使用可能使用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
类(或使用System.Windows.Point),隐式转换为/ Polar
:
Point2D cartesianCoordinate = polarCoordinate;
Console.WriteLine(cartesianCoordinate.X + ", " + cartesianCoordinate.Y);
//3.53553390593274, 3.53553390593274
正如我所说的,我想在这些类中进行另一次传递,并且可能会消除double
隐式转换到Radians
,以避免可能出现的几个极端情况混淆和编译模糊。在我们创建静态ONE_PI
,HALF_PI
(等等)字段之前,我们实际上已经存在这些字段,而且我们正在从Math.PI
双倍的多个转换。
编辑:这是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);
}
}
答案 2 :(得分:2)
我用它来实现从DateTime
到"yyyyMMdd"
或其对应的int
(yyyyMMdd)值的无缝转换。
例如:
void f1(int yyyyMMdd);
void f2(string yyyyMMdd);
...
f1(30.YearsFrom(DateTime.Today));
f2(30.YearsFrom(DateTime.Today));
...
public static DateAsYyyyMmDd YearsFrom(this int y, DateTime d)
{
return new DateAsYyyyMmDd(d.AddYears(y));
}
...
public class DateAsYyyyMmDd
{
private readonly DateTime date;
public DateAsYyyyMmDd(DateTime date)
{
this.date = date;
}
public static implicit operator int(DateOrYyyyMmDd d)
{
return Convert.ToInt32(d.date.ToString("yyyyMMdd"));
}
public static implicit operator string(DateOrYyyyMmDd d)
{
return d.date.ToString("yyyyMMdd");
}
}
答案 3 :(得分:1)
假设您有一个用于商店应用程序的产品(例如玩具)的课程:
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的意思是,如果您执行提供转换操作符,则不会返回不期望的结果。使用我们的Product
类的最后一个示例,这将返回一个不期望的结果:
public static explicit operator string(Product p)
{
return (p.price * 100).ToString();
//...
}
显然没有人会这样做,但如果其他人使用Product
类并使用显式字符串转换,他们就不会期望它将价格返回100.
希望这有帮助!
答案 4 :(得分:0)
一般来说,如果两件事物在逻辑上可兑换。我在这种情况下使用它们来提供更流畅的代码。我有时也会使用它们来解决语言功能,这些功能并不像我期望的那样完善。
这是一个非常简单,人为的例子,说明了最后一个与我在制作中使用过的东西相似的想法......
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;
}
}
答案 5 :(得分:0)
没有一般性答案。我会谨慎使用它,并且前提是保持代码易于理解且简单明了(即,它显示了预期的行为)。
因此,我可以根据一个实际示例为您提供答案,如果您遵循它,您将了解何时使用以及何时最好不使用转换运算符:
我最近想以一种更简单的方式来处理Guids。我的设计目标是:简化语法和初始化,简化转换和变量分配。
如您所知,如果需要创建GUID,则使用起来有点麻烦:
示例1:
开箱即用:
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")
};
如果您只能将GUID字符串隐式转换为字符串,例如:
var guids = new EasyGuid[] {
"2f78c861-e0c3-4d83-a2d2-cac269fb87f1", "2f78c861-e0c3-4d83-a2d2-cac269fb87f2",
"2f78c861-e0c3-4d83-a2d2-cac269fb87f3"
};
这将允许直接将JSON文件中的GUID列表粘贴到C#代码中。
示例2:
要初始化数组,您需要执行以下操作:
var guids = new Guid[30];
for (int i = 0; i < 30; i++)
{
guids[i] = System.Guid.Empty; // Guid with 000...
}
像这样的Guid会更容易吗?
var guids = new EasyGuid[30]; // create array with 30 Guids (value null)
然后可以使用两个示例中的向导
foreach (Guid g in guids)
{
g.Dump();
}
换句话说,它们可以在需要使用时隐式转换为“普通” Guid。在第二个示例中,如果它们为null,则会在运行中隐式分配一个Empty Guid。
您该怎么做?您不能从System.Guid继承。但是您可以使用隐式转换。看一看这个类,我称它为EasyGuid
,它使上面的声明成为可能:
/// <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;
}
您会看到EasyGuid
可以通过调用ToGuid()
隐式或显式地将字符串隐式转换为EasyGuid,并且可以将EasyGuid转换为Guid。也可以将其打印为字符串,因为我已覆盖.ToString()
。
最后,我希望能够轻松地动态生成新的GUID。我是通过写作实现的。
// 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();
会产生以下效果
var guids = new EasyGuid[30];
将在转换为GUID时立即生成新的GUID。但是我从@OskarBerggren得到了反馈,即这种方法虽然易于实现,但会引起混乱-对于其他阅读它的人来说,该代码不再明显(谢谢,Oskar提供此提示!)。它还可能导致意外的问题(错误)。记住微软所说的话:
如果最终用户不能明确预期转换,请不要提供转换操作符。
因此,我不是通过隐式转换来实现此目的,而是通过如下扩展方法实现的:
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;
}
}
这更加直接,因为现在您可以选择:
// shorter: .ToGuids(true)
var guids = new EasyGuid[30].ToGuids(replaceNullByNewGuid: true);
如果您只想创建一个包含空Guid的数组:
var guids = new EasyGuid[30].ToGuids();
这样做(与GUIDS列表相同)。
此示例表明,隐式转换运算符很容易因意外行为而引起混乱。有时,最好使用扩展方法(如下所示)。
我认为这个示例表明,在某些情况下,转换运算符可以使您的生活更轻松,而在其他情况下,您应该停下来思考并找到更明显的实现方式。
出于完整性考虑:其他情况是
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