C Sharp

Relational Operators

Most operators return a numeric result. Relational operators, however, are a bit different in that they generate a Boolean result. Instead of performing some mathematical operation on a set of operands, a relational operator compares the relationship between the operands and returns a value of true if the relationship is true and false if the relationship is untrue.

Comparison Operators

The set of relational operators referred to as comparison operators are less than (<), less than or equal to (<=), greater than (>), greater than or equal to (>=), equal to (==), and not equal to (!=). The meaning of each of these operators is obvious when working with numbers, but how each operator works on objects isn't so obvious. Here's an example: -

using System;
class NumericTest
{
    public NumericTest(int i)
    {
        this.i = i;
    }
    protected int i;
}
class RelationalOps1App
{
    public static void Main()
    {
        NumericTest test1 = new NumericTest(42);
        NumericTest test2 = new NumericTest(42);
        Console.WriteLine("{0}", test1 == test2);
    }
}

If you're a Java programmer, you know what's going to happen here. However, most C++ developers will probably be surprised to see that this example prints a statement of false. Remember that when you instantiate an object, you get a reference to a heap-allocated object. Therefore, when you use a relational operator to compare two objects, the C# compiler doesn't compare the contents of the objects. Instead, it compares the addresses of these two objects. Once again, to fully understand what's going on here, we'll look at the MSIL for this code: -

.method public hidebysig static void Main() il managed
{
  .entrypoint
  // Code size       39 (0x27)
  .maxstack  3
  .locals (class NumericTest V_0,
           class NumericTest V_1,
           bool V_2)
  IL_0000:  ldc.i4.s   42
  IL_0002:  newobj     instance void NumericTest::.ctor(int32)
  IL_0007:  stloc.0
  IL_0008:  ldc.i4.s   42
  IL_000a:  newobj     instance void NumericTest::.ctor(int32)
  IL_000f:  stloc.1
  IL_0010:  ldstr      "{0}"
  IL_0015:  ldloc.0
  IL_0016:  ldloc.1
  IL_0017:  ceq
  IL_0019:  stloc.2
  IL_001a:  ldloca.s   V_2
  IL_001c:  box        ['mscorlib']System.Boolean
  IL_0021:  call   void ['mscorlib']System.Console::WriteLine
                                      (class System.String,class System.Object)
  IL_0026:  ret
} // end of method 'RelationalOps1App::Main'

Take a look at the .locals line. The compiler is declaring that this Main method will have three local variables. The first two are NumericTest objects, and the third is a Booleantype. Now skip down to lines IL_0002 and IL_0007. It's here that the MSIL instantiates the test1 object and, with the stloc opcode, stores the returned reference to the first local variable. However, the key point here is that the MSIL is storing the address of the newly created object. Then, in lines IL_000a and IL_000f, you can see the MSIL opcodes to create the test2 object and store the returned reference in the second local variable. Finally, lines IL_0015 and IL_0016 simply load the local variables on the stack via a call to ldloc, and then line IL_0017 calls the ceq opcode, which compares the top two values on the stack (that is, the references to the test1 and test2 objects). The returned value is then stored in the third local variable and later printed via the call to System.Console.WriteLine.

How can one produce a member-by-member comparison of two objects? The answer is in the implicit base class of all .NET Framework objects. The System.Object class has a method called Equals designed for just this purpose. For example, the following code performs a comparison of the object contents as you would expect and returns a value of true: -

using System;
class RelationalOps2App
{
    public static void Main()
    {
        Decimal test1 = new Decimal(42);
        Decimal test2 = new Decimal(42);
        Console.WriteLine("{0}", test1.Equals(test2));
    }
}

Note that the RelationalOps1App example used a self-made class (NumericTest), and the second example used a .NET class (Decimal). The reason for this is that the System.Object.Equals method must be overridden to do the actual member-by-member comparison. Therefore, using the Equals method on the NumericTest class wouldn't work because we haven't overridden the method. However, because the Decimal class does override the inherited Equals method, it does work like you'd expect.

Another way to handle an object comparison is through operator overloading. Overloading an operator defines the operations that take place between objects of a specific type. For example, with string objects, the + operator concatenates the strings rather than performing an add operation. We'll be getting into operator overloading in Chapter 13.