Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An Introduction to Operator Overloading in C#

0.00/5 (No votes)
29 Sep 2008 2  
Conversion, binary, unary, and comparison operators for your types.

Introduction

Operator overloading is a powerful and underused (but often misused) feature that can help make your code much simpler and your objects more intuitive to use. Adding some simple operator overloads to your class or struct enables you to:

  1. allow conversion to and from your type and other types
  2. perform mathematical/logical operations on your type and itself or other types

Conversion Operators

There are several ways of converting between types, but the most common are using implicit/explicit conversion operators.

Implicit

Implicit means you can assign an instance of one type directly to another.

Let's create an example.

public struct MyIntStruct
{
    int m_IntValue;
    private MyIntStruct(Int32 intValue)
    {
        m_IntValue = intValue;
    }
    public static implicit operator MyIntStruct(Int32 intValue)
    {
        return new MyIntStruct(intValue);
    }
}

Once we have this in our struct, we can create a new instance of it from our code by simply assigning it a value - no casting or new declarations are required in our code.

MyIntStruct myIntStructInstance = 5;

To implicitly convert our struct back to an int, we need to add another conversion operator:

public static implicit operator Int32(MyIntStruct instance)
{
    return instance.m_IntValue;
}

Now, we can assign our myIntStructInstance directly to an int.

int i = myIntStructInstance;

We can also call any other function that will take an int and pass our instance directly to it. We can do this with as many types as we like.

Maybe, our struct also has a string member variable, in which case, it could be useful to be able to create an instance of it by directly assigning from a string. This struct may look like this...

public struct MyIntStringStruct
{
    int m_IntValue;
    string m_StringValue;
    private MyIntStringStruct(Int32 intValue)
    {
        m_IntValue = intValue;
        m_StringValue = string.Empty; // default value
    }
    private MyIntStringStruct(string stringValue)
    {
        m_IntValue = 0; // default value
        m_StringValue = stringValue;
    }
    public static implicit operator MyIntStringStruct(Int32 intValue)
    {
        return new MyIntStringStruct(intValue);
    }
    public static implicit operator MyIntStringStruct(string stringValue)
    {
        return new MyIntStringStruct(stringValue);
    }
    public static implicit operator Int32(MyIntStringStruct instance)
    {
        return instance.m_IntValue;
    }
}

... and we can create an instance by assigning an int value as before, or a string.

MyIntStringStruct myIntStringStructInstance = "Hello World";

Notice, I haven't created an implicit conversion operator from our struct to a string. Doing so can, in some situations, create ambiguity that the compiler cannot deal with. To see this, add the following:

public static implicit operator string(MyIntStringStruct instance)
{
    return instance.m_StringValue;
}

This compiles without issue. Now, try this:

MyIntStringStruct myIntStringStructInstance = "Hello World";
Console.WriteLine(myIntStringStructInstance); // compile fails here

The compiler will fail with "The call is ambiguous between the following methods or properties...". The console will happily take an int or a string (and many other types, of course), so how do we expect it to know which to use? The answer is to use an explicit conversion operator.

Explicit

Explicit means we have to explicitly perform the cast in our code. Change the keyword implicit to explicit in the last conversion operator so it looks like this.

public static explicit operator string(MyIntStringStruct instance)
{
    return instance.m_StringValue;
}

Now, we can return the string value, but only if we explicitly cast to string.

MyIntStringStruct myIntStringStructInstance = "Hello World";
Console.WriteLine((string)myIntStringStructInstance);

We get the output we expected.

Implicit vs. Explicit

Implicit is far more convenient than explicit as we don't have to explicitly cast in our code, but as we have seen above, there can be issues. There are other things to bear in mind, however, such as: if the objects being passed in either direction will be rounded or truncated in anyway, then you should use explicit conversion, so the person using your object isn't caught out when data is discarded.

Imagine a series of complex calculations being done with decimals, and we have allowed our struct to implicitly be converted to decimal for convenience, but the member variable where it is stored is an integer. The user would rightly expect any object that implicitly passes in or out a decimal to retain the precision, but our struct would lose all decimal places, and could cause the end result of their calculations to be significantly wrong!

If they have to explicitly cast to or from our struct, then we are effectively posting a warning sign. In this particular situation, it may be (is) better to inconvenience the user and have neither implicit nor explicit, so they have to cast to an int to use our struct, and then there is no misunderstanding.

There are others issues too - do some research before using implicit. If in doubt, use explicit, or don't implement the conversion operator for that type at all.

Binary operators

Binary operators (surprisingly!) take two arguments. The following operators can be overloaded: +, -, *, /, %, &, |, ^, <<, >>.

Note: It's important that you don't do anything unexpected when using operators, binary or otherwise.

These operators are normally pretty logical. Let's take the first, +. This normally adds two arguments together.

int a = 1;
int b = 2;
int c = a + b; // c = 3

The string class, however, uses the + operator for concatenation.

string x = "Hello";
string y = " World";
string z = x + y; // z = Hello World

So, depending on your situation, you may do whatever is logically required.

Imagine a struct that holds two ints, and you have two instances that you wish to add. The logical thing to do would be to add the corresponding ints in each instance. You may also want to add just one int to both values. The resulting struct would look something like this.

public struct MySize
{
    int m_Width;
    int m_Height;
    public MySize(int width, int height)
    {
        m_Width = width;
        m_Height = height;
    }
    public static MySize operator +(MySize mySizeA, MySize mySizeB)
    {
        return new MySize(
            mySizeA.m_Width + mySizeB.m_Width,
            mySizeA.m_Height + mySizeB.m_Height);
    }
    public static MySize operator +(MySize mySize, Int32 value)
    {
        return new MySize(
            mySize.m_Width + value,
            mySize.m_Height + value);
    }
}

It can then, obviously, be used like this:

MySize a = new MySize(1, 2);
MySize b = new MySize(3, 4);
MySize c = a + b; // Add MySize instances
MySize d = c + 5; // Add an int

You can do similar things for all the overloadable binary operators.

Unary operators

Unary operators take just one argument. The following operators can be overloaded: +, -, !, ~, ++, --, true, false. Most types have no need to implement all these operators, so only implement the ones you need!

A simple example using ++ on the struct used above:

public static MySize operator ++(MySize mySize)
{
    mySize.m_Width++;
    mySize.m_Height++;
    return mySize;
}

Note: The ++ and -- unary operators should (for standard operations) alter the integral values of your struct, and return the same instance, and not a new instance, with the new values.

Comparison operators

Comparison operators take two arguments. The following operators can be overloaded. [==, !=], [<, >], [<=, >=]. I have grouped these in square brackets as they must be implemented in pairs.

If using == and !=, you should also override Equals(object o) and GetHashCode().

public static bool operator ==(MySize mySizeA, MySize mySizeB)
{
    return (mySizeA.m_Width == mySizeB.m_Width && 
            mySizeA.m_Height == mySizeB.m_Height);
}
public static bool operator !=(MySize mySizeA, MySize mySizeB)
{
    return !(mySizeA == mySizeB);
}
public override bool Equals(object obj)
{
    if (obj is MySize)
        return this == (MySize)obj;
    return false;
}
public override int GetHashCode()
{
    return (m_Width * m_Height).GetHashCode();
    // unique hash for each area
}

The comparison operators normally return a bool, although they don't have to, but remember, don't surprise the end user!

Other operators

This just leaves the conditional operators, &&, ||, and the assignment operators, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=. These are not overloadable, but are evaluated using the binary operators. In other words, supply the binary operators, and you get these for free!

Conclusion

I hope this introduction to operator overloading has been useful and informative. If you haven't used them before, give them a try - they can be a useful tool in your box.

  • Don't overuse them. If an operator or conversion will definitely never be needed, or it's not obvious what the result will be, don't supply it.
  • Don't misuse them. Yes, you can make ++ decrement values, but you most definitely shouldn't!
  • Be very careful when implicitly converting your classes or structs to other types.
  • Remember, structs are value types, and classes are reference types. It can make a huge difference in how you handle things in your overload methods.

MSDN references

History

  • 31 August 2008: Initial version.
  • 6 September 2008: Added the ==, !=, Equals, and GetHashCode examples.
  • 2 October 2008: Added note about returning from the ++ and -- overloads.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here