HomeC# Behind the Scenes Look at C#: Operators, c...
Behind the Scenes Look at C#: Operators, continued
We have discussed the various operators that C# defines. In this article we will take a closer look at the MSIL generated code. You will realize that the CLR has no knowledge of C# operators; it's just a special type of method with special names. After that we will discuss operator overloading.
So let's begin. In the next section I will show you the generated MSIL code, where we use the + operator to add two integers and two decimals in separate examples.
Operators Again, MSIL viewpoint
Looking at the MSIL generated code from an application that performs operations by using operators, you will wonder why the code is not the same between the built-in data types. Let's look at a simple example of an application that add two integers.
using System; namespace Operators { class Class1 { static void Main(string[] args) { int x = 4; int y = 5; Console.WriteLine(x + y); } } }
Compile the application, then use the Ildasm.exe tool to load the MSIL code and navigate to the Main method code.
Where is the + operator? As we said before, the MSIL doesn't know anything about your C# operators; this example just clarifies that point. The + operator is expressed in the MSIL code as the add instruction (shown in the above code IL_0006: add). the add instruction takes two arguments from the stack and puts the result on the stack. it works with int, float, char, byte and the unsigned versions, but it doesn't work with the decimal data type. (Just as a side note, char is an integer value that represents the character, so the MSIL code knows only the integer value 97 of the character a, and so on).
Let's think about this for a minute. By using the add instruction, you can add two arguments and return the result without any process to the value or special manipulation -- but a data type such as decimal (or any user defined one like Customer, Order or Employee) needs a little more work. For example, the decimal data type needs some work to the precision operation, so the MSIL can't use the add instruction with these types.
Before we discuss the decimal data type with the + operator, I want to briefly discuss the MSIL above. The instruction ldc.i4.4 loads the constant value 4 on the stack. The instruction stloc.0 pops the value from the stack and stores it in the first local variable. The instruction ldc.i4.5 loads the constant value 5 on the stack, then the instruction stloc.1 pops the value from the stack and stores it in the second local variable. Both of the following instructions, ldloc.0 and ldloc.1, load the values of the local variables, number 0 and 1, on the stack, then the add instruction does its job and produces the sum.
Let's modify the application to work with the decimal data type.
using System; namespace Operators { class Class1 { static void Main(string[] args) { decimal x = 4; decimal y = 5; Console.WriteLine(x + y); } } }
This time the code is different and more complex. Do you see the add instruction? No, because as we said before, the decimal data type is complex and it can't use the add instruction. At the same time, the MSIL doesn't use operators, so the decimal data type defines a special type of method (op_Addition()) that performs the addition operation and returns the result. Inside this method is the code that will be called each time you write code that adds two decimal variables.
So with integers, the add instruction can do the job because it's just numbers, but with user defined types the MSIL needs a method to call to perform the addition operation. Note that the System.Int32 and System.Decimal both are built-in types. The add instruction can be used with the System.Int32 because it's just a simple number, but it can't be used with the System.Decimal because it doesn't know what to do; the developer of the type needs to write a method that will be called instead of the add instruction. The same issue exists with user defined types. We will look at an example soon and explain how it works.
Why do we need to overload operators? A student asked me this question two years ago. The expression "operator overloading" explains itself. You overload the operator with a new type to simulate the built-in types behavior. For example, the + operator has been overloaded to work with built-in types, so you can write arithmetic expressions like result = x + y (where result, x and y are of the decimal data type). With operator overloading, C# gave us the ability to write expressions like Course1 = Course1 + student1 instead of Course1.AddStudent(student1) to simulate the behaviors of built-in types and to express a level of abstraction to your types.
You can overload many of the operators except for the new operator, because it doesn't makes any sense to overload it; it's used to instantiate an object. The typeof operator can't be overloaded either, because it has one function (type information). The cast operator () and the array index operator [] can't be overloaded. The Assignment operator = and the Short Circuit OR ||, And && operators can't be overloaded.
In C# there's no construction that defines new operators, so we use operator overloading to overload the operators to work with our types. The rules of operator precedence apply to both built-in types and user-defined types. Let's take a look at an example of how to overload the + operator. Copy the following code and paste it into a file with the extension .cs, then compile the file.
using System; namespace Operators { class Class1 { static void Main(string[] args) { Number i = new Number(); i.aNumber = 90; i.aDecimal = .5m; i = i + 9; Console.WriteLine(i.aNumber); Console.WriteLine(i); Console.ReadLine(); } }
public class Number { public int aNumber; public decimal aDecimal; public static Number operator+(Number num, int x) { num.aNumber = num.aNumber + x; return num; }
public override string ToString() { return Convert.ToString(this.aNumber + this.aDecimal); }
} }
Run the application and you will get the following result to the console window:
I know that you have a lot of questions, such as how did we add an object of type Number to an int variable? And what does this static method with the operator+ keyword do?
First let's discuss the class Number. This class contains two public fields, one int to store an integer value and the other of type decimal to store a decimal value. It also contains two methods. The method ToString() just prints the string representation of the object and, in our class Number, it prints the value of the int field concatenated with the value of the decimal field. The magic of operator overloading comes with the static method operator+. Let's take a look at this method:
public static Number operator+(Number num, int x) { num.aNumber = num.aNumber + x; return num; }
This method overloads the + operator to work with the Number type. You should note that the logic of the operations that this operator performs is encapsulated inside the method. This method is public, so you can use the overloaded version of the addition operator on the Number class instances. If you think about, it you will find that this is a public method, so it can be called from client code. The method is also static, because it's a general method that can be used by all the instances of the class Number. So it's public and static, and so far it's much more like any method you code in C#.
Next is the return data type, which is of type Number, which makes sense (just a minute and we will talk about that). After the return data type comes the keyword operator, followed by the operator itself, which in our example is the addition operator +. Note the interesting parameter list. The first parameter is a Number type, and the second is of type int. This means that when you use (call) the + operator with the Number type, you need to supply an instance of type Number and an int instance. In our example you can't use the addition operator with two instances of the Number class, as with the following code:
static void Main(string[] args) { Number i = new Number(); i.aNumber = 90; i.aDecimal = .5m; Number o = new Number(); i.aNumber = 80; i.aDecimal = .7m; i = i + o; Console.WriteLine(i.aNumber); Console.WriteLine(i); Console.ReadLine(); }
This code will not compile, and it will generate the following error: "Operator '+' cannot be applied to operands of type 'Operators.Number' and 'Operators.Number'" because the definition of the operator overload method accepts the first paramter of type Number and the second of type int. We will modify the method to accept two parameters of type Number later. At any rate, the code inside the method is very simple, we just add the value of the current Number instance to the x and return this new instance.
In the Main method we simply create an object of type Number and set both the int and the decimal fields, then we add the value 9 to the i object and print the value of i.aNumber, which is now 99 after the addition operator usage. The method ToString() is the default method on objects, so it can be called implicitly by specifying the name of the object. In our example "Console.WriteLine(i);", this method returns a string that represents the number (we just concatenate both the int and the decimal fields to prove that there's no change to the decimal field). I imagine you want to investigate the MSIL code to see what's going on behind that. Load the application with the Ildasm.exe tool and navigate to the Number class.
The C# compiler has generated a method called op_Addition in the class Number to do the job of addition, so as we said before the runtime doesn't know about operators because the MSIL code contains a method to do the job. Now navigate to the Main method.
The highlighted line is a call to the method op_Addition of the class Number. Note the parameters here. It's very interested because you can learn more than you can imagine from the MSIL code. I can't discuss all of the MSIL instructions we encounter because it will take a lot of pages, so I may write another series about MSIL code soon. Let's take a look at the op_Addition MSIL code.
The first thing that you have to note here is the add instruction. Think about it for a minute please. We need to add two integers (the num.aNumber and the argument x) and, as we already know, the MSIL add instruction adds two integers, so in the method we have used the + operator on two integers. These are steps for doing the overloading, and at the end of these steps you have to find the MSIL arithmetic instructions.
Let's go further with this example. Can we modify the above example to return an int instead of an object of type int?
I will modify the example to return an int value (not an object of type Number) to prove that we have control over the return type.
using System; namespace Operators { class Class1 { static void Main(string[] args) { Number i = new Number(); i.aNumber = 90; i.aDecimal = .5m; int result = i + 9; Console.WriteLine("the value of i.aNumber = {0}", i.aNumber); Console.WriteLine("i.ToString() returns {0}", i.ToString()); Console.WriteLine("result = {0}", result); Console.ReadLine(); } }
public class Number { public int aNumber; public decimal aDecimal;
public static int operator+(Number num, int x) { return num.aNumber + x; }
public override string ToString() { return Convert.ToString(this.aNumber + this.aDecimal); }
} }
Compile and run the program and you will get the following result to the console window.
Look at the Main method and you will find that we are able to return a value of type int from the expression "int result = i + 9". This happened because we have modified the operator overload method to return an int value through two steps. First we have replaced the return data type Number by an int, then inside the method we have added the num.aNumber to x, and this is the value that will be returned.
With the same example, replace the Main method with the following updated code and try to compile the application.
static void Main(string[] args) { Number i = new Number(); i.aNumber = 90; i.aDecimal = .5m; int result = 9 + i; Console.WriteLine("the value of i.aNumber = {0}", i.aNumber); Console.WriteLine("i.ToString() returns {0}", i.ToString()); Console.WriteLine("result = {0}", result); Console.ReadLine(); }
This code will not compile, and it will generate the following error "Operator '+' cannot be applied to operands of type 'int' and 'Operators.Number.'" What happened? We have written an expression that consists of a constant field of type int, then the + operator, followed by an instance of type Number, so what's wrong?
Actually the operator overload method we wrote has a signature that accepts the Number instance first, then the int variable. As you know, the parameters' order is very important, so the compiler has generated an error. To solve this problem we need to write another method that accepts an int parameter followed by the Number type parameter.
public static int operator+(int x, Number num) { return num.aNumber + x; }
and this is the complete code example
using System; namespace Operators { class Class1 { static void Main(string[] args) { Number i = new Number(); i.aNumber = 90; i.aDecimal = .5m; int result = 9 + i; Console.WriteLine("the value of i.aNumber = {0}", i.aNumber); Console.WriteLine("i.ToString() returns {0}", i.ToString()); Console.WriteLine("result = {0}", result); Console.ReadLine(); } }
public class Number { public int aNumber; public decimal aDecimal;
public static int operator+(Number num, int x) { return num.aNumber + x; }
public static int operator+(int x, Number num) { return num.aNumber + x; }
public override string ToString() { return Convert.ToString(this.aNumber + this.aDecimal); }
In our example it makes sense to use the addition operator + with two instances of the Number class, so we need to overload the + operator to do that.
using System; namespace Operators { class Class1 { static void Main(string[] args) { Number i = new Number(); i.aNumber = 90; i.aDecimal = .5m;
Number o = new Number(); o.aNumber = 10; o.aDecimal = .3m;
Number SumNumber = i + o; Console.WriteLine("The value of i + o = {0}", SumNumber.aNumber); Console.ReadLine(); } }
public class Number { public int aNumber; public decimal aDecimal;
public static Number operator+(Number num, Number num2) { Number temp = new Number(); temp.aNumber = num.aNumber + num2.aNumber; return temp; }
public override string ToString() { return Convert.ToString(this.aNumber + this.aDecimal); }
} }
The result will be:
The ability to express basic operations on user-defined types is very unlimited and complex. Operator overloading is one feature that C# has over VB.NET. Usually when you overload an operator you will overload its family. In other words, when you overload the addition operator + you will need to overload all of the rest of the arithmetic operators (*, /, - and %). The compiler will not issue a warning if you overload only one of these operators, but it does make sense to overload all of them.
Why can you add a Number instance to another one, but you can't subtract them? If you have overloaded the > operator, it makes sense to overload the < operator, and it will be perfect if you overload the >= and the <= operators. The C# compiler will not let you overload only one operator without overloading the other one (this is different from the arithmetic operators). Let's complete our example by overloading the whole arithmetic operators family.
using System; namespace Operators { class Class1 { static void Main(string[] args) { Number i = new Number(); i.aNumber = 90; i.aDecimal = .5m;
Number o = new Number(); o.aNumber = 10; o.aDecimal = .3m;
Number SumNumber = i + o; Console.WriteLine("The value of i + o = {0}", SumNumber.aNumber); SumNumber = i - o; Console.WriteLine("The value of i - o = {0}", SumNumber.aNumber); SumNumber = i * o; Console.WriteLine("The value of i * o = {0}", SumNumber.aNumber); SumNumber = i / o; Console.WriteLine("The value of i / o = {0}", SumNumber.aNumber); SumNumber = i % o; Console.WriteLine("The value of i % o = {0}", SumNumber.aNumber); Console.ReadLine(); } }
public class Number { public int aNumber; public decimal aDecimal;
public static Number operator+(Number num, Number num2) { Number temp = new Number(); temp.aNumber = num.aNumber + num2.aNumber; return temp; }
public static Number operator-(Number num, Number num2) { Number temp = new Number(); temp.aNumber = num.aNumber - num2.aNumber; return temp; }
public static Number operator*(Number num, Number num2) { Number temp = new Number(); temp.aNumber = num.aNumber * num2.aNumber; return temp; }
public static Number operator/(Number num, Number num2) { Number temp = new Number(); temp.aNumber = num.aNumber / num2.aNumber; return temp; }
public static Number operator%(Number num, Number num2) { Number temp = new Number(); temp.aNumber = num.aNumber % num2.aNumber; return temp; }
public override string ToString() { return Convert.ToString(this.aNumber + this.aDecimal); }
} }
Compile and run the code to get the following result to the console window: