65.9K
CodeProject 正在变化。 阅读更多。
Home

初学者教程:C# 中的运算符重载

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (25投票s)

2012年9月4日

CPOL

5分钟阅读

viewsIcon

131211

downloadIcon

847

C# 中可重载的各种运算符类型

引言

本文讨论 C# 中的操作符重载。可以重载哪些类型的操作符?我们将创建一个简单的示例应用程序,看看如何重载一些基本操作符。

背景

在 C# 这样的面向对象编程语言中,操作符重载为实现自定义类型的运算提供了一种更自然的方式。假设我们创建了一个复数类,并希望对该类型执行所有算术运算。一种方法是在类中定义 AddSubtract 等函数并实现相应的功能。另一种方法是实际为该类型提供重载的操作符版本。

操作符重载为类型提供了更自然的抽象。当我们考虑对某种数据类型可能的运算时,可以想到二元操作符、一元操作符、关系操作符以及可能的一些基本类型的转换操作。在 C# 中,通过操作符重载可以实现这一切。

Using the Code

在深入了解实现细节之前,让我们先看看如果我们想重载一个操作符需要遵循哪些约定。

  • 操作符函数应该是包含类型的成员函数。
  • 操作符函数必须是 static 的。
  • 操作符函数必须使用 operator 关键字后跟要重载的操作符。
  • 函数的参数是操作数。
  • 函数的返回值是运算的结果。

为了更清楚地理解上述要点,我们可以将操作符的调用想象成函数调用(下面的代码无法编译,仅用于理解目的)。

class A
{
    public static A operator+(A a1, A a2)
    {
        A temp;
        // perform actual addition
        return temp;
    }
}

// so the operations like 
A a1 = new A();
A a2 = new A();

A result = a1 + a2;

// This can actually be visualized as:
A result = A.+(a1, a2);
// Note: Only for understanding, this won't even compile

遵循上述指南,让我们看看如何实现操作符重载。我们将创建一个简单的 Rational 类来表示一个有理数,并在其中实现一些基本的操作符重载。

class Rational
{
    // Private variables to hold the data
    int numerator;
    int denominator;

    // Constructor
    public Rational(int num, int den)
    {
        numerator = num;
        denominator = den;
    }

    public double Value
    {
        get
        {
            return ((double)numerator) / denominator;
        }
    }
}

重载二元操作符

二元操作符函数将接受两个参数并返回一个包含类型的*新*对象。让我们尝试为我们的 Rational 类实现二元操作符 +

// Let us overload the Binary operators                
public static Rational operator +(Rational rational1, Rational rational2)
{
    int resultingDenominator = rational1.denominator * rational2.denominator;
    int resultingNumerator = rational1.numerator * rational2.denominator + 
        rational1.denominator * rational2.numerator;

    return new Rational(resultingNumerator, resultingDenominator);
}

一旦我们重载了二元操作符,就可以简单地这样使用它:

Rational r1 = new Rational(3, 2);
Rational r2 = new Rational(2, 3);

Rational result = null;

// Testing + operator on types
result = r1 + r2;

这里需要注意的重要一点是,一旦实现了二元操作符 +,该操作符的复合赋值操作符,即本例中的 +=,也会以该操作符,即本例中的 + 为基础来实现。

另一种可能性是我们可能希望与我们的类型进行混合模式算术运算,即我们可能希望算术运算能够与我们的 Rational 类型以及某种基本类型一起工作。因此,如果我们希望能够进行 Rational + int 的运算,那么我们也必须为它实现一个重载函数。

// Binary operators for Mix mode arithmetics (Only for + and with int)
public static Rational operator +(Rational rational1, int num)
{
    return new Rational(rational1.numerator +(rational1.denominator * num), 
                        rational1.denominator);
}

public static Rational operator +(int num, Rational rational1)
{
    //let us implement this in terms of other
    return rational1 + num;
}

//Usage
static void Main(string[] args)
{
    // Testing + operator in mix mode
    result = r1 + 2;
    Console.WriteLine(result.Value.ToString());

    // Testing + operator in mix mode
    result = 3 + r1;
    Console.WriteLine(result.Value.ToString());
}

重载一元操作符

要重载一元操作符,我们需要一个只接受包含类型的一个参数的函数。重载一元操作符时需要注意的重要一点是,我们不应该创建新对象,而应该*更改*传递给我们的对象的*值*并返回它。现在让我们为我们的 Rational 类型实现一元 ++

// Let us overload the unary operator ++ now (-- will be on same lines)
public static Rational operator ++(Rational rational1)
{
    rational1 += 1;
    return rational1;
}

//Usage
static void Main(string[] args)
{
    // Post increment ++
    result = r1++;
    Console.WriteLine(result.Value.ToString());

    // Pre increment ++
    result = ++r1;
    Console.WriteLine(result.Value.ToString());
}

上面的代码片段中需要注意的一点是,我们重载了 ++,C# 内部会自动处理它在前缀增量和后缀增量场景中的使用。因此,与 C++ 不同,我们不需要为前缀和后缀一元操作符实现单独的版本。

重载关系操作符

诸如 <> 之类的关系操作符也可以被重载为接受两个参数的函数。需要注意的重要一点是,我们需要成对重载关系操作符,即如果我重载 < 操作符,那么我也必须重载 > 操作符。对于 (<=, >=)(==, !=) 操作符也是如此。所以,让我们继续在我们的 Rational 类中实现关系操作符。

// Let us overload the relational operator
// < and > comes in pair, if we define one we need to define other too.
// Same is applicable for <= and >=. they also come in pair.
public static bool operator < (Rational rational1, Rational rational2)
{
    return rational1.Value < rational2.Value;
}

public static bool operator >(Rational rational1, Rational rational2)
{
    return rational1.Value > rational2.Value;
}

//Usage
static void Main(string[] args)
{
    // test relational operator
    Console.WriteLine(r1 > r2);
}

需要注意的另一个重要事项是实现相等性,即 == 操作符。如果我们重载了 == 操作符,那么我们也需要实现 != 操作符(如上所述)。此外,我们需要重写 EqualsGetHashCode 函数,以便如果我们的对象返回 == 的 true,那么它也应该为 Equals 函数返回 true,并且应该从 GetHashCode() 返回相同的值。

// Let is now override the equality operator
// == and != comes in pair, if we define one we need to define other too.
public static bool operator ==(Rational rational1, Rational rational2)
{
    return rational1.Value == rational2.Value;
}

public static bool operator !=(Rational rational1, Rational rational2)
{
    return rational1.Value != rational2.Value;
}

public override bool Equals(object obj)
{
    Rational r = obj as Rational;
    if (r != null)
    {
        return r == this;
    }
    return false;
}

public override int GetHashCode()
{
    return Value.GetHashCode();
}

//Usage
static void Main(string[] args)
{
    // test equality 
    Console.WriteLine(r1 == r2);
}

重载转换操作符

有时我们也可能需要实现转换操作符,以便我们的类型能够安全地转换为其他类型以及从其他类型转换过来。我们可以将转换操作符定义为 implicit(隐式)或 explicit(显式)。对于 implicit 转换,用户不必显式地将我们的类型转换为目标类型,我们的转换操作就会起作用。对于 explicit 转换,用户必须显式地将我们的类型转换为目标类型才能调用我们的转换操作。如果未执行类型转换,则会导致编译时错误。

现在,让我们为我们的 Rational 类型定义转换操作。我们将隐式转换 integer 类型到 Rational 类型,但我们将把从 Rationaldouble 的转换保持为显式。

// Finally, let us have some conversion operators
public static implicit operator Rational(int i)
{
    // since the rational equivalent of an int has 1 as denominator
    Rational rational = new Rational(i, 1);

    return rational;
}

public static explicit operator double(Rational r)
{
    double result = ((double)r.numerator) / r.denominator;
    return result;
}

//Usage
static void Main(string[] args)
{
    // implicit conversion from int to rational
    Rational ri = 3;

    //explicit conversion from rational to double
    double value = (double)ri;
}

现在我们已经为这个 Rational 类重载/实现了几个基本的操作符。我们在每个操作符类别中都实现了一些操作符,其余的操作符可以按照相同的方式进行重载。

注意:请参阅附件的示例代码,查看包含测试代码的完整类。

看点

本文旨在从初学者的角度对 C# 操作符重载进行一次实践性教程。大多数资深程序员已经了解这些基础知识,并觉得这些信息很普通,但我仍然希望这能有所帮助。

历史

  • 2012 年 9 月 4 日:初版
© . All rights reserved.