![]() |
Languages »
C# »
General
Beginner
Simulating polymorphic operator overloads with C#By James T. Johnson, Nishant SivakumarArticle shows how to overcome the C# insistence on operator overloads being static and describes a method for simulating polymorphic behavior for operator overloads. |
C#, Windows, .NET, Visual Studio, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
Occasionally this question pops up in newsgroups and forums : Why does C# insist on operator overloads being static? The person raising the question also usually complains how this prevents him or her from implementing virtual overloaded operators.
This article explains why operator overloads have to be static in C# (or any other MSIL compiler), and also shows how you can simulate virtual overloaded operators very easily. It's not a universe-shattering theory (nor a very original one for that matter) and uses a very simple pattern though it's this very simplicity that makes it interesting.
In C#, GC'd objects are heap-allocated (the managed CLR heap, not the CRT
heap) and thus GC'd objects are always used as references (into the CLR heap).
You cannot have stack-based GC'd objects in C# and what this means that you
never know when an object variable is null. So, you can
imagine what happens if operator overloads were instance methods and you tried
to use an operator on a null object! Traditional C++ (as
opposed to the CLI version) never faced this problem because the operators were
always applied on stack objects; and if at all pointers were used, since
pointers followed their own set of operational behavior - you never face a
situation where an overloaded op-overload method is invoked on an invalid
object.
See below some C++ code that uses virtual operator overloads :-
class Base { public: Base(int x):x_value(x){} virtual bool operator ==(const Base& b) { return x_value == b.x_value; } protected: int x_value; }; class Derived : public Base { public: Derived(int x, int y): Base(x), y_value(y){} virtual bool operator ==(const Base& b) { Derived* pD = (Derived*)&b; return (pD->y_value == y_value) && (pD->x_value == x_value); } private: int y_value; }; int _tmain(int argc, _TCHAR* argv[]) { Base* b1 = new Derived(2,11); Base* b2 = new Derived(2,11); cout << (*b1==*b2) << endl; return 0; }
When (*b1==*b2) is evaluated the == operator
overload for class Derived is invoked which can be easily verified
by trying out different values for the Base constructors for
b1 and b2 or by setting a breakpoint inside the
operator overload.
Taking what we know of C# let's attempt a straight-forward port of the above example to C#.
public class Base
{
int x;
public Base( int x )
{
this.x = x;
}
public static bool operator==( Base l, Base r )
{
if( object.ReferenceEquals( l, r ) )
return true;
else if( object.ReferenceEquals( l, null ) ||
object.ReferenceEquals( r, null ) )
return false;
return l.x == r.x;
}
public static bool operator!=( Base l, Base r )
{
return !(l == r);
}
public int X { get { return x; } }
}
public class Derived : Base
{
int y;
public Derived( int x, int y ) : base( x )
{
this.y = y;
}
public static bool operator==( Derived l, Derived r )
{
if( object.ReferenceEquals( l, r ) )
return true;
else if( object.ReferenceEquals( l, null ) ||
object.ReferenceEquals( r, null ) )
return false;
return (l.y == r.y) && (l.X == r.X);
}
public static bool operator!=( Derived l, Derived r )
{
return !(l == r);
}
public int Y { get { return y; } }
}
class Program
{
static void Main()
{
Derived d1 = new Derived( 2, 11 );
Derived d2 = new Derived( 2, 11 );
Console.WriteLine( d1 == d2 );
Console.ReadLine();
}
}
If we run the program as it is above everything will work like the C++ version, but if we introduce a slight change to the program things begin to deviate greatly.
class Program
{
static void Main()
{
Base d1 = new Derived( 2, 11 );
Base d2 = new Derived( 2, 12 );
Console.WriteLine( d1 == d2 );
Console.ReadLine();
}
}
What's going on here? As simple debugging will show us the despite the
objects being compared being instances of the Derived class
the Base class == operator is being
called. This is because C# (and thus, most other languages) figure out which
== operator method to call based on the known (i.e.
compile-time) type of the object on the left hand side of the operation.
There are ways around this as you'll see below.
Here, we see how to simulate this in C# :-
class Base
{
protected int x_value = 0;
public Base(int x)
{
x_value = x;
}
public static bool operator==(Base b1, Base b2)
{
if( object.ReferenceEquals( b1, b2 ) )
{
return true;
}
else if( object.ReferenceEquals( b1, null ) ||
object.ReferenceEquals( b2, null ) )
{
return false;
}
return b1.Equals(b2);
}
public static bool operator !=(Base b1, Base b2)
{
return !(b1 == b2);
}
public override bool Equals(object obj)
{
if( obj == null )
return false;
Base o = obj as Base;
if( o != null )
return x_value == o.x_value;
return false;
}
public override int GetHashCode()
{
return x_value.GetHashCode();
}
}
class Derived : Base
{
protected int y_value = 0;
public Derived(int x, int y) : base(x)
{
y_value = y;
}
public override bool Equals(object obj)
{
if( !base.Equals( obj ) )
return false;
Derived o = obj as Derived;
if( o == null )
return false;
return y_value == o.y_value;
}
public override int GetHashCode()
{
return x_value.GetHashCode() ^ y_value.GetHashCode() + x_value;
}
}
class Program
{
static void Main(string[] args)
{
Base b1 = new Derived(10, 12);
Base b2 = new Derived(10, 11);
Console.WriteLine(b1 == b2);
b2 = null;
Console.WriteLine(b1 == b2);
Console.ReadKey(true);
}
}
Rather than rely on the == operator overload to do all
of the heavy lifting we push all of the work onto the virtual Equals method, from there we let polymorphism work its magic.
The operator overload has to be static, so we have virtual instance methods that implement the logic for us and we invoke these virtual methods from the static operators
In our example, the method Equals corresponds to == and Equals is a virtual method (inherited
from System.Object)
Within the static overload we need to check for null (to
avoid null-reference exceptions)
Within each derived class's corresponding operator-logic method (Equals in our case), we cast the System.Object
argument to the type of the class (e.g. - In the Derived class we cast to Derived)
While Equals and == already exist
in System.Object, we can implement similar methods for any
operator in our class hierarchies. Say, we need to implement the ++ operator, we then add a PlusPlus (or
Increment) virtual method to the root
base class in our object hierarchy and in the ++ overload
we invoke the Increment method on the passed-in object.
In our example, we check for null and return true or false depending on whether the
objects are both null or not. But, if you are implementing
an operator like ++ thenyou might want to
check for null and throw an ArgumentNullException (to override the inappropriate NullReferenceException that'd otherwise get
thrown).
Derived.Equals and also changed
the GetHashCode implementations for Base and
Derived (thanks to Jeffrey Sax for pointing this out)
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 19 Apr 2005 Editor: Nishant Sivakumar |
Copyright 2005 by James T. Johnson, Nishant Sivakumar Everything else Copyright © CodeProject, 1999-2009 Web13 | Advertise on the Code Project |