It
is a design goal of C# that user-defined classes can have all the
functionality of built-in types. For example, suppose you have
defined a type to represent fractions. Ensuring that this class has
all the functionality of the built-in types means that you must be
able to perform arithmetic on instances of your fractions (e.g., add
two fractions, multiply, etc.) and convert fractions to and from
built-in types such as integer (int
). You could,
of course, implement methods for each operation and invoke them by
writing statements such as:
Fraction theSum = firstFraction.Add(secondFraction);
Although this will work, it is ugly and not how the built-in types are used. It would be much better to write:
Fraction theSum = firstFraction + secondFraction;
Statements like this are intuitive and consistent with how built-in
types, such as int
, are added.
In this chapter, you will learn techniques for adding standard operators to your user-defined types. You will also learn how to add conversion operators so that your user-defined types can be implicitly and explicitly converted to other types.
In C#, you implement operators
by creating static methods whose return values represent the result
of an operation and whose parameters are the operands. When you
create an operator for a class you say you have
“overloaded” that operator, much as
you might overload any member method. Thus, to overload the
addition
operator (+
), you would write:
public static Fraction operator+(Fraction lhs, Fraction rhs)
It is my convention to name the parameters lhs
and
rhs
. The parameter name lhs
stands for “lefthand side” and
reminds me that the first parameter represents the lefthand side of
the operation. Similarly, rhs
stands for
“righthand side.”
The C# syntax for overloading an operator is to write the word
operator
followed by the operator to overload. The
operator
keyword is a method modifier. Thus, to
overload the addition operator (+
), write
operator+
.
When you write:
Fraction theSum = firstFraction + secondFraction;
the overloaded +
operator is invoked, with the
first Fraction
passed as the first argument, and
the second Fraction
passed as the second argument.
When the compiler sees the expression:
firstFraction + secondFraction
it translates that expression into:
Fraction.operator+(firstFraction, secondFraction)
The result is that a new Fraction
is returned,
which in this case is assigned to the Fraction
object named theSum
.
C# provides the ability to overload operators for your classes, even though this is not, strictly speaking, in the CLS. Other .NET languages, such as VB.NET, might not support operator overloading, and it is important to ensure that your class supports the alternative methods that these other languages might call to create the same effect.
Thus, if you overload the addition operator (+
),
you might also want to provide an add()
method
that does the same work. Operator overloading ought to be a syntactic
shortcut, not the only path for your objects to accomplish a given
task.
Operator overloading can make your code more intuitive and enable it to act more like the built-in types. It can also make your code unmanageable, complex, and obtuse if you break the common idiom for the use of operators. Resist the temptation to use operators in new and idiosyncratic ways.
For example, although it might be tempting to overload the increment
operator (++
) on an employee class to invoke a
method incrementing the employee’s pay level, this
can create tremendous confusion for clients of your class. It is best
to use operator overloading sparingly, and only when its meaning is
clear and consistent with how the built-in classes operate.
It is quite common to overload the
equality operator (==
) to test whether two objects
are equal (however equality might be defined for your object). C#
insists that if you overload the equals operator, you must also
overload the not-equals operator (!=
). Similarly,
the less-than (<
) and greater-than
(>
) operators must be paired, as must the
less-than or equals (<=
) and greater-than or
equals (>=
) operators.
If you
overload the
equality
operator (==
), it is recommended that you also
override the virtual Equals( )
method provided by object
and route its
functionality back to the equals operator. This allows your class to
be polymorphic and provides
compatibility with other .NET languages that don’t
overload operators (but do support method overloading). The FCL
classes will not use the overloaded operators but will expect your
classes to implement the underlying methods. The
object
class implements the
Equals()
method with this signature:
public virtual bool Equals(object o)
By overriding this method, you allow your Fraction
class to act polymorphically with all other objects. Inside the body
of Equals( )
, you will need to ensure that you are
comparing with another Fraction
, and if so you can
pass the implementation along to the equals operator definition that
you’ve written:
public override bool Equals(object o) { if (! (o is Fraction) ) { return false; } return this == (Fraction) o; }
The is
operator is used to check whether the
runtime type of an object is compatible with the operand (in this
case, Fraction
). Thus, o
is
Fraction
will evaluate true
if o
is in fact a type compatible with
Fraction
.
C# converts int
to
long
implicitly, and allows you to convert
long
to int
explicitly. The
conversion from int
to long
is
implicit (it
happens without requiring any special syntax) and is safe because you
know that any int
will fit into the memory
representation of a long
. The reverse operation,
from long
to int
, must be
explicit (using
a cast operator) because it is possible to lose information in the
conversion:
int myInt = 5; long myLong; myLong = myInt; // implicit myInt = (int) myLong; // explicit
You must have the same functionality for your fractions. Given an
int
, you can support an implicit conversion to a
fraction because any whole value is equal to that value over 1 (e.g.,
15==15/1
).
Given a fraction, you might want to provide an explicit conversion
back to an integer, understanding that some value might be lost.
Thus, you might convert 9/4
to the integer value
2
.
When implementing your own conversions, the keyword
implicit
is used when the conversion is guaranteed
to succeed and no information will be lost; otherwise
explicit
is used.
Example 6-1 illustrates how you might implement
implicit and explicit conversions, and some of the operators of the
Fraction
class. (Although I’ve used
Console.WriteLine( )
to print messages illustrating
which method we’re entering, the better way to
pursue this kind of trace is with the debugger. You can place a
breakpoint on each test statement, and then step into the code,
watching the invocation of the constructors as they occur.) When you
compile this example, it will generate some warnings because
GetHashCode( )
is not implemented (see Chapter 9).
Example 6-1. Defining conversions and operators for the fraction class operators
public class Fraction { private int numerator; private int denominator; public Fraction(int numerator, int denominator) { Console.WriteLine("In Fraction Constructor(int, int)"); this.numerator=numerator; this.denominator=denominator; } public Fraction(int wholeNumber) { Console.WriteLine("In Fraction Constructor(int)"); numerator = wholeNumber; denominator = 1; } public static implicit operator Fraction(int theInt) { Console.WriteLine("In implicit conversion to Fraction"); return new Fraction(theInt); } public static explicit operator int(Fraction theFraction) { Console.WriteLine("In explicit conversion to int"); return theFraction.numerator / theFraction.denominator; } public static bool operator==(Fraction lhs, Fraction rhs) { Console.WriteLine("In operator =="); if (lhs.denominator == rhs.denominator && lhs.numerator == rhs.numerator) { return true; } // code here to handle unlike fractions return false; } public static bool operator !=(Fraction lhs, Fraction rhs) { Console.WriteLine("In operator !="); return !(lhs==rhs); } public override bool Equals(object o) { Console.WriteLine("In method Equals"); if (! (o is Fraction) ) { return false; } return this == (Fraction) o; } public static Fraction operator+(Fraction lhs, Fraction rhs) { Console.WriteLine("In operator+"); if (lhs.denominator == rhs.denominator) { return new Fraction(lhs.numerator+rhs.numerator, lhs.denominator); } // simplistic solution for unlike fractions // 1/2 + 3/4 == (1*4) + (3*2) / (2*4) == 10/8 int firstProduct = lhs.numerator * rhs.denominator; int secondProduct = rhs.numerator * lhs.denominator; return new Fraction( firstProduct + secondProduct, lhs.denominator * rhs.denominator ); } public override string ToString( ) { String s = numerator.ToString( ) + "/" + denominator.ToString( ); return s; } } public class Tester { static void Main( ) { Fraction f1 = new Fraction(3,4); Console.WriteLine("f1: {0}", f1.ToString( )); Fraction f2 = new Fraction(2,4); Console.WriteLine("f2: {0}", f2.ToString( )); Fraction f3 = f1 + f2; Console.WriteLine("f1 + f2 = f3: {0}", f3.ToString( )); Fraction f4 = f3 + 5; Console.WriteLine("f3 + 5 = f4: {0}", f4.ToString( )); Fraction f5 = new Fraction(2,4); if (f5 == f2) { Console.WriteLine("F5: {0} == F2: {1}", f5.ToString( ), f2.ToString( )); } } }
The Fraction
class begins with two constructors.
One takes a numerator and denominator, the other takes a whole
number. The constructors are followed by the declaration of two
conversion operators. The first conversion operator changes an
integer into a
Fraction
:
public static implicit operator Fraction(int theInt) { return new Fraction(theInt); }
This conversion is marked implicit
because any
whole number (int
) can be converted to a
Fraction
by setting the numerator to the
int
and the denominator to 1
.
Delegate this responsibility to the constructor that takes an
int
.
The second conversion operator is for the explicit conversion of
Fractions
into integers:
public static explicit operator int(Fraction theFraction) { return theFraction.numerator / theFraction.denominator; }
Because this example uses integer division, it will truncate the
value. Thus, if the fraction is 15/16
, the
resulting integer value will be 0
. A more
sophisticated conversion operator might accomplish rounding.
The conversion operators are followed by the equals operator
(==
) and the not equals operator
(!=
). Remember that if you implement one of these
equals operators, you must implement the other.
You have defined value equality for a Fraction
such that the numerators and denominators must match. For this
exercise, 3/4
and 6/8
aren’t considered equal. Again, a more sophisticated
implementation would reduce these fractions and notice the equality.
Include an override of the object class’
Equals()
method so that your
Fraction
objects can be treated polymorphically
with any other object. Your implementation is to delegate the
evaluation of equality to the equality operator.
A Fraction
class would, no doubt, implement all
the arithmetic operators (addition, subtraction, multiplication,
division). To keep the illustration simple, implement only addition,
and even here you simplify greatly. Check to see if the denominators
are the same; if so, add the following numerators:
public static Fraction operator+(Fraction lhs, Fraction rhs) { if (lhs.denominator == rhs.denominator) { return new Fraction(lhs.numerator+rhs.numerator, lhs.denominator); }
If the denominators aren’t the same, cross multiply:
int firstProduct = lhs.numerator * rhs.denominator; int secondProduct = rhs.numerator * lhs.denominator; return new Fraction( firstProduct + secondProduct, lhs.denominator * rhs.denominator );
This code is best understood with an example. If you were adding
1/2
and 3/4
, you can multiply
the first numerator (1
) by the second denominator
(4
) and store the result (4
) in
firstProduct
. You can also multiply the second
numerator (3
) by the first denominator
(2
) and store that result (6
)
in secondProduct
. You add these products
(6+4
) to a sum of 10
, which is
the numerator for the answer. You then multiply the two denominators
(2*4
) to generate the new denominator
(8
). The resulting fraction
(10/8
) is the correct answer.[1]
Finally, you override ToString()
so that
Fraction
can return its value in the format
numerator/denominator
:
public override string ToString() { String s = numerator.ToString() + "/" + denominator.ToString( ); return s; }
With your Fraction
class in hand,
you’re ready to test. Your first tests create simple
fractions, 3/4
and 2/4
:
Fraction f1 = new Fraction(3,4); Console.WriteLine("f1: {0}", f1.ToString()); Fraction f2 = new Fraction(2,4); Console.WriteLine("f2: {0}", f2.ToString( ));
The output from this is what you would expect—the invocation of
the constructors and the value printed in
WriteLine( )
:
In Fraction Constructor(int, int) f1: 3/4 In Fraction Constructor(int, int) f2: 2/4
The next line in Main( )
invokes the static
operator+
. The purpose of this operator is to add
two fractions and return the sum in a new fraction:
Fraction f3 = f1 + f2; Console.WriteLine("f1 + f2 = f3: {0}", f3.ToString());
Examining the output reveals how operator+
works:
In operator+ In Fraction Constructor(int, int) f1 + f2 = f3: 5/4
The operator+
is invoked, and then the constructor
for f3
, taking the two int
values representing the numerator and denominator of the resulting
new fraction.
The next test in Main()
adds an
int
to the Fraction
f3
and assigns the resulting value to a new
Fraction
, f4
:
Fraction f4 = f3 + 5; Console.WriteLine("f3 + 5: {0}", f4.ToString());
The output shows the steps for the various conversions:
In implicit conversion to Fraction In Fraction Constructor(int) In operator+ In Fraction Constructor(int, int) f3 + 5 = f4: 25/4
Notice that the implicit conversion operator was invoked to convert
5
to a fraction. In the return statement from the
implicit conversion operator, the Fraction
constructor was called, creating the fraction 5/1
.
This new fraction was then passed along with
Fraction
f3
to
operator+
, and the sum was passed to the
constructor for f4
.
In your final test, a new fraction (f5)
is
created. Test whether it is equal to f2
. If so,
print their values:
Fraction f5 = new Fraction(2,4); if (f5 == f2) { Console.WriteLine("F5: {0} == F2: {1}", f5.ToString( ), f2.ToString( )); }
The output shows the creation of f5
, and then the
invocation of the overloaded equals operator:
In Fraction Constructor(int, int) In operator == F5: 2/4 == F2: 2/4
[1] To recap: 1/2=4/8, 3/4=6/8, 4/8+6/8=10/8. The example doesn’t reduce the fraction, to keep the code simple.
18.223.170.63