Search
Close this search box.

C#/.NET Little Pitfalls: Operators are Overloaded, not Overridden

C# is a wonderful language for modern programming.  While everything in C# has a reason and a place, occasionally, there are things that can be confusing for a developer who isn’t aware of what is happening behind the scenes. This is my third post in the Little Pitfalls series where I explore these small pitfalls; the previous Little Pitfall post can be found here.

This week we’re going to look at operator overloading.  Yes, I bolded that because it is very important to note we overload operators, we don’t override them.  Yet, many times I’ve seen or heard people talk about operator overriding by mistake.

So, is this just a semantics argument and I should just relax?  No, not really, because the two terms imply entirely different things, and if you are expecting an override behavior when you are instead getting an overload behavior, you can fall headlong into this pitfall.

Overloading in a nutshell

When you overload a method, you are taking a more “horizontal” approach on increasing the functionality of a type.  We typically do this by providing a parallel definition of the method with a different set of parameters (note that you cannot define an overload based on differences in return type alone).

For example, let’s say we define the following class for managing fractions (yes, a real class like this would have many more members, but keeping it simple).

1 : public class Fraction
2 : {
  3 : public int Numerator { get; set; }
  4 : public int Denominator { get; set; }
  5 : 6 :  // set the fraction to a given value
           7 : public void Set(int numerator, int denominator) 8 : {
    9 : Numerator = numerator;
    10 : Denominator = denominator;
    11:
  }
  12:
}

Now, perhaps we want to be able to set the value of the fraction in other ways as well.  For example, maybe we want to set a fraction from a whole number (e.g. 7 = 7/1) or a mixed number (e.g. 3 1/2 = 7/2).

If we want to provide a series of alternative definitions for a member that the type (Fraction in our case) provides, we are “expanding” the options available by providing additional options.  This is in effect increasing the functionality “horizontally” by giving us more members to choose from:

1 : public class Fraction
2 : {
  3 :  // ... the other stuff ...
       4 : 5
      :  // initialize fraction with just a whole number.  For example 7 = 7/1
         6 : public void Set(int wholeNumber) 7 : {
    8 : Numerator = wholeNumber;
    9 : Denominator = 1;
    10:
  }
  11 : 12 :  // set the fraction from a mixed number.  For example 3 1/2 = 7/2
             13 : public void Set(int whole, int numerator, int denominator) 14
      : {
    15 : Numerator = whole * denominator + numerator;
    16 : Denominator = denominator;
    17:
  }
  18:
}

Now instead of one Set() method exposed, the class has three Set() methods exposed, each with a different parameter signature.  This is how overloading works, you are adding additional methods (or constructors) with different parameter signatures from the class (or a base class).

To that last point, you can overload across a hierarchy, but this is still overloading, not overriding.  You are simply overloading the inherited member from the base class in your derived class.

1 : public class Base
2 : {
  3 : public void DoSomething(int x) {}
  4:
}
5 : 6 : public class Derived : Base 7 : {
  8 :  // this does not replace Base.DoSomething(int x), it lives beside it in
       // Derived
       9 : public void DoSomething(int x, bool y) {}
  10:
}

So in the example above, Base has one DoSomething() method that takes an int x.  And Derived has two DoSomething() methods available, the one from Base that takes the int x and the overload that takes int x, bool y.

The thing to note here is that overloads do not hide or override other method definitions in the class hierarchy, they simply are added beside them in the class in which they are defined.

This is important because given the type of the reference you are using to access the type, you may see different methods available.  This is because non-virtual members are resolved at compile-time, not run-time.

1 : Derived asDerived = new Derived();
2 : 3 :  // valid since Derived inherits from Base
         4 : Base asBase = asDerived;
5 : 6 :  // valid because Derived inherits Base's methods
         7 : asDerived.DoSomething(13);
8 : 9 :  // valid, this is the new overload for derived
         10 : asDerived.DoSomething(7, true);
11 : 12 :  // valid, this is the original method in base
           13 : asBase.DoSomething(7);
14 : 15 :  // Syntax Error: even though asBase refers to Derived, Derived's
           // overloads
           16 :  // are not available to a Base reference at compile time.
                 17 : asBase.DoSomething(7, true);

Hiding is another Little Pitfall for another day, for now we’ll just concentrate on overloading and overriding

Overriding in a nutshell

So what, then, is overriding?  Overriding is when you are “replacing” a base class method definition with a derived class method definition.  This is a much more “vertical” approach because you’re changing functionality going down the inheritence chain.  Note that we’re not not completely “replacing” the original definition – the base class definition still exists and can be invoked from the overriding member, if desired.  The main point being it is no longer available to be called directly from outside the type, and as such has been “replaced” in favor of the new definition.

To override a member you must mark the definition in the base class as abstract (if no definition exists and concrete sub-classes must override it) or virtual (if a base definition exists and sub-classes may optionally override it).

1 : public class Base
2 : {
  3 :  // base method must be virtual (has a body) or abstract (has no body)
       4 : public virtual void DoSomething(int x) 5 : {
    6 : Console.WriteLine("Base: " + x);
    7:
  }
  8:
}
9 : 10 : public class Derived : Base 11 : {
  12 :  // derived method must use override keyword (or it hides, which is
        // another pitfall for another day)
        13 : public override void DoSomething(int x) 14 : {
    15 : Console.WriteLine("Derived: " + x);
    16:
  }
  17:
}

So in this example, Derived.DoSomething() replaces (in a sense) Base.DoSomething().  Thus no matter if you call a DoSomething() method from a Base reference or a Derived reference, you will still get the overridden behavior of the Derived class.  This magic happens because virtual methods are resolved at run-time, not compile-time.  The CLR will examine the actual type behind the reference (not the type of the reference itself) and get the nearest (up the class hierarchy) override available for that member of the type.

1 : Derived asDerived = new Derived();
2 : Base asBase = asDerived;
3 : 4 :  // output is Derived: 13
         5 : asDerived.DoSomething(13);
6 : 7 :  // output is also Derived: 13 since overridden methods are checked at
         // run-time
         8 :  // for the actual type of the object they are invoked upon.
              9 : asBase.DoSomething(13);

As a side note, constructors are not truly overridden in a pure sense.  That is, whenever you create a new derived class it does not actually inherit the constructors from the base class, per se.  If you want to have the same constructor signatures that the base class has in your derived class you must re-implement them in the derived class and call through to the base class constructors (otherwise assumes base class default constructor).

How does this apply to operators?

Let’s now look at operators, but first let’s summarize the key differences between overload and override behaviors:

  • Overloads
    • Supplies a separate, parallel definition to an existing member.
    • Can overload methods, constructors, and operators.
    • Resolved at compile-time.
  • Overrides
    • “Replaces” a definition of an existing member for a derived type.
    • Can override instance methods and properties (cannot override static members).
    • Resolved at run-time.

Now here’s where it gets tricky.  People think when they add an operator definition to their class, they are overriding that operator’s behavior, but this is incorrect.  Let’s look at an example to allow us to add to Fraction instances using the + operator:

1 : public class Fraction
2 : {
  3 :           // ... Other stuff ...
       4 : 5 :  // all operator overloads must be public and static
                6 : public static Fraction
                    operator +(Fraction first, Fraction second) 7 : {
    8 : if (first == null) throw new ArgumentNullException("first");
    9 : if (second == null) throw new ArgumentNullException("second");
    10 : 11 :  // doing very simple fraction addition, not bothering with
               // reducing or LCD
               12 : return new Fraction 13 : {
                 14 : Numerator = first.Numerator * second.Denominator +
                                  15 : second.Numerator * first.Denominator,
                 16 : Denominator = first.Denominator * second.Denominator 17 :
               };
    18:
  }
  19:
}

Notice a few things here.  The operator is static (must public and static or C# gives you a compiler error) and is not marked as an override (can’t be or C# will give you an error again) nor is there a definition of operator + in the base class (object).  These three points alone should signify to the reader that the defining an operator is an overload and not an override.

This throws people off because they think of the + operator as being overridden for the class and that they are somehow overriding a base behavior of the operator from a base type.  Strictly speaking this is incorrect.  The operators do not belong to a base class at all (not even == and != if you look inside the definition of object).

I find it more meaningful to think of this as the operators being independent of all types, and that we are overloading the definition of the operator (as if it was a global method in the world of C++) to support our type.

For example, if all you see is the code below, you have no context to know where the correct operator + is defined:

 1: // what operator + are we invoking?  Depends on the args!
   2: z = x + y;

There is nothing in this code directly that tells us what operator + to invoke.  The only thing we know is that it is an operator + between x and y, so whatever types x and y are dictate what overload will get called.

Little Pitfall: Operator overloads are not overrides

So why is this really a problem?  You may still look at this and say that’s all semantics and who cares it works, right?  Well, the main place where this comes to be a problem is when you overload an operator, but expect it to override a base behavior.

Where do we typically see this?  Anytime where we are overloading an operator that already has meaning – either because it has an inherent definition for all types, or was otherwise already defined for a base type (like == and != for object).

So let’s illustrate.  Let’s say we want to be really full featured in our Fraction class and implement all the comparison operators.  Well, the first two we’d probably implement would be == and != (these two must always be overloaded in pairs):

1 : public class Fraction
2 : {
  3 :  // ... other stuff ...
       4 : 5 : public static bool
               operator ==(Fraction first, Fraction second) 6 : {
    7 :       // if both non-null, check numerator and denominator (ignore
              // reductions)
         8 :  // use ReferenceEquals() to get underlying object == so don't
              // recursively call self
              9 : if (!ReferenceEquals(first, null) &&
                      !ReferenceEquals(second, null)) 10 : {
      11 : return first.Numerator == second.Numerator && 12
          : first.Denominator == second.Denominator;
      13:
    }
    14 : 15 :  // otherwise, equal if both null, not equal otherwise
               16 : return ReferenceEquals(first, null) &&
                    ReferenceEquals(second, null);
    17:
  }
  18 : 19 : public static bool operator !=(Fraction first, Fraction second) 20
      : {
    21 :  // cross call to == and negate
          22 : return !(first == second);
    23:
  }
  24:
}

So that overload seems logical, right? We can now do this:

1 : public static void Main() 2 : {
  3 : Fraction oneHalf = new Fraction { Numerator = 1, Denominator = 2 };
  4 : Fraction anotherOneHalf = new Fraction { Numerator = 1, Denominator = 2 };
  5 : 6
      :  // invoke the equals operator from Fraction, gives us True as expected
         7 : Console.WriteLine(oneHalf == anotherOneHalf);
  8:
}

Which gives as a true result as we’d expect!

So what if I made the simplest of changes in this example, and instead of having Fraction references I have object references?

1 : public static class Program
2 : {
  3 : public static void Main() 4 : {
    5 : object oneHalf = new Fraction { Numerator = 1, Denominator = 2 };
    6 : object anotherOneHalf = new Fraction { Numerator = 1, Denominator = 2 };
    7 : 8 :  // What does this do?  Uh oh... This yields False
             9 : Console.WriteLine(oneHalf == anotherOneHalf);
    10:
  }
  11:
}

Perfectly syntactically legal because == works between two type object references, and Fraction inherits from object (as all types do) so we can assign a Fraction to object.  But now we yield false instead!  Why?

Because, the == operator is an overload, not an override.  We have not replaced the behavior of == dynamically at run-time, but are binding to the definition of == at compile-time.  And the two types it is appearing between are object which means we’d get a reference equality check.  And since oneHalf and anotherOneHalf are obviously referring to separate (though “identical”) objects, the comparison yields false!

This would also fail if one of the types was object and one was Fraction, because when it looks for a working overload it looks for the nearest common ancestor for both classes.  For Fraction and object, that’s object itself.

This is why it’s operator overloading and not overriding.  With overriding, we’d expect it to dynamically look up the definition of == at run-time, but because it’s an overload, it doesn’t, it’s bound at compile-time as we mentioned before.

To protect yourself against this pitfall, make sure you aren’t treating an operator overload as an override, especially when dealing with == and != since these are defined already between all types.  In addition, when comparing objects consider using Equals() instead of == and != if you think the definition of equality may be overridden in a subclass, that way you will get the correct result since Equals() can be overridden.

What if you do want to “override” an operator?

Operators, like static methods, cannot be overridden directly.  But the work-around for both of these is the same.  Have the overloaded operator call a virtual instance method that does the operation for you and can be overridden in a sub-class.  This gives you the flexibility of both using the operator and redefining the behavior in a sub-class if desired in a polymorphic way:

1 : public class Fraction
2 : {
  3 :  // ... other stuff ...
       4 : public virtual Fraction Add(Fraction second) 5 : {
    6 : if (second == null) throw new ArgumentNullException("second");
    7 : 8 :  // doing very simple fraction addition, not bothering with reducing
             // or LCD
             9 : return new Fraction 10 : {
               11 : Numerator = Numerator * second.Denominator +
                                12 : second.Numerator * Denominator,
               13 : Denominator = Denominator * second.Denominator 14 :
             };
    15:
  }
  16 : 17 :  // REWORKED to use the overridable Add() method...
             18 : public static Fraction
                  operator +(Fraction first, Fraction second) 19 : {
    20 : if (first == null) throw new ArgumentNullException("first");
    21 : if (second == null) throw new ArgumentNullException("second");
    22 : 23 : return first.Add(second);
    24:
  }
  25:
}

Summary

So, we’ve seen that overloading and overriding are sometimes confused, especially when it comes to operator definitions.  The key thing to remember is that operators are overloaded not overridden, which means that if you use operators on base class references, you will get the base class definition (if one exists) and not any derived class operator overload definition that may exist.

This can be particularly painful if you are calling the == or != operator between two object references and are expecting it to call an override in the actual objects being held.  Stay tuned for more Little Pitfalls to follow…

This article is part of the GWB Archives. Original Author: James Michael Hare

Related Posts