Let's look at our employee example to see what I mean. The Poly1App application runs correctly because we have two objects: an Employee object and a SalariedEmployee object. In a more practical application, we'd probably read all the employee records from a database and populate an array. Although some of these employees would be contractors and some would be salaried employees, we'd need to place them all in our array as the same type-the base class type, Employee. However, when we iterate through this array, retrieving and calling each object's CalculatePay method, we'd want the compiler to call the correct object's implementation of the CalculatePay method.
In the following example, I've added a new class, ContractEmployee. The main application class now contains an array of type Employee and two additional methods-LoadEmployees loads the employee objects into the array, and DoPayroll iterates through the array, calling each object's CalculatePay method.
using System; class Employee { public Employee(string name) { this.Name = name; } protected string Name; public string name { get { return this.Name; } } public void CalculatePay() { Console.WriteLine("Employee.CalculatePay called for {0}", name); } } class ContractEmployee : Employee { public ContractEmployee(string name) : base(name) { } public new void CalculatePay() { Console.WriteLine("ContractEmployee.CalculatePay called for {0}", name); } } class SalariedEmployee : Employee { public SalariedEmployee (string name) : base(name) { } public new void CalculatePay() { Console.WriteLine("SalariedEmployee.CalculatePay called for {0}", name); } } class Poly2App { protected Employee[] employees; public void LoadEmployees() { // Simulating loading from a database. employees = new Employee[2]; employees[0] = new ContractEmployee("Kate Dresen"); employees[1] = new SalariedEmployee("Megan Sherman"); } public void DoPayroll() { foreach(Employee emp in employees) { emp.CalculatePay(); } } public static void Main() { Poly2App poly2 = new Poly2App(); poly2.LoadEmployees(); poly2.DoPayroll(); } }
However, running this application results in the following output: -
c:\>Poly2App Employee.CalculatePay called for Kate Dresen Employee.CalculatePay called for Megan Sherman
Obviously, this is not what we wanted-the base class's implementation of CalculatePay is being called for each object. What happened here is an example of a phenomenon called early binding. When the code was compiled, the C# compiler looked at the call to emp.CalculatePay and determined the address in memory that it would need to jump to when the call is made. In this case, that would be the memory location of the Employee.CalculatePay method.
Take a look at the following MSIL that was generated from the Poly2App application, and specifically take note of line IL_0014 and the fact that it explicitly calls the Employee.CalculatePay method: -
.method public hidebysig instance void DoPayroll() il managed { // Code size 34 (0x22) .maxstack 2 .locals (class Employee V_0, class Employee[] V_1, int32 V_2, int32 V_3) IL_0000: ldarg.0 IL_0001: ldfld class Employee[] Poly2App::employees IL_0006: stloc.1 IL_0007: ldloc.1 IL_0008: ldlen IL_0009: conv.i4 IL_000a: stloc.2 IL_000b: ldc.i4.0 IL_000c: stloc.3 IL_000d: br.s IL_001d IL_000f: ldloc.1 IL_0010: ldloc.3 IL_0011: ldelem.ref IL_0012: stloc.0 IL_0013: ldloc.0 IL_0014: call instance void Employee::CalculatePay() IL_0019: ldloc.3 IL_001a: ldc.i4.1 IL_001b: add IL_001c: stloc.3 IL_001d: ldloc.3 IL_001e: ldloc.2 IL_001f: blt.s IL_000f IL_0021: ret } // end of method Poly2App::DoPayroll
That call to the Employee.CalculatePay method is the problem. What we want instead is for late binding to occur. Late binding means that the compiler does not select the method to execute until run time. To force the compiler to call the correct version of an upcasted object's method, we use two new keywords: virtual and override. The virtual keyword must be used on the base class's method, and the override keyword is used on the derived class's implementation of the method. Here's the example again-this time functioning properly! -
using System; class Employee { public Employee(string name) { this.Name = name; } protected string Name; public string name { get { return this.Name; } } virtual public void CalculatePay() { Console.WriteLine("Employee.CalculatePay called for {0}", name); } } class ContractEmployee : Employee { public ContractEmployee(string name) : base(name) { } override public void CalculatePay() { Console.WriteLine("ContractEmployee.CalculatePay called for {0}", name); } } class SalariedEmployee : Employee { public SalariedEmployee (string name) : base(name) { } override public void CalculatePay() { Console.WriteLine("SalariedEmployee.CalculatePay called for {0}", name); } } class Poly3App { protected Employee[] employees; public void LoadEmployees() { // Simulating loading from a database. employees = new Employee[2]; employees[0] = new ContractEmployee("Kate Dresen"); employees[1] = new SalariedEmployee("Megan Sherman"); } public void DoPayroll() { foreach(Employee emp in employees) { emp.CalculatePay(); } } public static void Main() { Poly3App poly3 = new Poly3App(); poly3.LoadEmployees(); poly3.DoPayroll(); } }
Before running this application, let's take a peek at the IL code that's generated, this time noting that line IL_0014 uses the MSIL opcode callvirt,which tells the compiler that the exact method to be called won't be known until run time because it's dependent on which derived object is being used: -
.method public hidebysig instance void DoPayroll() il managed { // Code size 34 (0x22) .maxstack 2 .locals (class Employee V_0, class Employee[] V_1, int32 V_2, int32 V_3) IL_0000: ldarg.0 IL_0001: ldfld class Employee[] Poly3App::employees IL_0006: stloc.1 IL_0007: ldloc.1 IL_0008: ldlen IL_0009: conv.i4 IL_000a: stloc.2 IL_000b: ldc.i4.0 IL_000c: stloc.3 IL_000d: br.s IL_001d IL_000f: ldloc.1 IL_0010: ldloc.3 IL_0011: ldelem.ref IL_0012: stloc.0 IL_0013: ldloc.0 IL_0014: callvirt instance void Employee::CalculatePay() IL_0019: ldloc.3 IL_001a: ldc.i4.1 IL_001b: add IL_001c: stloc.3 IL_001d: ldloc.3 IL_001e: ldloc.2 IL_001f: blt.s IL_000f IL_0021: ret } // end of method Poly3App::DoPayroll
Running the code at this point should yield the following results: -
c:\>Poly3App ContractEmployee.CalculatePay called for Kate Dresen SalariedEmployee.CalculatePay called for Megan Sherman
Virtual methods cannot be declared as private because, by definition, they would not be visible in the derived classes.