Tuesday, 27 December 2011

Lesson 18: Overloading Operators

This lesson shows you how to overload C# operators. Our objectives are as follows:
  • Understand what operator overloading is
  • Determine when it is appropriate to overload an operator
  • Learn how to overload an operator
  • Familiarize yourself with rules for operator overloading

About Operator Overloading

In Lesson 2, you learned what operators were available in C#, which included + (plus), - (minus), ^ (exclusive or), and others. Operators are defined for the built-in types, but that's not all. You can add operators to your own types, allowing them to be used much like the operators with the built-in C# types.
To understand the need for operator overloading, imagine that you need to perform matrix math operations in your program. You could instantiate a couple 2-dimensional arrays and do what you need. However, add the requirement for the matrix behavior to be reusable. Because you need to do the same thing in other programs and want to take advantage of the fact that you have already written the code, you will want to create a new type.
So, you create a Matrix type, which could be a class or a struct. Now consider how this Matrix type would be used. You would want to initialize two or more Matrix instances with data and then do a mathematical operation with them, such as add or get a dot product. To accomplish the mathematical operation, you could implement an Add(), DotProduct(), and other methods to get the job done. Using the classes would look something like this:
Matrix result = mat1.Add(mat2);  // instance
or
Matrix result = Matrix.Add(mat1, mat2);  // static
or even worse
Matrix result = mat1.DotProduct(mat2).DotProduct(mat3); // and so on...
The problem with using methods like this is that it is cumbersome, verbose, and unnatural for the problem you are trying to solve. It would be much easier to have a + operator for the add operation and a * operator for the dot product operation. The following shows how the syntax appears using operators:
Matrix result = mat1 + mat2;
or
Matrix result = mat1 * mat2;
or even better
Matrix result = mat1 * mat2 * mat3 * mat4;
This is much more elegant and easier to work with. For a single operation, one could argue that the amount of work to implement one syntax over the other is not that great. However, when chaining multiple mathematical operations, the syntax is much simpler. Additionally, if the primary users of your type are mathematicians and scientists, operators are more intuitive and natural.

When Not to Use Operator Overloading

A lot of the discussion, so far, has emphasized the need to write code and implement types in the simplest and most natural way possible. A very important concept to remember is that although operators are simple, they are not always natural. In the example above it made sense to use operators with the Matrix type. This is similar to the reason why operators make sense with the built-in types such as int and float. However, it is easy to abuse operators and create convoluted implementations that are hard for anyone, including the original author, to understand.
For an example of a bad implementation, consider a Car class that needs an implementation allowing you to park the car in a garage. It would be a mistake to think that the following implementation was smart:
Car mySedan = new Car();
Garage parkingGarage = new Garage();

mySedan = mySedan + parkingGarage; // park car in the garage
This is bad code. If you ever have the temptation to do something like this - don't. No one will truly understand what it means and they will not think it is clever. Furthermore, it hurts the maintainability of the application because it is so hard to understand what the code does. Although the comment is there, it doesn't help much and if it wasn't there, it would be even more difficult to grasp the concept of adding a Car and a Garage.
The idea is this: Use operators where they lend understanding and simplicity to a type. Otherwise, do not use them.

Implementing an Overloaded Operator

The syntax required to implement an overloaded operator is much the same as a static method with a couple exceptions. You must use the operator keyword and specify the operator symbol being overloaded. Here's a skeleton example of how the dot product operator could be implemented:
public static Matrix operator *(Matrix mat1, Matrix mat2)
{
    // dot product implementation
}
Notice that the method is static. Use the keyword operator after specifying the return type, Matrix in this case. Following the operator keyword, the actual operator symbol is specified and then there is a set of parameters to be operated on. See Listing 18-1 for a full example of how to implement and use an overloaded operator.
Listing 18-1. Implementing an Overloaded Operator: Matrix.cs
using System;

class Matrix
{
 public const int DimSize = 3;
 private double[,] m_matrix = new double[DimSize, DimSize];

 // allow callers to initialize
 public double this[int x, int y]
 {
  get { return m_matrix[x, y]; }
  set { m_matrix[x, y] = value; }
 }

 // let user add matrices
 public static Matrix operator +(Matrix mat1, Matrix mat2)
 {
  Matrix newMatrix = new Matrix();

  for (int x=0; x < DimSize; x++)
   for (int y=0; y < DimSize; y++)
    newMatrix[x, y] = mat1[x, y] + mat2[x, y];

  return newMatrix;
 }
}

class MatrixTest
{
 // used in the InitMatrix method.
 public static Random m_rand = new Random();

 // test Matrix
 static void Main()
 {
  Matrix mat1 = new Matrix();
  Matrix mat2 = new Matrix();

  // init matrices with random values
  InitMatrix(mat1);
  InitMatrix(mat2);
  
  // print out matrices
  Console.WriteLine("Matrix 1: ");
  PrintMatrix(mat1);

  Console.WriteLine("Matrix 2: ");
  PrintMatrix(mat2);

  // perform operation and print out
            results
  Matrix mat3 = mat1 + mat2;

  Console.WriteLine();
  Console.WriteLine("Matrix 1 + Matrix
            2 = ");
  PrintMatrix(mat3);

  Console.ReadLine();
 }

 // initialize matrix with random values
 public static void InitMatrix(Matrix mat)
 {
  for (int x=0; x < Matrix.DimSize; x++)
   for (int y=0; y < Matrix.DimSize; y++)
    mat[x, y] = m_rand.NextDouble();
 }

 // print matrix to console
 public static void PrintMatrix(Matrix mat)
 {
  Console.WriteLine();
  for (int x=0; x < Matrix.DimSize; x++)
  {
   Console.Write("[ ");
   for (int y=0; y < Matrix.DimSize; y++)
   {
    // format the output
    Console.Write("{0,8:#.000000}", mat[x, y]);

    if ((y+1 % 2) < 3)
     Console.Write(", ");
   }
   Console.WriteLine(" ]");
  }
  Console.WriteLine();
 }
}
Similar to the skeleton example of the dot product operator, the Matrix class in Listing 18-1 contains an operator overload for the + operator. For your convenience, I've extracted the pertinent overload implementation in the code below:
 // let user add matrices
 public static Matrix operator +(Matrix mat1, Matrix mat2)
 {
  Matrix newMatrix = new Matrix();

  for (int x=0; x < DimSize; x++)
   for (int y=0; y < DimSize; y++)
    newMatrix[x, y] = mat1[x, y] + mat2[x, y];

  return newMatrix;
 }
The operator is static, which is the only way it can and should be declared because an operator belongs to the type and not a particular instance. There are just a few rules you have to follow when implementing operator overloads. What designates this as an operator is the use of the keyword operator, followed by the + symbol. The parameter types are both of the enclosing type, Matrix. The implementation of the operator overload creates a new instance of the return type and performs a matrix add.

Operator Rules

C# enforces certain rules when you overload operators. One rule is that you must implement the operator overload in the type that will use it. This is sensible because it makes the type self-contained.
Another rule is that you must implement matching operators. For example, if you overload ==, you must also implement !=. The same goes for <= and >=.
When you implement an operator, its compound operator works also. For example, since the + operator for the Matrix type was implemented, you can also use the += operator on Matrix types.

Summary

Operator overloading allows you to implement types that behave like the built-in types when using operators. Be sure to use operators in a way that is natural and understandable for the type. Syntax for implementing operators is much like a static method, but includes the operator keyword and the operator symbol in place of an identifier. Additionally, there are rules, such as maintaining symmetry,for using operators, which encourage construction of robust types.

No comments:

Post a Comment