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