C# 中的运算符重载介绍
为您的类型提供转换、二元、一元和比较运算符。
引言
运算符重载是一个强大且未充分利用(但经常被误用)的功能,它可以帮助您的代码更简洁,您的对象使用起来更直观。为您的类或结构添加一些简单的运算符重载可以让您
- 允许您的类型与其他类型之间进行转换
- 对您的类型自身或其他类型执行数学/逻辑运算
转换运算符
类型之间有几种转换方式,但最常见的是使用隐式/显式转换运算符。
隐式
隐式意味着您可以将一种类型的实例直接分配给另一种类型。
我们来创建一个例子。
public struct MyIntStruct
{
int m_IntValue;
private MyIntStruct(Int32 intValue)
{
m_IntValue = intValue;
}
public static implicit operator MyIntStruct(Int32 intValue)
{
return new MyIntStruct(intValue);
}
}
一旦我们的结构中有了这个,我们就可以通过简单地为其赋值来从代码中创建它的新实例——我们的代码中不需要进行任何强制转换或新的声明。
MyIntStruct myIntStructInstance = 5;
要将我们的结构隐式转换回一个 int
,我们需要添加另一个转换运算符
public static implicit operator Int32(MyIntStruct instance)
{
return instance.m_IntValue;
}
现在,我们可以将 myIntStructInstance
直接赋值给一个 int
。
int i = myIntStructInstance;
我们还可以调用任何其他将接受 int
的函数,并将我们的实例直接传递给它。我们可以对任意多种类型执行此操作。
也许,我们的结构还有一个字符串成员变量,在这种情况下,能够通过直接从字符串赋值来创建它的实例可能很有用。这个结构可能看起来像这样...
public struct MyIntStringStruct
{
int m_IntValue;
string m_StringValue;
private MyIntStringStruct(Int32 intValue)
{
m_IntValue = intValue;
m_StringValue = string.Empty; // default value
}
private MyIntStringStruct(string stringValue)
{
m_IntValue = 0; // default value
m_StringValue = stringValue;
}
public static implicit operator MyIntStringStruct(Int32 intValue)
{
return new MyIntStringStruct(intValue);
}
public static implicit operator MyIntStringStruct(string stringValue)
{
return new MyIntStringStruct(stringValue);
}
public static implicit operator Int32(MyIntStringStruct instance)
{
return instance.m_IntValue;
}
}
... 我们可以像以前一样通过赋值一个 int
值或一个 string
来创建实例。
MyIntStringStruct myIntStringStructInstance = "Hello World";
请注意,我没有创建从我们的结构到 string
的隐式转换运算符。这样做在某些情况下会产生编译器无法处理的歧义。要查看此情况,请添加以下内容
public static implicit operator string(MyIntStringStruct instance)
{
return instance.m_StringValue;
}
这编译时没有问题。现在,尝试这个
MyIntStringStruct myIntStringStructInstance = "Hello World";
Console.WriteLine(myIntStringStructInstance); // compile fails here
编译器将因“以下方法或属性之间调用不明确...”而失败。控制台很乐意接受一个 int
或一个 string
(当然还有许多其他类型),那么我们如何期望它知道要使用哪个呢?答案是使用显式转换运算符。
Explicit (显式)
显式意味着我们必须在代码中显式执行转换。将最后一个转换运算符中的关键字 implicit
更改为 explicit
,使其看起来像这样。
public static explicit operator string(MyIntStringStruct instance)
{
return instance.m_StringValue;
}
现在,我们可以返回字符串值,但前提是我们显式转换为 string
。
MyIntStringStruct myIntStringStructInstance = "Hello World";
Console.WriteLine((string)myIntStringStructInstance);
我们得到了预期的输出。
隐式与显式
隐式比显式方便得多,因为我们无需在代码中显式转换,但正如我们上面所看到的,可能会出现问题。然而,还有其他事情需要牢记,例如:如果对象在任何方向上传递时会被四舍五入或截断,那么您应该使用显式转换,这样使用您的对象的人就不会因为数据丢失而被蒙蔽。
想象一下一系列使用小数进行的复杂计算,我们为了方便允许我们的结构隐式转换为小数,但存储它的成员变量是一个整数。用户理所当然地会期望任何隐式传入或传出小数的对象都能保持精度,但我们的结构会丢失所有小数位,这可能导致他们计算的最终结果严重错误!
如果他们必须显式地转换到或从我们的结构,那么我们实际上是在发出警告信号。在这种特殊情况下,不便用户且既不使用隐式也不使用显式可能(是)更好,这样他们就必须转换为 int
才能使用我们的结构,然后就不会有误解。
还有其他问题——在使用 implicit
之前请做一些研究。如果有疑问,请使用 explicit
,或者根本不要为该类型实现转换运算符。
二元运算符
二元运算符(令人惊讶地!)接受两个参数。以下运算符可以重载:+
, -
, *
, /
, %
, &
, |
, ^
, <<
, >>
。
注意: 使用运算符(无论是二元还是其他)时,不要执行任何意料之外的操作,这一点很重要。
这些运算符通常非常合乎逻辑。我们以第一个 +
为例。它通常将两个参数相加。
int a = 1;
int b = 2;
int c = a + b; // c = 3
然而,string
类使用 +
运算符进行连接。
string x = "Hello";
string y = " World";
string z = x + y; // z = Hello World
因此,根据您的情况,您可以执行任何逻辑上需要的操作。
想象一个包含两个 int
的结构,并且您有两个希望相加的实例。逻辑上应该做的是将每个实例中相应的 int
相加。您可能还希望将一个 int
添加到两个值中。生成的结构将看起来像这样。
public struct MySize
{
int m_Width;
int m_Height;
public MySize(int width, int height)
{
m_Width = width;
m_Height = height;
}
public static MySize operator +(MySize mySizeA, MySize mySizeB)
{
return new MySize(
mySizeA.m_Width + mySizeB.m_Width,
mySizeA.m_Height + mySizeB.m_Height);
}
public static MySize operator +(MySize mySize, Int32 value)
{
return new MySize(
mySize.m_Width + value,
mySize.m_Height + value);
}
}
然后,它显然可以像这样使用
MySize a = new MySize(1, 2);
MySize b = new MySize(3, 4);
MySize c = a + b; // Add MySize instances
MySize d = c + 5; // Add an int
您可以对所有可重载的二元运算符执行类似的操作。
一元运算符
一元运算符只接受一个参数。以下运算符可以重载:+
, -
, !
, ~
, ++
, --
, true
, false
。大多数类型不需要实现所有这些运算符,因此只实现您需要的那些!
使用上面使用的结构上的 ++
的一个简单示例
public static MySize operator ++(MySize mySize)
{
mySize.m_Width++;
mySize.m_Height++;
return mySize;
}
注意: ++
和 --
一元运算符应该(对于标准操作)改变您的结构的整数值,并返回相同的实例,而不是一个带有新值的新实例。
比较运算符
比较运算符接受两个参数。以下运算符可以重载。 [==
, !=
], [<
, >
], [<=
, >=
]。我将它们分组在方括号中,因为它们必须成对实现。
如果使用 ==
和 !=
,您还应该重写 Equals(object o)
和 GetHashCode()
。
public static bool operator ==(MySize mySizeA, MySize mySizeB)
{
return (mySizeA.m_Width == mySizeB.m_Width &&
mySizeA.m_Height == mySizeB.m_Height);
}
public static bool operator !=(MySize mySizeA, MySize mySizeB)
{
return !(mySizeA == mySizeB);
}
public override bool Equals(object obj)
{
if (obj is MySize)
return this == (MySize)obj;
return false;
}
public override int GetHashCode()
{
return (m_Width * m_Height).GetHashCode();
// unique hash for each area
}
比较运算符通常返回一个 bool
,尽管它们不必如此,但请记住,不要让最终用户感到惊讶!
其他运算符
这只剩下条件运算符 &&
, ||
,以及赋值运算符 +=
, -=
, *=
, /=
, %=
, &=
, |=
, ^=
, <<=
, >>=
。这些不可重载,但通过二元运算符进行评估。换句话说,提供二元运算符,您就可以免费获得这些运算符!
结论
我希望这篇关于运算符重载的介绍对您有用且富有启发性。如果您以前没有使用过它们,请尝试一下——它们可以是您工具箱中的一个有用工具。
- 不要过度使用它们。如果一个运算符或转换绝对不需要,或者结果不明显,请不要提供它。
- 不要滥用它们。是的,您可以让
++
递减值,但您绝对不应该这样做! - 在将您的类或结构隐式转换为其他类型时要非常小心。
- 请记住,结构是值类型,类是引用类型。这会对您在重载方法中处理事物的方式产生巨大影响。
MSDN 参考
历史
- 2008 年 8 月 31 日:初始版本。
- 2008 年 9 月 6 日:添加了
==
、!=
、Equals
和GetHashCode
示例。 - 2008 年 10 月 2 日:添加了关于从
++
和--
重载返回的注意事项。