Defining Operators (Optional)

Operator Overloading

The Rational class is a fine example of a useful utility class. Still, to an experienced user, it has a striking deficiency: A Rational is a number and we are used to doing arithmetic with standard operators. We would like to replace the mouthful frac1.Multiply(frac2) by our common symbolism for multiplication, frac1*frac2. This can be coded in C#, using operator overloading to give new meanings to the operator *. The C# syntax is illustrated in the variant of the Rational class in rational_ops_stub/rational.cs. This class also contains code discussed in the next section, Casts in User-Defined Classes. Here are operator overload declarations for * and others:

      /// * binary multiplication operator
      public static Rational operator *(Rational f1, Rational f2)
         return new Rational(f1.num*f2.num, f1.denom*f2.denom);

      /// - unary negation operator
      public static Rational operator - (Rational r)
         return new Rational(-r.num, r.denom);

      /// binary == operator
      public static Boolean operator ==(Rational f1, Rational f2)
         return f1.num == f2.num && f1.denom == f2.denom;

      /// binary != operator
      public static Boolean operator !=(Rational f1, Rational f2)
         return f1.num != f2.num || f1.denom != f2.denom;
      } // or extra call, but clearly consistent: return !(f1 == f2)

All operator overload headings have the special form

public static returnType operator opSymbol ( parameters )

Here opSymbol can be any arithmetic or comparison operator, or some other operators that we have not discussed. So something like operator * or operator - replaces the method name. Binary operations like multiplication require two operands, and hence the method has two parameters. The method computes and returns the named return type in the normal fashion. In general at least one of the parameters must be of the type of the class being defined.

(We could have directly defined four further overloads of *, with the first or second parameter being an int or a double, but we will avoid that by also adding methods to provide implicit Casts in User-Defined Classes.)

The - symbol is special, since it can be used either as a unary operator for negation, or as a binary operator for subtraction. Since we include only one parameter above, we are defining the unary version.

The operator does not need to produce a result of the same type. We included == and != as examples (returning a Boolean).

(These methods do not cause compiler errors, but warnings are generated: We have not added further more advanced overrides of Equals and HashCode methods, that are ideally in sync with the meaning of ==. You should see a discussion of these methods in a data structures course, like Loyola’s Comp 271.)

The class is a stub, and Operator Overloading Exercise invites you to add further operator overloads.

Precedence: Note that the operator overloading method definitions include nothing about Precedence of Operators. That is because the precedence of operators is fixed across the whole language. Unary - has higher precedence than * … no matter what the types involved.

An example testing class also uses the new casting syntax of next section:

Casts in User-Defined Classes

We have discussed casts before. We know that an int can also be represented as a double with an integer value, and the cast from int to double is done implicitly when needed: An expression like 3.2 * 2 is processed by the compiler, implicitly casting the 2 to double 2.0, and then doing a double multiplication. The same idea makes sense with an int n and a Rational f. We only defined the operator overload * for two Rationals, so in our code so far, f * n does not make sense. Mathematically an integer is rational, so mathematically, it should make sense. We bridge this difference by defining an implicit cast of an int to a Rational, so the compiler will take f * n and see the need to implicitly cast n to a Rational. The definition below will also allow explicit casts if you choose, like f * (Rational) n:

      /// Code to cast an int to a Rational implicitly when needed.
      public static implicit operator Rational(int n)
         return new Rational(n, 1);

Again it is the heading that takes a special form, starting with public static implicit operator followed by the type being cast to, like Rational, while the parameter is the starting type, like int. This is not like a regular method with its return type and method name. Here it looks something like a constructor with a type in place of a method name, but a constructor would not start with static implicit operator!

Now consider a double d and a Rational f. We would like to allow an expression like d * f. Again, the operator overload for * does not allow this directly, so consider implicit casts: Since a double is only an approximation, in general, it would not be wise to implicitly convert a double to a Rational, but it does make sense to approximate a Rational by a double before use with a double:

      /// Code to cast to a double implicitly when needed.
      public static implicit operator double(Rational f)
         return (double)f.num/f.denom;

The general format of such an implicit cast in a user-defined class is:

public static implicit operator resultType ( sourceType paramName )

One of the two types should be the type of the containing class. We have illustrated both combinations.

Finally, you need to be very careful where you declare implicit casts, to make sure you are not being overly general, and maybe allowing trouble in a form that may be very hard to debug: It is much harder to foresee and trace implicit actions than explicit actions. You are safe, but more verbose, if you only allow explicit casts. For example, we have already seen these required for a cast from double to int. To only allow an explicit cast with your type, replace implicit by explicit in the cast method heading.

      /// Code to cast to a decimal with an explicit cast.
      public static explicit operator decimal(Rational f)
         return (decimal)f.num/f.denom;

The decimal type: Though we have not used the decimal type before, we use it here for contrast to illustrate a cast from Rational to decimal that can only be used explicitly, as in (decimal) f:

Example rational_ops_stub/test_ops.cs tests all of the operator overloads and casts shown for a Rational. Look at the source code and run it. Note where overloaded operators are used and where implicit and explicit casts to or from a Rational are used.

The example also illustrates a special feature of the decimal type. While a double is encoded with a power of 2, so 0.1 is not stored accurately, a decimal is encoded with a power of 10, so exact decimal values with up to 28 digits can be stored and manipulated. (This is important for monetary calculations, so a decimal literal has m for money appended, like 5.99m, representing the mathematical quantity 5.99 exactly.)

Operator Overloading Exercise

The classes discussed above from example project rational_ops_stub are incomplete. Add the overloaded binary operators /, +, -, <, >, <= and >= to the Rational class, and extend the TestOps class to test them.