Skip to content

4. Classes, Structures, Interfaces

4.1. The Object by Example

4.1.1. General Overview

We will now explore object-oriented programming through examples. An object is an entity that contains data defining its state (referred to as fields, attributes, etc.) and functions (referred to as methods). An object is created based on a template called a class:

public class C1{
    Type1 p1;        // field p1
    Type2 p2;        // field p2
    Type3 m3(…){        // method m3
    }
    Type4 m4(…){        // m4 method
    }
}

From the previous class C1, we can create many objects O1, O2,… All will have the fields p1, p2,… and the methods m3, m4,… But they will have different values for their fields pi, thus each having its own state. If o1 is an object of type C1, o1.p1 denotes the property p1 of o1 and o1.m1 denotes the method m1 of O1.

Let’s consider a first object model: the Person class.

4.1.2. Creating the C# Project

In the previous examples, we had only a single source file in a project: Program.cs. From now on, we can have multiple source files in the same project. We’ll show you how to do this.

In [1], create a new project. In [2], select Console Application. In [3], leave the default value. In [4], confirm. In [5], the generated project. The contents of Program.cs are as follows:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1 {
    class Program {
        static void Main(string[] args) {
        }
    }
}

Let's save the project we created:

In [1], select the save option. In [2], specify the folder where you want to save the project. In [3], give the project a name. In [5], indicate that you want to create a solution. A solution is a collection of projects. In [4], enter the name of the solution. In [6], confirm the save.

In [1], the saved project. In [2], add a new item to the project.

In [1], indicate that you want to add a class. In [2], enter the class name. In [3], confirm the information. In [4], project [01] has a new source file Personne.cs:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1 {
    class Person {
    }
}

We change the namespace of each source file to Chap2 and remove the imports of unnecessary namespaces:


using System;

namespace Chap2 {
    class Person {
    }
}

using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
        }
    }
}

4.1.3. Definition of the Person class

The definition of the Person class in the source file [Person.cs] will be as follows:


using System;

namespace Chap2 {
    public class Person {
        // attributes
        private string firstName;
        private string lastName;
        private int age;

        // method
        public void Initialize(string P, string N, int age) {
            this.firstName = P;
            this.lastName = N;
            this.age = age;
        }

        // method
        public void Identify() {
            Console.WriteLine("[{0}, {1}, {2}]", firstName, lastName, age);
        }
    }

}

Here we have the definition of a class, which is a data type. When we create variables of this type, we call them objects or class instances. A class is therefore a template from which objects are constructed.

The members or fields of a class can be data (attributes), methods (functions), or properties. Properties are special methods used to retrieve or set the value of an object’s attributes. These fields can be accompanied by one of the following three keywords:

private
A private field is accessible only by the class's internal methods
public
A public field is accessible by any method, whether or not it is defined within the class
protected
A protected field is accessible only by the class's internal methods or by a derived object (see the concept of inheritance later).

Generally, a class’s data is declared private, while its methods and properties are declared public. This means that the user of an object (the programmer)

  • will not have direct access to the object’s private data
  • can call the object’s public methods, particularly those that provide access to its private data.

The syntax for declaring a class C is as follows:


public class C{
    private data or method or private property;
    public data or method or public property;
    protected data or method or protected property;
}

The order in which private, protected, and public attributes are declared is arbitrary.

4.1.4. The Initialize method

Let’s return to our Person class declared as:


using System;

namespace Chap2 {
    public class Person {
        // attributes
        private string firstName;
        private string name;
        private int age;

        // method
        public void Initialize(string p, string n, int age) {
            this.firstName = p;
            this.lastName = n;
            this.age = age;
        }

        // method
        public void Identify() {
            Console.WriteLine("[{0}, {1}, {2}]", firstName, lastName, age);
        }
    }

}

What is the role of the Initialize method? Because lastName, firstName, and age are private members of the Person class, the statements:

Person p1;
p1.firstName="Jean";
p1.lastName = "Dupont";
p1.age=30;

are invalid. We must initialize an object of type Person using a public method. This is the role of the Initialize method. We will write:

Person p1;
p1.Initialize("Jean", "Dupont", 30);

The syntax p1.Initialize is valid because Initialize is public.

4.1.5. The new operator

The sequence of statements

Person p1;
p1.Initialize("Jean", "Dupont", 30);

is incorrect. The statement

    Person p1;

declares p1 as a reference to an object of type Person. This object does not yet exist, so p1 is not initialized. It is as if we had written:

Person p1 = null;

where we explicitly indicate with the keyword null that the variable p1 does not yet reference any object. When we then write

p1.Initialize("Jean", "Dupont", 30);

we call the Initialize method of the object referenced by p1. However, this object does not yet exist, and the compiler will report an error. To make p1 reference an object, we must write:

Person p1 = new Person();

This creates an uninitialized Person object: the first_name and last_name attributes, which are references to String objects, will have the value null, and age will have the value 0. Thus, default initialization occurs. Now that p1 references an object, the initialization statement for this object

p1.Initialize("Jean", "Dupont", 30);

is valid.

4.1.6. The keyword this

Let’s look at the code for the Initialize method:


        public void Initialize(string p, string n, int age) {
            this.firstName = p;
            this.lastName = n;
            this.age = age;
}

The statement this.firstName = p means that the firstName property of the current object (this) is assigned the value p. The keyword this refers to the current object: the one in which the method being executed resides. How do we know this? Let’s look at how the object referenced by p1 is initialized in the calling program:

p1.Initialize("Jean", "Dupont", 30);

It is the Initialise method of the p1 object that is called. When we reference the this object within this method, we are actually referencing the p1 object. The Initialise method could also have been written as follows:


        public void Initialize(string p, string n, int age) {
            firstName = p;
            lastName = n;
            this.age = age;
}

When a method of an object references an attribute A of that object, the notation this.A is implied. It must be used explicitly when there is a conflict of identifiers. This is the case with the statement:


this.age=age;

where age refers to both an attribute of the current object and the age parameter received by the method. The ambiguity must then be resolved by referring to the age attribute as this.age.

4.1.7. A test program

Here is a short test program. It is written in the source file [Program.cs]:


using System;

namespace Chap2 {
    class P01 {
        static void Main() {
            Person p1 = new Person();
            p1.Initialize("Jean", "Dupont", 30);
            p1.Identify();
        }
    }
}

Before running project [01], you may need to specify the source file to run:

In the properties of project [01], specify the class to run in [1].

The results obtained upon execution are as follows:

[Jean, Dupont, 30]

4.1.8. Another method: Initialize

Let’s consider the Person class again and add the following method to it:


        public void Initialize(Person p) {
            firstName = p.firstName;
            lastName = p.lastName;
            age = p.age;
}

We now have two methods named Initialize: this is valid as long as they accept different parameters. That is the case here. The parameter is now a reference p to a person. The attributes of the person p are then assigned to the current object (this). Note that the Initialize method has direct access to the attributes of the object p even though they are of type private. This is always true: an object o1 of a class C always has access to the attributes of objects of the same class C.

Here is a test of the new Person class:


using System;

namespace Chap2 {
    class Program {
        static void Main() {
            Person p1 = new Person();
            p1.Initialize("Jean", "Dupont", 30);
            p1.Identify();
            Person p2 = new Person;
            p2.Initialize(p1);
            p2.Identify();
        }
    }
}

and its results:

[Jean, Dupont, 30]
[Jean, Dupont, 30]

4.1.9. Constructors of the Person class

A constructor is a method that bears the name of the class and is called when the object is created. It is generally used to initialize the object. It is a method that can accept arguments but returns no result. Its prototype or definition is not preceded by any type (not even void).

If a class C has a constructor that accepts n arguments args, the declaration and initialization of an object of this class can be done as follows:

        C object = new C(arg1, arg2, ..., argn);

or

        C object;
        object = new C(arg1, arg2, ... argn);

When a class C has one or more constructors, one of these constructors must be used to create an object of that class. If a class C has no constructors, it has a default constructor, which is the constructor without parameters: public C(). The object’s attributes are then initialized with default values. This is what happened in the previous programs, where we wrote:

    Person p1;
    p1 = new Person();

Let’s create two constructors for our Person class:


using System;

namespace Chap2 {
    public class Person {
        // attributes
        private string firstName;
        private string lastName;
        private int age;

        // constructors
        public Person(String firstName, String lastName, int age) {
            Initialize(p, n, age);
        }
        public Person(Person P) {
            initialize(P);
        }

        // method
        public void Initialize(string firstName, string lastName, int age) {
...
        }

        public void Initialize(Person p) {
...
        }

        // method
        public void Identify() {
            Console.WriteLine("[{0}, {1}, {2}]", firstName, lastName, age);
        }
    }

}

Our two constructors simply call the Initialize methods discussed earlier. Recall that when, in a constructor, we see the notation Initialize(p), for example, the compiler translates it to this.Initialize(p). In the constructor, the Initialize method is therefore called to operate on the object referenced by this, that is, the current object, the one being constructed.

Here is a short test program:


using System;

namespace Chap2 {
    class Program {
        static void Main() {
            Person p1 = new Person("Jean", "Dupont", 30);
            p1.Identify();
            Person p2 = new Person(p1);
            p2.Identify();
        }
    }
}

and the results obtained:

[Jean, Dupont, 30]
[Jean, Dupont, 30]

4.1.10. Object references

We are still using the same Person class. The test program becomes the following:


using System;

namespace Chap2 {
    class Program2 {
        static void Main() {
            // p1
            Person p1 = new Person("Jean", "Dupont", 30);
            Console.Write("p1="); p1.Identify();
            // p2 references the same object as p1
            Person p2 = p1;
            Console.Write("p2="); p2.Identify();
            // p3 references an object that will be a copy of the object referenced by p1
            Person p3 = new Person(p1);
            Console.Write("p3="); p3.Identify();
            // we change the state of the object referenced by p1
            p1.SetProperties("Micheline", "Benoît", 67);
            Console.Write("p1="); p1.Identify();
            // since p2=p1, the object referenced by p2 must have changed state
            Console.Write("p2="); p2.Identify();
            // since p3 does not reference the same object as p1, the object referenced by p3 must not have changed
            Console.Write("p3="); p3.Identify();
        }
    }
}

The results are as follows:

1
2
3
4
5
6
p1=[Jean, Dupont, 30]
p2=[John, Smith, 30]
p3=[Jean, Dupont, 30]
p1=[Micheline, Benoît, 67]
p2=[Micheline, Benoît, 67]
p3=[John, Smith, 30]

When declaring the variable p1 as

Person p1 = new Person("Jean", "Dupont", 30);

p1 references the object Person("Jean", "Dupont", 30) but is not the object itself. In C, we would say that it is a pointer, i.e., the address of the created object. If we then write:

    p1 = null;

It is not the Person("Jean", "Dupont", 30) object that is modified; it is the p1 reference that changes value. The Person("Jean", "Dupont", 30) object will be "lost" if it is not referenced by any other variable.

When we write:

Person p2 = p1;

We initialize the pointer p2: it "points" to the same object (it refers to the same object) as the pointer p1. Thus, if we modify the object "pointed to" (or referenced) by p1, we also modify the one referenced by p2.

When we write:

Person p3 = new Person(p1);

a new Person object is created. This new object will be referenced by p3. If we modify the object "pointed to" (or referenced) by p1, we do not modify the one referenced by p3 in any way. This is what the results show.

4.1.11. Passing Object Reference Parameters

In the previous chapter, we studied how function parameters are passed when they represent a simple C# type represented by a .NET structure. Let’s see what happens when the parameter is an object reference:


using System;
using System.Text;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // Example 4
            StringBuilder sb0 = new StringBuilder("test0"), sb1 = new StringBuilder("test1"), sb2 = new StringBuilder("test2"), sb3;
            Console.WriteLine("In calling function before call: sb0={0}, sb1={1}, sb2={2}", sb0, sb1, sb2);
            ChangeStringBuilder(sb0, sb1, ref sb2, out sb3);
            Console.WriteLine("In calling function after call: sb0={0}, sb1={1}, sb2={2}, sb3={3}", sb0, sb1, sb2, sb3);

        }

        private static void ChangeStringBuilder(StringBuilder sbf0, StringBuilder sbf1, ref StringBuilder sbf2, out StringBuilder sbf3) {
            Console.WriteLine("Start of called function: sbf0={0}, sbf1={1}, sbf2={2}", sbf0, sbf1, sbf2);
            sbf0.Append("*****");
            sbf1 = new StringBuilder("test1*****");
            sbf2 = new StringBuilder("test2*****");
            sbf3 = new StringBuilder("test3*****");
            Console.WriteLine("End of function call: sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3);
        }
    }
}
  • Line 8: defines 3 StringBuilder objects. A StringBuilder object is similar to a string object. When manipulating a string object, a new string object is returned. Thus, in the code snippet:
string s = "a string";
s = s.ToUpperCase();

Line 1 creates a string object in memory, and s is its address. On line 2, s.ToUpperCase() creates another string object in memory. Thus, between lines 1 and 2, s has changed its value (it now points to the new object). The StringBuilder class, on the other hand, allows you to modify a string without creating a second object. This is the example given above:

  • line 8: 4 references [sb0, sb1, sb2, sb3] to StringBuilder objects
  • line 10: are passed to the ChangeStringBuilder method with different modes: sb0 and sb1 with the default mode, sb2 with the ref keyword, and sb3 with the out keyword.
  • lines 15–22: a method with formal parameters [sbf0, sbf1, sbf2, sbf3]. The relationships between the formal parameters sbf1 and the actual parameters sbi are as follows:
  • sbf0 and sb0 are, at the start of the method, two distinct references pointing to the same object (passing addresses by value)
  • The same applies to sbf1 and sb1
  • sbf2 and sb2 are, at the start of the method, the same reference to the same object (ref keyword)
  • sbf3 and sb3 are, after the method has executed, the same reference to the same object (out keyword)

The results obtained are as follows:

1
2
3
4
In the calling function before the call: sb0=test0, sb1=test1, sb2=test2
Start of called function: sbf0=test0, sbf1=test1, sbf2=test2
End of called function: sbf0=test0*****, sbf1=test1*****, sbf2=test2*****, sbf3=test3*****
In the calling function after the call: sb0=test0*****, sb1=test1, sb2=test2*****, sb3=test3*****

Explanations:

  • sb0 and sbf0 are two separate references to the same object. This object was modified via sbf0—line 3. This modification can be seen via sb0—line 4.
  • sb1 and sbf1 are two distinct references to the same object. sbf1’s value is modified within the method and now points to a new object—line 3. This does not change the value of sb1, which continues to point to the same object—line 4.
  • sb2 and sbf2 are the same reference to the same object. sbf2’s value is modified within the method and now points to a new object—line 3. Since sbf2 and sb2 are one and the same entity, sb2’s value has also been modified, and sb2 points to the same object as sbf2—lines 3 and 4.
  • Before the method call, sb3 had no value. After the method, sb3 receives the value of sbf3. We therefore have two references to the same object—lines 3 and 4

4.1.12. Temporary objects

In an expression, you can explicitly call an object’s constructor: the object is created, but you do not have access to it (to modify it, for example). This temporary object is created for the purpose of evaluating the expression and then discarded. The memory space it occupied will be automatically reclaimed later by a program called a "garbage collector," whose role is to reclaim memory space occupied by objects that are no longer referenced by program data.

Consider the following new test program:


using System;

namespace Chap2 {
    class Program {
        static void Main() {
            new Person(new Person("Jean", "Dupont", 30)).Identify();
        }
    }
}

and let's modify the constructors of the Person class so that they display a message:


        // constructors
        public Person(String p, String n, int age) {
            Console.WriteLine("Person constructor (string, string, int)");
            Initialize(p, n, age);
        }
        public Person(Person P) {
            Console.Out.WriteLine("Person constructor (Person)");
            Initialize(P);
}

We get the following results:

1
2
3
Constructor Person(string, string, int)
Person constructor(Person)
[John, Smith, 30]

showing the successive construction of the two temporary objects.

4.1.13. Methods for reading and writing private attributes

We add the necessary methods to the Person class to read or modify the state of the objects' attributes:


using System;

namespace Chap2 {
    public class Person {
        // attributes
        private string firstName;
        private string lastName;
        private int age;

        // constructors
        public Person(String firstName, String lastName, int age) {
            Console.WriteLine("Person constructor (string, string, int)");
            Initialize(p, n, age);
        }
        public Person(Person p) {
            Console.Out.WriteLine("Person constructor (Person)");
            Initialize(p);
        }

        // method
        public void Initialize(string p, string n, int age) {
            this.firstName = p;
            this.lastName = n;
            this.age = age;
        }

        public void Initialize(Person p) {
            firstName = p.firstName;
            lastName = p.lastName;
            age = p.age;
        }

        // accessors
        public String GetFirstName() {
            return firstName;
        }
        public String GetLastName() {
            return lastName;
        }
        public int GetAge() {
            return age;
        }

        //accessors
        public void SetFirstName(String P) {
            this.firstName = P;
        }
        public void SetLastName(String N) {
            this.lastName = N;
        }
        public void SetAge(int age) {
            this.age = age;
        }

        // method
        public void Identify() {
            Console.WriteLine("[{0}, {1}, {2}]", firstName, lastName, age);
        }
    }

}

We test the new class with the following program:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Person p = new Person("Jean", "Michelin", 34);
            Console.Out.WriteLine("p=(" + p.GetFirstName() + "," + p.GetLastName() + "," + p.GetAge() + ")");
            p.SetAge(56);
            Console.Out.WriteLine("p=(" + p.GetFirstName() + "," + p.GetLastName() + "," + p.GetAge() + ")");
        }
    }
}

and we get the results:

1
2
3
Person constructor(string, string, int)
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)

4.1.14. Properties

There is another way to access a class’s attributes: by creating properties. These allow us to manipulate private attributes as if they were public.

Consider the following Person class, where the previous accessors and mutators have been replaced by read-write properties:


using System;

namespace Chap2 {
    public class Person {
        // attributes
        private string firstName;
        private string lastName;
        private int age;

        // constructors
        public Person(String firstName, String lastName, int age) {
            Initialize(p, n, age);
        }
        public Person(Person p) {
            initialize(p);
        }

        // method
        public void Initialize(string firstName, string lastName, int age) {
            this.firstName = p;
            this.lastName = n;
            this.age = age;
        }

        public void Initialize(Person p) {
            firstName = p.firstName;
            lastName = p.lastName;
            age = p.age;
        }

        // properties
        public string FirstName {
            get { return firstName; }
            set {
                // Is the first name valid?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("Invalid first name (" + value + ")");
                } else {
                    firstName = value;
                }
            }//if
        }//firstName

        public string LastName {
            get { return lastName; }
            set {
                // Is the name valid?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("Invalid name (" + value + ")");
                } else { name = value; }
            }//if
        }//name


        public int Age {
            get { return age; }
            set {
                // Is the age valid?
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("Invalid age (" + value + ")");
            }//if
        }//age

        // method
        public void Identify() {
            Console.WriteLine("[{0}, {1}, {2}]", firstName, lastName, age);
        }
    }

}

A property allows you to read (get) or set the value of an attribute. A property is declared as follows:

public Type Property{
    get {...}
    set {...}
}

where Type must be the type of the attribute managed by the property. It can have two methods called get and set. The get method is usually responsible for returning the value of the attribute it manages (it could return something else; there is nothing preventing it from doing so). The set method receives a parameter called value, which it normally assigns to the attribute it manages. It can use this opportunity to validate the received value and, if necessary, throw an exception if the value is invalid. This is what is done here.

How are these get and set methods called? Consider the following test program:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Person p = new Person("Jean", "Michelin", 34);
            Console.Out.WriteLine("p=(" + p.FirstName + "," + p.LastName + "," + p.Age + ")");
            p.Age = 56;
            Console.Out.WriteLine("p=(" + p.FirstName + "," + p.LastName + "," + p.Age + ")");
            try {
                p.Age = -4;
            } catch (Exception ex) {
                Console.Error.WriteLine(ex.Message);
            }//try-catch
        }
    }
}

In the statement


    Console.Out.WriteLine("p=(" + p.FirstName + "," + p.LastName + "," + p.Age + ")");

we are trying to obtain the values of the FirstName, LastName, and Age properties of the person p. It is the get method of these properties that is then called and returns the value of the attribute they manage.

In the statement

        p.Age = 56;

We want to set the value of the Age property. The set method of this property is then called. It will receive 56 in its value parameter.

A property P of a class C that defines only the get method is said to be read-only. If c is an object of class C, the operation c.P=value will be rejected by the compiler.

Executing the previous test program yields the following results:

1
2
3
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)
age (-4) invalid

Properties therefore allow us to manipulate private attributes as if they were public. Another feature of properties is that they can be used in conjunction with a constructor according to the following syntax:

Class object = new Class (...) {Property1 = val1, Property2 = val2, ...}

This syntax is equivalent to the following code:

1
2
3
4
Class object = new Class(...);
object.Property1 = val1;
object.Property2 = val2;
...

The order of the properties does not matter. Here is an example.

A new constructor with no parameters is added to the Person class:


        public Person() {
}

The constructor does not initialize the object’s members. This is called the default constructor. It is used when the class does not define any constructors.

The following code creates and initializes (line 6) a new Person using the syntax shown above:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Person p2 = new Person { Age = 7, FirstName = "Arthur", LastName = "Martin" };
            Console.WriteLine("p2=({0},{1},{2})", p2.FirstName, p2.LastName, p2.Age);
        }
    }
}

In line 6 above, the parameterless constructor Person() is used. In this particular case, we could also have written


            Person p2 = new Person() { Age = 7, FirstName = "Arthur", LastName = "Martin" };

but the parentheses for the parameterless Person() constructor are not required in this syntax.

The results of the execution are as follows:

p2=(Arthur,Martin,7)

In many cases, the get and set methods of a property simply read from and write to a private field without any further processing. In this scenario, we can use an automatic property declared as follows:

public Type Property{ get; set; }

The private field associated with the property is not declared. It is automatically generated by the compiler. It can only be accessed via its property. Thus, instead of writing:


    private string firstName;
...
    // associated property
        public string FirstName {
            get { return firstName; }
            set {
                // Is the first name valid?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("Invalid first name (" + value + ")");
                } else {
                    firstName = value;
                }
            }//if
        }//firstName

we can write:

public string FirstName {get; set;}

without declaring the private field firstName. The difference between the two properties above is that the first one checks the validity of the first name in the set, while the second one performs no validation.

Using the automatic FirstName property is equivalent to declaring a public FirstName field:

public string FirstName;

One might wonder if there is a difference between the two declarations. Declaring a class field as public is discouraged. This violates the concept of encapsulating an object’s state, which should be private and exposed through public methods.

If the automatic property is declared virtual, it can then be overridden in a child class:


    class Class1 {
        public virtual string Prop { get; set; }
}

    class Class2 : Class1 {
        public override string Prop { get { return base.Prop; } set {... } }
}

In line 2 above, the child class Class2 can include code in the setter that checks the validity of the value assigned to the automatic property base.Prop of the parent class Class1.

4.1.15. Class methods and attributes

Suppose we want to count the number of Person objects created in an application. We could manage a counter ourselves, but we risk overlooking temporary objects created here and there. It would seem safer to include an instruction in the Person class constructors that increments a counter. The problem is passing a reference to this counter so that the constructor can increment it: we need to pass a new parameter to them. We can also include the counter in the class definition. Since it is an attribute of the class itself and not of a particular instance of that class, we declare it differently using the static keyword:


        private static long nbPeople;

To reference it, we write Person.nbPeople to show that it is an attribute of the Person class itself. Here, we have created a private attribute that cannot be accessed directly from outside the class. We therefore create a public property to provide access to the class attribute nbPeople. To return the value of nbPeople, the get method of this property does not need a specific Person object: indeed, nbPeople is an attribute of the entire class. Therefore, we need a property that is also declared as static:


        public static long NbPeople {
            get { return nbPeople; }
}

which will be called from outside using the syntax Person.NbPersonnes. Here is an example.

The Person class becomes the following:


using System;

namespace Chap2 {
    public class Person {

        // class attributes
        private static long numberOfPeople;
        public static long numberOfPeople {
            get { return numberOfPeople; }
        }

        // instance attributes
        private string lastName;
        private string lastName;
        private int age;

        // constructors
        public Person(String firstName, String lastName, int age) {
            Initialize(p, n, age);
            numberOfPeople++;
        }
        public Person(Person p) {
            initialize(p);
            numberOfPeople++;
        }

...
}

Lines 20 and 24: the constructors increment the static field on line 7.

With the following program:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Person p1 = new Person("John", "Smith", 30);
            Person p2 = new Person(p1);
            new Person(p1);
            Console.WriteLine("Number of people created: " + Person.NumberOfPeople);
        }
    }
}

The following results are obtained:

    Number of people created: 3

4.1.16. An array of people

An object is a piece of data like any other, and as such, multiple objects can be grouped together in an array:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            // an array of people
            Person[] friends = new Person[3];
            friends[0] = new Person("Jean", "Dupont", 30);
            friends[1] = new Person("Sylvie", "Vartan", 52);
            friends[2] = new Person("Neil", "Armstrong", 66);
            // display
            foreach (Person friend in friends) {
                friend.Identify();
            }
        }
    }
}
  • Line 7: creates an array of 3 elements of type Person. These 3 elements are initialized here with the value null, meaning they do not reference any objects. Again, strictly speaking, we refer to it as an "array of objects" when it is actually just an array of object references. The creation of the object array, which is an object itself (indicated by the use of `new`), does not create any objects of the type of its elements: this must be done later.
  • lines 8–10: creation of the 3 objects of type Person
  • lines 12–14: display of the contents of the friends array

The following results are obtained:

1
2
3
[Jean, Dupont, 30]
[Sylvie, Vartan, 52]
[Neil, Armstrong, 66]

4.2. Inheritance by example

4.2.1. Overview

Here we discuss the concept of inheritance. The purpose of inheritance is to "customize" an existing class so that it meets our needs. Suppose we want to create a Teacher class: a teacher is a specific person ( ). They have attributes that other people do not have: the subject they teach, for example. But they also have the attributes of any person: first name, last name, and age. A teacher is therefore a full member of the Person class but has additional attributes. Rather than writing a Teacher class from scratch, we would prefer to build upon the existing Person class and adapt it to the specific characteristics of teachers. It is the concept of inheritance that allows us to do this.

To express that the Teacher class inherits the properties of the Person class, we would write:

    public class Teacher : Person

The Person class is called the parent (or base) class, and the Teacher class is called the derived (or child) class. A Teacher object has all the characteristics of a Person object: it has the same attributes and methods. These attributes and methods of the parent class are not repeated in the definition of the child class; we simply list the attributes and methods added by the child class:

We assume that the Person class is defined as follows:


using System;

namespace Chap2 {
    public class Person {

        // class attributes
        private static long numberOfPeople;
        public static long numberOfPeople {
            get { return numberOfPeople; }
        }

        // instance attributes
        private string firstName;
        private string lastName;
        private int age;

        // constructors
        public Person(String firstName, String lastName, int age) {
            LastName = lastName;
            FirstName = firstName;
            Age = age;
            nbPeople++;
            Console.WriteLine("Person constructor (string, string, int)");
        }
        public Person(Person p) {
            LastName = p.LastName;
            FirstName = p.FirstName;
            Age = p.Age;
            nbPeople++;
            Console.WriteLine("Person constructor (Person)");
        }

        // properties
        public string FirstName {
            get { return firstName; }
            set {
                // Is the first name valid?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("Invalid first name (" + value + ")");
                } else {
                    firstName = value;
                }
            }//if
        }//firstName

        public string LastName {
            get { return lastName; }
            set {
                // Is the name valid?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("Invalid name (" + value + ")");
                } else { name = value; }
            }//if
        }//name

        public int Age {
            get { return age; }
            set {
                // Is the age valid?
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("Invalid age (" + value + ")");
            }//if
        }//age

        // property
        public string Identity {
            get { return String.Format("[{0}, {1}, {2}]", firstName, lastName, age);}
        }
    }

}

The Identify method has been replaced by the read-only Identity property, which identifies the person. We create a Teacher class that inherits from the Person class:


using System;

namespace Chap2 {
    class Teacher : Person {
        // attributes
        private int section;

        // constructor
        public Teacher(string firstName, string lastName, int age, int section)
            : base(firstName, lastName, age) {
            // Store the section using the Section property
            Section = section;
            // tracking
            Console.WriteLine("Construction Teacher(string, string, int, int)");
        }//constructor

        // Section property
        public int Section {
            get { return section; }
            set { section = value; }
        }// Section

    }
}

The Teacher class adds to the methods and attributes of the Person class:

  • line 4: the Teacher class derives from the Person class
  • line 6: a `section` attribute representing the section number to which the teacher belongs within the teaching staff (roughly one section per subject). This private attribute is accessible via the public `Section` property in lines 18–21
  • line 9: a new constructor that initializes all of a teacher’s attributes

4.2.2. Creating a Teacher object

A child class does not inherit constructors from its parent class. It must therefore define its own constructors. The constructor for the Teacher class is as follows:


        // constructor
        public Teacher(string firstName, string lastName, int age, int section)
            : base(firstName, lastName, age) {
            // store the section
            Section = section;
            // follow-up
            Console.WriteLine("Constructing Teacher(string, string, int, int)");
}//constructor

The declaration


        public Teacher(string firstName, string lastName, int age, int section)
            : base(firstName, lastName, age) {

states that the constructor takes four parameters—firstName, lastName, age, and section—and passes three of them (firstName, lastName, age) to its base class, in this case the Person class. We know that this class has a constructor Person(string, string, int) that will allow us to construct a person using the passed parameters (firstName, lastName, age). Once the base class has been constructed, the construction of the Teacher object continues by executing the body of the constructor:


            // we store the section
            Section = section;

Note that on the left side of the = sign, it is not the object’s section attribute that was used, but the Section property associated with it. This allows the constructor to take advantage of any validity checks that this method might perform. This avoids having to place these checks in two different places: the constructor and the property.

In summary, the constructor of a derived class:

  • passes to its base class the parameters it needs to construct itself
  • uses the other parameters to initialize its own attributes

One might have preferred to write:


// constructor
  public Teacher(string firstName, string lastName, int age, int section){
    this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
      this.class = class;
  }

That's impossible. The Person class has declared its three fields—firstName, lastName, and age—as private. Only objects of the same class have direct access to these fields. All other objects, including child objects as in this case, must use public methods to access them. This would have been different if the Person class had declared the three fields as protected: it would then have allowed derived classes to have direct access to the three fields. In our example, using the parent class’s constructor was therefore the correct solution, and this is the standard approach: when constructing a child object, we first call the parent object’s constructor and then complete the initializations specific to the child object (section in our example).

Let’s try a first test program [Program.cs]:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Teacher("Jean", "Dupont", 30, 27).Identity);
        }
    }
}

This program simply creates a Teacher object (new) and identifies it. The Teacher class does not have an Identity method, but its parent class does have one, which is also public: through inheritance, it becomes a public method of the Teacher class.

The entire project is as follows:

The results obtained are as follows:

1
2
3
Person constructor(string, string, int)
Teacher constructor(string, string, int, int)
[Jean, Dupont, 30]

We can see that:

  • a Person object (line 1) was constructed before the Teacher object (line 2)
  • the identity obtained is that of the Person object

4.2.3. Redefining a method or property

In the previous example, we had the identity of the Person part of the teacher, but some information specific to the Teacher class (the section) is missing. We therefore need to write a property that allows us to identify the teacher:


using System;

namespace Chap2 {
    class Teacher : Person {
        // attributes
        private int section;

        // constructor
        public Teacher(string firstName, string lastName, int age, int section)
            : base(firstName, lastName, age) {
            // store the section via the Section property
            Section = section;
            // follow-up
            Console.WriteLine("Constructing Teacher(string, string, int, int)");
        }//constructor

        // Section property
        public int Section {
            get { return section; }
            set { section = value; }
        }// section

        // Identity property
        public new string Identity {
            get { return String.Format("Teacher[{0},{1}]", base.Identity, Section); }
        }
    }
}

Lines 24–26: The Identity property of the Teacher class relies on the Identity property of its parent class (base.Identity) (line 25) to display its "Person" portion, then supplements it with the Section field, which is specific to the Teacher class. Note the declaration of the Identity property:


    public new string Identity{

Let E be a Teacher object. This object contains a Person object:

The Identity property is defined in both the Teacher class and its parent class Person. In the child class Teacher, the Identity property must be preceded by the keyword new to indicate that we are redefining a new Identity property for the Teacher class.


    public new string Identity{

The Teacher class now has two Identity properties:

  • the one inherited from the parent Person class
  • its own

If E is a Teacher object, E.Identity refers to the Identity property of the Teacher class. We say that the Identity property of the child class redefines or hides the Identity property of the parent class. In general, if O is an object and M is a method, to execute the method O.M, the system searches for a method M in the following order:

  • in the class of object O
  • in its parent class, if it has one
  • in the parent class of its parent class, if it exists
  • etc…

Inheritance therefore allows methods and properties with the same name in the parent class to be redefined in the child class. This is what allows the child class to be adapted to its own needs. Combined with polymorphism, which we will discuss shortly, the redefinition of methods and properties is the main benefit of inheritance.

Let’s consider the same test program as before:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Teacher("Jean", "Dupont", 30, 27).Identity);
        }
    }
}

The results obtained this time are as follows:

1
2
3
Person constructor(string, string, int)
Constructor Teacher(string, string, int, int)
Teacher[[Jean, Dupont, 30],27]

4.2.4. Polymorphism

Consider a class hierarchy: C0 C1 C2 … ← Cn

where Ci Cj indicates that class Cj is derived from class Ci. This implies that class Cj has all the characteristics of class Ci plus additional ones. Let Oi be objects of type Ci. It is valid to write:

    Oi = Oj with j > i

Indeed, by inheritance, class Cj has all the characteristics of class Ci plus additional ones. Therefore, an object Oj of type Cj contains within it an object of type Ci. The operation

    Oi = Oj

means that Oi is a reference to the object of type Ci contained within the object Oj.

The fact that a variable Oi of class Ci can in fact refer not only to an object of class Ci but to any object derived from class Ci is called polymorphism: the ability of a variable to refer to different types of objects.

Let’s take an example and consider the following class-independent (static) function:

    public static void Display(Person p){
        ….
    }

We could just as easily write

    Person p;
    ...
    Display(p);

as

    Teacher;
    ...
    Display(e);

In the latter case, the formal parameter p of type Person in the static method Display will receive a value of type Teacher. Since the Teacher type derives from the Person type, this is valid.

4.2.5. Overriding and Polymorphism

Let’s complete our Display method:


        public static void Display(Person p) {
            // displays the identity of p
            Console.WriteLine(p.Identity);
}//display

The p.Identity property returns a string that identifies the Person object p. What happens in the previous example if the parameter passed to the Display method is an object of type Teacher:


            Teacher e = new Teacher(...);
            Display(e);

Let's look at the following example:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // a teacher
            Teacher e = new Teacher("Lucile", "Dumas", 56, 61);
            Display(e);
            // a person
            Person p = new Person("Jean", "Dupont", 30);
            Display(p);
        }

        // displays
        public static void Display(Person p) {
            // displays p's identity
            Console.WriteLine(p.Identity);
        }//display
    }
}

The results are as follows:

1
2
3
4
5
Person constructor(string, string, int)
Constructor Teacher(string, string, int, int)
[Lucile, Dumas, 56]
Person constructor(string, string, int)
[Jean, Dupont, 30]

The execution shows that the statement p.Identity (line 17) evaluated the Identity property of a Person each time: first (line 7) for the person contained in the Teacher e, then (line 10) for the Person p itself. It did not adapt to the object actually passed as a parameter to Display. We would have preferred to have the full identity of the Teacher e. For this to happen, the notation p.Identity would have had to refer to the Identity property of the object actually pointed to by p rather than the Identity property of the "Person" part of the object actually pointed to by p.

This result can be achieved by declaring Identity as a virtual property in the Person base class:


public virtual string Identity {
            get { return String.Format("[{0}, {1}, {2}]", firstName, lastName, age); }
        }

The virtual keyword makes Identity a virtual property. This keyword can also be applied to methods. Derived classes that redefine a virtual property or method must then use the override keyword instead of new to qualify their redefined property/method. Thus, in the Teacher class, the Identity property is redefined as follows:


        public override string Identity {
            get { return String.Format("Teacher[{0},{1}]", base.Identity, Section); }
}

The previous program then produces the following results:

1
2
3
4
5
Person constructor(string, string, int)
Teacher constructor(string, string, int, int)
Teacher[[Lucile, Dumas, 56],61]
Person constructor(string, string, int)
[Jean, Dupont, 30]

This time, on line 3, we did get the teacher’s full identity. Let’s now redefine a method rather than a property. The object class (C# alias for System.Object) is the “parent” class of all C# classes. So when we write:

    public class Person

we are implicitly writing:

    public class Person : System.Object

The System.Object class defines a virtual method called ToString:

The ToString method returns the name of the class to which the object belongs, as shown in the following example:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // a teacher
            Console.WriteLine(new Teacher("Lucile", "Dumas", 56, 61).ToString());
            // a person
            Console.WriteLine(new Person("Jean", "Dupont", 30).ToString());
        }
    }
}

The results are as follows:

1
2
3
4
5
Person constructor(string, string, int)
Construction Teacher(string, string, int, int)
Chap2.Teacher
Person constructor(string, string, int)
Chap2.Person

Note that although we have not overridden the ToString method in the Person and Teacher classes, we can see that the ToString method of the Object class was able to display the actual class name of the object.

Let’s override the ToString method in the Person and Teacher classes:


        // ToString method
        public override string ToString() {
            return Identity;
}

The definition is the same in both classes. Consider the following test program:


using System;
namespace Chap2 {
    class Program3 {
        public static void Main() {
            // a teacher
            Teacher e = new Teacher("Lucile", "Dumas", 56, 61);
            Display(e);
            // a person
            Person p = new Person("Jean", "Dupont", 30);
            Display(p);
        }
        // display
        public static void Display(Person p) {
            // displays p's identity
            Console.WriteLine(p);
        }//Display
    }
}

Let’s focus on the Display method, which takes a Person p as a parameter. On line 15, the WriteLine method of the Console class has no variant that accepts a Person-type parameter. Among the various variants of WriteLine, there is one that accepts an Object type as a parameter. The compiler will use this method, WriteLine(Object o), because this signature means that the parameter o can be of type Object or a derived type. Since Object is the base class of all classes, any object can be passed as a parameter to WriteLine, and therefore an object of type Person or Teacher. The WriteLine(Object o) method writes o.ToString() to the Out output stream. Since the ToString method is virtual, if the object o (of type Object or a derived type) has overridden the ToString method, that method will be used instead. This is the case here with the Person and Teacher classes.

This is what the execution results show:

1
2
3
4
5
Person constructor(string, string, int)
Teacher constructor(string, string, int, int)
Teacher[[Lucile, Dumas, 56],61]
Person constructor(string, string, int)
[Jean, Dupont, 30]

4.3. Redefining the meaning of an operator for a class

4.3.1. Introduction

Consider the statement

op1 + op2

where op1 and op2 are two operands. It is possible to redefine the meaning of the + operator. If the operand op1 is an object of class C1, a static method must be defined in class C1 with the following signature:

public static [type] operator +(C1 operand1, C2 operand2);

When the compiler encounters the statement

op1 + op2

It then translates it to C1.operator+(op1,op2). The type returned by the operator method is important. Consider the operation op1+op2+op3. The compiler translates it to (op1+op2)+op3. Let res12 be the result of op1+op2. The next operation performed is res12+op3. If res12 is of type C1, it will also be translated as C1.operator+(res12,op3). This allows operations to be chained together.

We can also redefine unary operators that have only a single operand. Thus, if op1 is an object of type C1, the operation op1++ can be redefined by a static method of the C1 class:

public static [type] operator ++(C1 operand1);

What has been said here is true for most operators, with a few exceptions:

  • the == and != operators must be redefined together
  • the operators &&, ||, [], (), +=, -=, ... cannot be redefined

4.3.2. An example

We create a PersonList class derived from the ArrayList class. This class implements a dynamic list and is presented in the following chapter. From this class, we use only the following elements:

  • the method L.Add(Object o), which allows us to add an object o to the list L. Here, the object o will be a Person object.
  • the property L.Count, which returns the number of elements in list L
  • the notation L[i], which returns the i-th element of the list L

The PersonList class will inherit all the attributes, methods, and properties of the ArrayList class. Its definition is as follows:


using System;
using System.Collections;
using System.Text;

namespace Chap2 {
    class PeopleList : ArrayList{
        // Redefining the + operator to add a person to the list
        public static PersonList operator +(PersonList l, Person p) {
            // Add Person p to PeopleList l
            l.Add(p);
            // return the ListOfPeople l
            return l;
        }// operator +

        // ToString
        public override string ToString() {
            // returns (el1, el2, ..., eln)
            // opening parenthesis
            StringBuilder listToString = new StringBuilder("(");
            // iterate through the list of people (this)
            for (int i = 0; i < Count - 1; i++) {
                listeToString.Append(this[i]).Append(",");
            }//for
            // last element
            if (Count != 0) {
                listeToString.Append(this[Count-1]);
            }
            // closing parenthesis
            listeToString.Append(""));
            // we need to return a string
            return listToString.ToString();
        }//ToString
    }
}
  • line 6: the PeopleList class derives from the ArrayList class
  • lines 8–13: definition of the + operator for the operation l + p, where l is of type ListOfPeople and p is of type Person or a derived class.
  • line 10: the person p is added to the list l. The Add method of the parent class ArrayList is used here.
  • line 12: the reference to the list l is returned so that + operators can be chained together, such as in l + p1 + p2. The operation l+p1+p2 will be interpreted (operator precedence) as (l+p1)+p2. The operation l+p1 will return the reference l. The operation (l+p1)+p2 then becomes l+p2, which adds person p2 to the list of people l.
  • Line 16: We redefine the ToString method to display a list of people in the form (person1, person2, ...), where personi is itself the result of the ToString method of the Person class.
  • Line 19: We use a StringBuilder object. This class is more suitable than the String class whenever you need to perform multiple operations on a string, in this case appending characters. This is because every operation on a String object returns a new String object, whereas the same operations on a StringBuilder object modify the existing object without creating a new one. We use the Append method to concatenate the strings.
  • Line 21: We iterate through the elements of the list of people. This list is referred to here as `this`. It is the current object on which the `ToString` method is executed. The `Count` property is a property of the parent `ArrayList` class.
  • Line 22: Element number i of the current list this is accessible via the notation this[i]. Again, this is a property of the ArrayList class. Since we are adding strings, the method this[i].ToString() will be used. Since this method is virtual, the ToString method of the this object, of type Person or a derived class, will be used.
  • Line 31: We need to return an object of type string (line 16). The StringBuilder class has a ToString method that allows us to convert from a StringBuilder type to a string type.

Note that the ListOfPeople class does not have a constructor. In this case, we know that the constructor

public ListOfPeople(){
}

will be used. This constructor does nothing other than call the parameterless constructor of its parent class:

public ArrayList(){
...
}

A test class might look like this:


using System;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // a list of people
            ListOfPeople l = new ListOfPeople();
            // adding people
            l = l + new Person("jean", "martin", 10) + new Person("pauline", "leduc", 12);
            // display
            Console.WriteLine("l=" + l);
            l = l + new Teacher("camille", "germain", 27, 60);
            Console.WriteLine("l=" + l);
        }
    }
}
  • line 7: creating a list of people l
  • line 9: adding 2 people using the + operator
  • line 12: adding a teacher
  • lines 11 and 13: use of the redefined method ListOfPeople.ToString().

The results:

l=([jean, martin, 10],[pauline, leduc, 12])
l=([jean, martin, 10],[pauline, leduc, 12],Teacher[[camille, germain, 27],60])

4.4. Defining an indexer for a class

Here, we continue to use the PersonList class. If l is a PersonList object, we want to be able to use the notation l[i] to refer to the person at index i in the list l, both for reading (Person p = l[i]) and for writing (l[i] = new Person(...)).

To be able to write l[i], where l[i] refers to a Person object, we need to define the following this method in the PeopleList class:


        public Person this[int i] {
            get { ... }
            set { ... }
}

We call the method this[int i] an indexer because it gives meaning to the expression obj[i], which resembles array notation even though obj is not an array but an object. The get method of the this method of the obj object is called when we write variable = obj[i], and the set method when we write obj[i] = value.

The PeopleList class derives from the ArrayList class, which itself has an indexer:

    public object this[int i] { ... }

There is a conflict between the this method of the PersonList class:


 public Person this[int i] 

and the *this method of the *ArrayList class


 public object this[int i] 

because they have the same name and accept the same parameter type (int). To indicate that the this method of the ListeDePersonnes class "hides" the method of the same name in the *ArrayList class, we must add the new* keyword to the declaration of the *ListeDePersonnes* indexer. We will therefore write:


    public new Person this[int i]{
        get { ... }
        set { ... }
    }

Let’s complete this method. The this.get method is called when we write variable = l[i], for example, where l is of type PersonList. We must then return the person at index i from the list l. This is done using the notation base[i], which returns the object at index i from the ArrayList class underlying the PersonList class. Since the returned object is of type Object, a cast to the Person class is necessary.


    public new Person this[int i]{
        get { return (Person) base[i]; }
        set { ... }
    }

The set method is called when we write l[i] = p, where p is a Person. This involves assigning the person p to element i of the list l.


    public new Person this[int i]{
        get { ... }
        set { base[i] = value; }
    }

Here, the Person p represented by the keyword value is assigned to element i of the base class ArrayList.

The indexer for the PersonList class will therefore be as follows:


    public new Person this[int i] {
        get { return (Person) base[i]; }
        set { base[i] = value; }
    }

Now, we also want to be able to write Person p = l["name"], i.e., index the list l not by an element number but by a person's name. To do this, we define a new indexer:


        // indexer via a name
        public int this[string name] {
            get {
                // search for the person
                for (int i = 0; i < Count; i++) {
                    if (((Person)base[i]).Name == name)
                        return i;
                }//for
                return -1;
            }//get
}

The first line


public int this[string name]

indicates that the PeopleList class is indexed by a string named name and that the result of l[name] is an integer. This integer will be the position in the list of the person with the name name, or -1 if that person is not in the list. We define only the get property, thereby preventing the assignment l["name"]=value, which would have required the definition of the set property. The new keyword is not required in the indexer declaration because the base class ArrayList does not define an indexer this[string].

In the body of the get method, we iterate through the list of people looking for the name passed as a parameter. If we find it at position i, we return i; otherwise, we return -1.

The previous test program is completed as follows:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // a list of people
            ListOfPeople l = new ListOfPeople();
            // adding people
            l = l + new Person("jean", "martin", 10) + new Person("pauline", "leduc", 12);
            // display
            Console.WriteLine("l=" + l);
            l = l + new Teacher("Camille", "Germain", 27, 60);
            Console.WriteLine("l=" + l);
            // change element 1
            l[1] = new Person("franck", "gallon", 5);
            // display element 1
            Console.WriteLine("l[1]=" + l[1]);
            // display list l
            Console.WriteLine("l=" + l);
            // search for people
            string[] names = { "martin", "germain", "xx" };
            for (int i = 0; i < names.Length; i++) {
                int name = l[names[i]];
                if (nom != -1)
                    Console.WriteLine("Person(" + names[i] + ")=" + l[nom]);
                else
                    Console.WriteLine("Person(" + names[i] + ") does not exist");
            }//for
        }
    }
}

Running this code produces the following results:

1
2
3
4
5
6
7
l=([jean, martin, 10],[pauline, leduc, 12])
l=([jean, martin, 10],[pauline, leduc, 12],Teacher[[camille, germain, 27],60])
l[1]=[franck, gallon, 5]
l=([jean, martin, 10],[franck, gallon, 5],Teacher[[camille, germain, 27],60])
Person(martin)=[jean, martin, 10]
Person(germain)=Teacher[[camille, germain, 27],60]
Person(xx) does not exist

4.5. Structures

The C# structure is analogous to the structure in the C language and is very similar to the concept of a class. A structure is defined as follows:

struct StructureName{
// attributes
    ...
// properties
...
// constructors
...
// methods
...
}

Despite similarities in their declarations, there are significant differences between classes and structures. For example, the concept of inheritance does not exist with structures. If we are writing a class that is not intended to be derived from another, what are the differences between structures and classes that will help us choose between the two? Let’s use the following example to find out:


using System;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // a structure sp1
            SPerson sp1;
            sp1.Name = "paul";
            sp1.Age = 10;
            Console.WriteLine("sp1=SPerson(" + sp1.Name + "," + sp1.Age + ")");
            // a structure sp2
            SPerson sp2 = sp1;
            Console.WriteLine("sp2 = SPerson(" + sp2.Name + "," + sp2.Age + ")");
            // sp2 is modified
            sp2.Name = "nicole";
            sp2.Age = 30;
            // Checking sp1 and sp2
            Console.WriteLine("sp1 = Person(" + sp1.Name + "," + sp1.Age + ")");
            Console.WriteLine("sp2=SPerson(" + sp2.Name + "," + sp2.Age + ")");

            // an object op1
            CPerson op1 = new CPersonne();
            op1.Name = "paul";
            op1.Age = 10;
            Console.WriteLine("op1 = CPerson(" + op1.Name + "," + op1.Age + ")");
            // an object op2
            CPerson op2 = op1;
            Console.WriteLine("op2 = CPerson(" + op2.Name + "," + op2.Age + ")");
            // op2 is modified
            op2.Name = "nicole";
            op2.Age = 30;
            // Check op1 and op2
            Console.WriteLine("op1 = CPerson(" + op1.Name + "," + op1.Age + ")");
            Console.WriteLine("op2=CPerson(" + op2.Name + "," + op2.Age + ")");
        }
    }
    // SPersonne structure
    struct SPerson {
        public string Name;
        public int Age;
    }

    // CPerson class
    class CPerson {
        public string Name;
        public int Age;
    }

}
  • lines 38–41: a structure with two public fields: Name, Age
  • lines 44–47: a class with two public fields: Name, Age

If we run this program, we get the following results:

1
2
3
4
5
6
7
8
sp1=SPerson(paul,10)
sp2=SPerson(paul,10)
sp1=SPerson(paul,10)
sp2=SPerson(nicole,30)
op1=CPerson(paul,10)
op2 = CPerson(paul, 10)
op1 = CPerson(nicole, 30)
op2 = CPerson(nicole, 30)

Where we previously used a Person class, we now use an SPersonne structure:


    struct SPerson {
        public string Name;
        public int Age;
}

The structure does not have a constructor here. It could have one, as we will show later. By default, it always has a constructor without parameters, in this case SPersonne().

  • Line 7 of the code: the declaration

    SPersonne sp1;

is equivalent to the statement:


    SPersonne sp1 = new SPersonne();

A structure (Name,Age) is created, and the value of sp1 is this structure itself. In the case of a class, the object (Name,Age) must be created explicitly using the new operator (line 22):


CPerson op1 = new CPerson();

The previous statement creates a CPerson object (roughly equivalent to our structure) and the value of p1 is then the address (the reference) of this object.

To summarize

  • in the case of the structure, the value of sp1 is the structure itself
  • in the case of a class, the value of op1 is the address of the created object

When in the program we write line 12:


            SPersonne sp2 = sp1;

A new structure sp2(Name, Age) is created and initialized with the value of sp1, i.e., the structure itself.

The structure of sp1 is duplicated in sp2 [1]. This is a copy by value. Now consider the statement on line 27:


CPerson op2 = op1;

In the case of classes, the value of op1 is copied into op2, but since this value is actually the address of the object, the object itself is not duplicated [2].

In the case of the structure [1], if we modify the value of sp2, we do not modify the value of sp1, as the program demonstrates. In the case of the object [2], if we modify the object pointed to by op2, the one pointed to by op1 is modified since they are the same. This is also demonstrated by the program’s results.

We can therefore conclude from these explanations that:

  • the value of a variable of type structure is the structure itself
  • the value of a variable of type object is the address of the object it points to

Once this fundamental difference is understood, the structure proves to be very similar to a class, as shown in the following new example:


using System;

namespace Chap2 {

    // structure SPerson
    struct SPerson {
        // private attributes
        private string name;
        private int age;

        // properties
        public string Name {
            get { return name; }
            set { name = value; }
        }//name

        public int Age {
            get { return age; }
            set { age = value; }
        }//age

        // Constructor
        public SPersonne(string name, int age) {
            this.name = name;
            this.age = age;
        }//constructor

        // ToString
        public override string ToString() {
            return "SPerson(" + Name + "," + Age + ")";
        }//ToString
    }//structure
}//namespace
  • lines 8-9: two private fields
  • lines 12–20: the associated public properties
  • lines 23–26: a constructor is defined. Note that the parameterless constructor SPersonne() is always present and does not need to be declared. Its declaration is rejected by the compiler. In the constructor on lines 23–26, one might be tempted to initialize the private fields name and age via their public properties Name and Age. This is rejected by the compiler. The structure’s methods cannot be used during its construction.
  • Lines 29–31: redefinition of the ToString method.

A test program might look like this:


using System;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // a person p1
            SPerson p1 = new SPersonne();
            p1.Name = "paul";
            p1.Age = 10;
            Console.WriteLine("p1={0}", p1);
            // a person p2
            SPerson p2 = p1;
            Console.WriteLine("p2=" + p2);
            // p2 is modified
            p2.Name = "nicole";
            p2.Age = 30;
            // Check p1 and p2
            Console.WriteLine("p1=" + p1);
            Console.WriteLine("p2=" + p2);
            // a person p3
            SPerson p3 = new SPerson("amandin", 18);
            Console.WriteLine("p3=" + p3);
            // a person p4
            SPerson p4 = new SPerson { Name = "x", Age = 10 };
            Console.WriteLine("p4=" + p4);
        }
    }
}
  • Line 7: We are required to explicitly use the parameterless constructor because there is another constructor in the structure. If the structure had no constructor, the statement

            SPerson p1;

would have been sufficient to create an empty structure.

  • lines 8–9: the structure is initialized via its public properties
  • line 10: the p1.ToString method will be used in the WriteLine.
  • line 21: creation of a structure using the SPersonne(string, int) constructor
  • line 24: creation of a structure using the parameterless constructor SPersonne() with, within curly braces, initialization of the private fields via their public properties.

The following execution results are obtained:

1
2
3
4
5
6
p1 = SPersonne(paul, 10)
p2=SPersonne(paul,10)
p1=SPersonne(paul,10)
p2=SPersonne(nicole,30)
p3=SPerson(amandin,18)
p4 = SPerson(x, 10)

The only notable difference here between a structure and a class is that with a class, the objects p1 and p2 would have pointed to the same object at the end of the program.

4.6. Interfaces

An interface is a set of method or property prototypes that forms a contract. A class that decides to implement an interface commits to providing an implementation of all the methods defined in the interface. The compiler verifies this implementation.

Here, for example, is the definition of the System.Collections.IEnumerator interface:

public interface System.Collections.IEnumerator 
{

    // Properties
    Object Current { get; }

    // Methods
    bool MoveNext();
    void Reset();
}

The properties and methods of the interface are defined only by their signatures. They are not implemented (have no code). It is the classes that implement the interface that provide code for the interface's methods and properties.

1
2
3
4
5
6
public class C : IEnumerator{
    ...
    Object Current{ get {...}}
    bool MoveNext{...}
    void Reset(){...}
}
  • Line 1: Class C implements the IEnumerator interface. Note that the colon (:) used for implementing an interface is the same as the one used for class inheritance.
  • Lines 3–5: The implementation of the methods and properties of the IEnumerator interface.

Consider the following interface:


namespace Chap2 {
    public interface IStats {
        double Mean { get; }
        double StandardDev();
    }
}

The IStats interface provides:

  • a read-only property Average: to calculate the average of a series of values
  • a StandardDev method: to calculate the standard deviation

Note that it is not specified anywhere which series of values is being referred to. It could be the average grades of a class, the monthly average sales of a particular product, the average temperature in a given location, etc. This is the principle of interfaces: we assume the existence of methods in the object but not that of specific data.

A first implementation class for the IStats interface could be a class used to store the grades of students in a class for a given subject. A student would be characterized by the following Student structure:


    public struct Student {
        public string LastName { get; set; }
        public string LastName { get; set; }
}//Student

The student would be identified by their last name and first name. Lines 2–3 contain the automatic properties for these two attributes.

A grade would be characterized by the following Grade structure:


    public struct Grade {
        public Student Student { get; set; }
        public double Value { get; set; }
}//Grade

The grade would be identified by the student being graded and the grade itself. Lines 2–3 contain the automatic properties for these two attributes.

The grades of all students in a given subject are collected in the following GradeTable class:


using System;
using System.Text;

namespace Chap2 {

    public class GradeTable : IStats {
        // attributes
        public string Subject { get; set; }
        public Note[] Notes { get; set; }
        public double Average { get; private set; }
        private double standardDeviation;

        // constructor
        public GradeArray(string subject, Grade[] grades) {
            // Storing via public properties
            Subject = subject;
            Grades = grades;
            // Calculate the average of the grades
            double sum = 0;
            for (int i = 0; i < Grades.Length; i++) {
                sum += Grades[i].Value;
            }
            if (Grades.Length != 0) Average = sum / Grades.Length;
            else Average = -1;
            // standard deviation
            double squared = 0;
            for (int i = 0; i < Notes.Length; i++) {
                squares += Math.Pow((Notes[i].Value - Average), 2);
            }//for
            if (Scores.Length != 0)
                standardDeviation = Math.Sqrt(squares / Notes.Length);
            else standardDeviation = -1;
        }//constructor

        public double StandardDeviation() {
            return standardDeviation;
        }

        // ToString
        public override string ToString() {
            StringBuilder value = new StringBuilder(String.Format("subject={0}, grades=(", Subject));
            int i;
            // concatenate all grades
            for (i = 0; i < Notes.Length - 1; i++) {
value.Append("[").Append(Grades[i].Student.FirstName).Append(",").Append(Grades[i].Student.LastName).Append(",").Append(Grades[i].Score).Append("],");
            };
            //last grade
            if (Notes.Length != 0) {
value.Append("[").Append(Grades[i].Student.FirstName).Append(",").Append(Grades[i].Student.LastName).Append(",").Append(Grades[i].Grade).Append("]");
            }
            value.Append(")");
            // end
            return value.ToString();
        }//ToString

    }//class
}
  • line 6: the TableauDeNotes class implements the IStats interface. It must therefore implement the Average property and the StandardDeviation method. These are implemented on lines 10 (Average) and 35–37 (StandardDeviation)
  • lines 8–10: three automatic properties
  • line 8: the subject for which the object stores grades
  • line 9: the array of student grades (Student, Grade)
  • line 10: the average of the grades—a property implementing the Average property of the IStats interface.
  • line 11: field storing the standard deviation of the grades—the associated get method StandardDev on lines 35–37 implements the StandardDev method of the IStats interface.
  • line 9: the grades are stored in an array. This array is passed to the constructor in lines 14–33 when the GradeArray class is instantiated.
  • Lines 14–33: the constructor. Here, we assume that the scores passed to the constructor will not change thereafter. Therefore, we use the constructor to immediately calculate the mean and standard deviation of these scores and store them in the fields on lines 10–11. The mean is stored in the private field underlying the automatic property Moyenne in line 10, and the standard deviation in the private field in line 11.
  • Line 10: The get method of the automatic property Average will return the underlying private field.
  • Lines 35–37: The `EcartType` method returns the value of the private field from line 11.

There are a few subtleties in this code:

  • Line 23: The set method of the Moyenne property is used to perform the assignment. This method was declared private on line 10 so that assigning a value to the Moyenne property is only possible within the class.
  • Lines 40–54: use a StringBuilder object to construct the string representing the TableauDeNotes object in order to improve performance. It should be noted that code readability suffers significantly as a result. This is the flip side of the coin.

In the previous class, grades were stored in an array. It was not possible to add a new grade after the TableauDeNotes object was constructed. We now propose a second implementation of the IStats interface, called ListeDeNotes, where grades are stored in a list, allowing grades to be added after the initial construction of the ListeDeNotes object.

The code for the GradeList class is as follows:


using System;
using System.Text;
using System.Collections.Generic;

namespace Chap2 {

    public class NoteList : IStats {
        // properties
        public string Subject { get; set; }
        public List<Grade> Grades { get; set; }
        public double average = -1;
        public double standardDeviation = -1;

        // constructor
        public ListOfGrades(string subject, List<Grade> grades) {
            // Storing via public properties
            Subject = subject;
            Notes = notes;
        }//constructor

        // Add a grade
        public void Add(Grade grade) {
            // add the grade
            Notes.Add(note);
            // reset average and standard deviation
            mean = -1;
            standardDev = -1;
        }

        // ToString
        public override string ToString() {
            StringBuilder value = new StringBuilder(String.Format("subject={0}, grades=(", Subject));
            int i;
            // concatenate all grades
            for (i = 0; i < Grades.Count - 1; i++) {
value.Append("[").Append(Grades[i].Student.FirstName).Append(",").Append(Grades[i].Student.LastName).Append(",").Append(Grades[i].Score).Append("],");
            };
            //last grade
            if (Notes.Count != 0) {
value.Append("[").Append(Grades[i].Student.FirstName).Append(",").Append(Grades[i].Student.LastName).Append(",").Append(Grades[i].Score).Append("]");
            }
            value.Append(")");
            // end
            return value.ToString();
        }//ToString

        // average of grades
        public double Average {
            get {
                if (average != -1) return average;
                // Calculate the average of the scores
                double sum = 0;
                for (int i = 0; i < Grades.Count; i++) {
                    sum += Grades[i].Value;
                }
                // Calculate the average
                if (Scores.Count != 0) average = sum / Scores.Count;
                return average;
            }
        }

        public double StandardDeviation() {
            // standard deviation
            if (standardDev != -1) return standardDev;
            // mean
            double mean = Mean;
            double sumsOfSquares = 0;
            for (int i = 0; i < Scores.Count; i++) {
                squares += Math.Pow((Scores[i].Value - average), 2);
            }//for
            // calculate the standard deviation
            if (Notes.Count != 0)
                standardDev = Math.Sqrt(squares / Notes.Count);
            return standardDeviation;
        }
    }//class
}
  • line 7: the GradeList class implements the IStats interface
  • line 10: grades are now stored in a list rather than an array
  • line 11: the automatic Average property of the GradeArray class has been dropped here in favor of a private field average, line 11, associated with the read-only public property Average in lines 48–60
  • lines 22–28: we can now add a grade to those already stored, which was not possible previously.
  • Lines 15–19: Consequently, the average and standard deviation are no longer calculated in the constructor but in the interface methods themselves: Average (lines 48–60) and StandardDeviation (62–76). However, the recalculation is only triggered if the mean and standard deviation are different from -1 (lines 50 and 64).

A test class could look like this:


using System;
using System.Collections.Generic;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // Some students & English grades
            Student[] students1 = { new Student { FirstName = "Paul", LastName = "Martin" }, new Student { FirstName = "Maxime", LastName = "Germain" }, new Student { FirstName = "Berthine", LastName = "Samin" } };
            Grade[] grades1 = { new Grade { Student = students1[0], Score = 14 }, new Grade { Student = students1[1], Score = 16 }, new Grade { Student = students1[2], Score = 18 } };
            // which we store in a GradesArray object
            GradesTable English = new GradesTable("English", grades1);
            // display average and standard deviation
            Console.WriteLine("{2}, Average={0}, StandardDeviation={1}", English.Average, English.StandardDeviation(), English);
            // we put the students and the subject into a GradesList object
            GradesList French = new GradesList("French", new List<Grade>(grades1));
            // Display average and standard deviation
            Console.WriteLine("{2}, Average={0}, StandardDeviation={1}", French.Average, French.StandardDeviation(), French);
            // add a grade
            French.Add(new Note { Student = new Student { FirstName = "Jérôme", LastName = "Jaric" }, Score = 10 });
            // display average and standard deviation
            Console.WriteLine("{2}, Mean={0}, Standard Deviation={1}", French.Mean, French.StandardDeviation(), French);
        }
    }
}
  • Line 8: Creating an array of students using the parameterless constructor and initializing via public properties
  • line 9: creation of an array of grades using the same technique
  • line 11: a GradeArray object for which the mean and standard deviation are calculated line 13
  • line 15: a GradeList object for which the mean and standard deviation are calculated line 17. The List<Grade> class has a constructor that accepts an object implementing the IEnumerable<Grade> interface. The grades1 array implements this interface and can be used to construct the List<Grade> object.
  • line 19: adding a new note
  • line 21: recalculating the mean and standard deviation

The results of the execution are as follows:

1
2
3
subject=English, grades=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Average=16, Standard Deviation=1.63299316185545
subject=French, grades=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Mean=16, Standard deviation=1.63299316185545
subject=French, grades=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18],[Jérôme,Jaric,10]), Mean=14.5, Standard Deviation=2.95803989154981

In the previous example, two classes implement the IStats interface. That said, the example does not demonstrate the value of the IStats interface. Let’s rewrite the test program as follows:


using System;
using System.Collections.Generic;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // some students & English grades
            Student[] students1 = { new Student { FirstName = "Paul", LastName = "Martin" }, new Student { FirstName = "Maxime", LastName = "Germain" }, new Student { FirstName = "Berthine", LastName = "Samin" } };
            Grade[] grades1 = { new Grade { Student = students1[0], Score = 14 }, new Grade { Student = students1[1], Score = 16 }, new Grade { Student = students1[2], Score = 18 } };
            // which we store in a GradesArray object
            GradesTable English = new GradesTable("English", grades1);
            // Display mean and standard deviation
            DisplayStats(English);
            // we put the students and the subject into a GradeList object
            GradesList French = new GradesList("French", new List<Grade>(grades1));
            // Display average and standard deviation
            DisplayStats(French);
            // add a grade
            French.Add(new Note { Student = new Student { FirstName = "Jérôme", LastName = "Jaric" }, Score = 10 });
            // display average and standard deviation
            DisplayStats(French);
        }

        // Display the mean and standard deviation of an IStats type
        static void DisplayStats(IStats values) {
            Console.WriteLine("{2}, Mean={0}, StandardDeviation={1}", values.Mean, values.StandardDeviation(), values);
        }
    }
}
  • Lines 25–27: The static method DisplayStats takes an IStats type as a parameter, which is an interface type. This means that the actual parameter can be any object that implements the IStats interface. When using data of an interface type, this means that only the interface methods implemented by the data will be used. The rest is ignored. This is a property similar to the polymorphism seen with classes. If a set of classes Ci that are not related to each other by inheritance (and thus cannot use inheritance polymorphism) has a set of methods with the same signature, it may be useful to group these methods into an interface I that all the relevant classes would implement. Instances of these classes Ci can then be used as actual parameters of functions that accept a formal parameter of type I, i.e., functions that use only the methods of the Ci objects defined in the interface I and not the specific attributes and methods of the various Ci classes.
  • line 13: the AfficheStats method is called with a TableauDeNotes type that implements the IStats interface
  • line 17: same as above, but with a type called GradeList

The results of the execution are identical to those of the previous one.

A variable can be of the type of an interface. Thus, we can write:

1
2
3
IStats stats1 = new TableauDeNotes(...);
...
stats1 = new ListOfNotes(...);

The declaration on line 1 indicates that stats1 is an instance of a class that implements the IStats interface. This declaration means that the compiler will only allow access to the interface’s methods within stats1: the Moyenne property and the EcartType method.

Finally, note that interfaces can be implemented multiple times, i.e., we can write

public class DerivedClass : BaseClass, I1, I2, ..., In {
...
}

where the Ij are interfaces.

4.7. Abstract classes

An abstract class is a class that cannot be instantiated. You must create derived classes that can be instantiated.

Abstract classes can be used to factor out code across a family of classes. Let’s examine the following case:


using System;

namespace Chap2 {
    abstract class User {
        // fields
        private string login;
        private string password;
        private string role;

        // constructor
        public User(string login, string password) {
            // save the information
            this.login = login;
            this.password = password;
            // authenticate the user
            role = identify();
            // Is the user identified?
            if (role == null) {
                throw new UnknownUserException(String.Format("[{0},{1}]", login, password));
            }
        }

        // toString
        public override string ToString() {
            return String.Format("User[{0},{1},{2}]", login, password, role);
        }

        // identify
        abstract public string identify();
    }
}
  • lines 11–21: the constructor of the User class. This class stores information about a web application’s user. The application has various types of users authenticated via a username and password (lines 6–7). These credentials are verified against an LDAP service for some users, against a database management system for others, and so on...
  • lines 13-14: the authentication information is stored
  • Line 16: It is verified by an `identify` method. Because the identification method is not yet defined, it is declared abstract on line 29 using the `abstract` keyword. The `identify` method returns a string specifying the user’s role (basically, what they are authorized to do). If this string is a null pointer, an exception is thrown on line 19.
  • Line 4: Because it has an abstract method, the class itself is declared abstract using the abstract keyword.
  • Line 29: The abstract method `identify` has no definition. Derived classes will provide one.
  • Lines 24–26: The ToString method, which identifies an instance of the class.

Here, we assume that the developer wants to control the construction of instances of the User class and derived classes, perhaps because they want to ensure that an exception of a certain type is thrown if the user is not recognized (line 19). Derived classes will be able to rely on this constructor. To do so, they must provide the identify method.

The UnknownUserException class is as follows:


using System;

namespace Chap2 {
    class UnknownUserException : Exception {
        public ExceptionUtilisateurInconnu(string message) : base(message){
        }
    }
}
  • Line 3: It derives from the Exception class
  • lines 4–6: It has a single constructor that takes an error message as a parameter. This is passed to the parent class (line 5), which has the same constructor.

We now derive the User class from the Administrator child class:


namespace Chap2 {
    class Administrator : User {
        // constructor
        public Administrator(string login, string password)
            : base(login, password) {
        }

        // identify
        public override string identify() {
            // LDAP authentication
            // ...
            return "admin";
        }
    }
}
  • lines 4–6: the constructor simply passes the parameters it receives to its parent class
  • lines 9-12: the identify method of the Administrator class. We assume that an administrator is identified by an LDAP system. This method overrides the identify method of its parent class. Because it overrides an abstract method, there is no need to use the override keyword.

We now derive the User class from the Observer child class:


namespace Chap2 {
    class Observer : User{
        // constructor
        public Observer(string login, string password)
            : base(login, password) {
        }

        //identify
        public override string identify() {
            // DB identification
            // ...
            return "observer";
        }
        
    }
}
  • lines 4–6: the constructor simply passes the parameters it receives to its parent class
  • lines 9–13: the `identify` method of the Observer class. We assume that an observer is identified by verifying their identification data in a database.

Ultimately, the Administrator and Observer objects are instantiated by the same constructor, that of the parent User class. This constructor will use the identify method provided by these classes.

A third class, Unknown, also derives from the User class:


namespace Chap2 {
    class Unknown : User{

        // constructor
        public Unknown(string login, string password)
            : base(login, password) {
        }

        //identify
        public override string identify() {
            // user not known
            // ...
            return null;
        }
        
    }
}
  • Line 13: The `identify` method sets the pointer to `null` to indicate that the user was not recognized.

A test program might look like this:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Observer("observer", "mdp1"));
            Console.WriteLine(new Administrator("admin", "mdp2"));
            try {
                Console.WriteLine(new Unknown("xx", "yy"));
            } catch (UnknownUserException e) {
                Console.WriteLine("Unknown user: " + e.Message);
            }
        }
    }
}

Note that in lines 6, 7, and 9, the [User].ToString() method is used by the WriteLine method.

The results of the execution are as follows:

1
2
3
User[observer,mdp1,observer]
User[admin,mdp2,admin]
Unknown user: [xx,yy]

4.8. Classes, interfaces, generic methods

Suppose we want to write a method that swaps two integers. This method could be as follows:


        public static void Swap1(ref int value1, ref int value2){
            // swap the references value1 and value2
            int temp = value2;
            value2 = value1;
            value1 = temp;
}

Now, if we wanted to swap two references to Person objects, we would write:


        public static void Swap2(ref Person value1, ref Person value2){
            // swap the references value1 and value2
            Person temp = value2;
            value2 = value1;
            value1 = temp;
}

What distinguishes the two methods is the type T of the parameters: int in Swap1, Person in Swap2. Generic classes and interfaces address the need for methods that differ only in the type of some of their parameters.

With a generic class, the Swap method could be rewritten as follows:


namespace Chap2 {
    class Generic1<T> {
        public static void Swap(ref T value1, ref T value2){
            // swap the references value1 and value2
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • Line 2: The Generic1 class is parameterized by a type denoted T. You can give it any name you want. This type T is then reused in the class on lines 3 and 5. We say that the Generic1 class is a generic class.
  • line 3: defines the two references to a type T to be swapped
  • line 5: the temporary variable temp is of type T.

A test program for the class could look like this:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            // int
            int i1 = 1, i2 = 2;
            Generic1<int>.Swap(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // string
            string s1 = "s1", s2 = "s2";
            Generic1<string>.Swap(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
            // Person
            Person p1 = new Person("jean", "clu", 20), p2 = new Person("pauline", "dard", 55);
            Generic1<Person>.Swap(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);

        }
    }
}
  • Line 8: When using a generic class parameterized by types T1, T2, ... these types must be "instantiated." On line 8, we use the static Swap method of type Generic1<int> to indicate that the references passed to the Swap method are of type int.
  • Line 12: We use the static Echanger method of type Generic1<string> to indicate that the references passed to the Echanger method are of type string.
  • Line 16: We use the static method `Echanger` of type `Generic1<Personne>` to indicate that the references passed to the `Echanger` method are of type `Personne`.

The results of the execution are as follows:

1
2
3
i1=2,i2=1
s1=s2, s2=s1
p1=[pauline, dard, 55],p2=[jean, clu, 20]

The *Echanger* method could also have been written as follows:


namespace Chap2 {
    class Generic2 {
        public static void Swap<T>(ref T value1, ref T value2){
            // swap the references value1 and value2
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • line 2: the Generic2 class is no longer generic
  • line 3: the static method Echanger is generic

The test program then becomes the following:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // int
            int i1 = 1, i2 = 2;
            Generic2.Swap<int>(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // string
            string s1 = "s1", s2 = "s2";
            Generic2.Swap<string>(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
            // Person
            Person p1 = new Person("jean", "clu", 20), p2 = new Person("pauline", "dard", 55);
            Generic2.Swap<Person>(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);
        }
    }
}
  • Lines 8, 12, and 16: The Swap method is called by specifying the parameter types within <>. In fact, the compiler can deduce which variant of the Swap method to use based on the actual parameter types. Therefore, the following syntax is valid:

            Generic2.Swap(ref i1, ref i2);
...
            Generic2.Swap(ref s1, ref s2);
...
            Generic2.Swap(ref p1, ref p2);

Lines 1, 3, and 5: the variant of the Swap method being called is no longer specified. The compiler is able to infer it from the nature of the actual parameters used.

Constraints can be placed on generic parameters:

Image

Consider the following new generic Swap method:


namespace Chap2 {
    class Generic3 {
        public static void Swap<T>(ref T value1, ref T value2) where T : class {
            // swap the references value1 and value2
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • Line 3: Type T must be a reference (class, interface)

Consider the following test program:


using System;

namespace Chap2 {
    class Program4 {
        static void Main(string[] args) {
            // int
            int i1 = 1, i2 = 2;
            Generic3.Swap<int>(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // string
            string s1 = "s1", s2 = "s2";
            Generic3.Swap(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
            // Person
            Person p1 = new Person("jean", "clu", 20), p2 = new Person("pauline", "dard", 55);
            Generic3.Swap(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);

        }
    }
}

The compiler reports an error on line 8 because the type int is not a class or an interface; it is a struct:

Image

Consider the following new generic method Exchange:


namespace Chap2 {
    class Generic4 {
        public static void Swap<T>(ref T element1, ref T element2) where T : Interface1 {
            // retrieve the values of the two elements
            int value1 = element1.Value();
            int value2 = element2.Value();
            // if the first element is greater than the second element, swap the elements
            if (value1 > value2) {
                T temp = element2;
                element2 = element1;
                element1 = temp;
            }
        }
    }
}
  • line 3: The type T must implement the Interface1 interface. This interface has a Value method, used in lines 5 and 6, which returns the value of the object of type T.
  • lines 8–12: the two references element1 and element2 are swapped only if the value of element1 is greater than the value of element2.

The Interface1 interface is as follows:


namespace Chap2 {
    interface Interface1 {
        int Value();
    }
}

It is implemented by the following Class1 class:


using System;
using System.Threading;

namespace Chap2 {
    class Class1 : Interface1 {
        // object value
        private int value;

        // constructor
        public Class1() {
            // wait 1 ms
            Thread.Sleep(1);
            // random value between 0 and 99
            value = new Random(DateTime.Now.Millisecond).Next(100);
        }

        // private field accessor value
        public int Value() {
            return value;
        }

        // instance state
        public override string ToString() {
            return value.ToString();
        }
    }
}
  • line 5: Class1 implements the Interface1 interface
  • line 7: the value of an instance of Class1
  • lines 10–14: the value field is initialized with a random value between 0 and 99
  • lines 18–20: the Value method of the Interface1 interface
  • lines 23–25: the ToString method of the class

The Interface1 interface is also implemented by the Class2 class:


using System;

namespace Chap2 {
    class Class2 : Interface1 {
        // object values
        private int value;
        private String s;

        // constructor
        public Class2(String s) {
            this.s = s;
            value = s.Length;
        }

        // private field accessor value
        public int Value() {
            return value;
        }

        // instance state
        public override string ToString() {
            return s;
        }
    }
}
  • line 4: Class2 implements the Interface1 interface
  • line 6: the value of an instance of Class2
  • lines 10–13: the value field is initialized with the length of the string passed to the constructor
  • lines 16–18: the Value method of the Interface1 interface
  • lines 21–22: the ToString method of the class

A test program might look like this:


using System;

namespace Chap2 {
    class Program5 {
        static void Main(string[] args) {
            // exchange instances of type Class1
            Class1 c1, c2;
            for (int i = 0; i < 5; i++) {
                c1 = new Class1();
                c2 = new Class1();
                Console.WriteLine("Before swap --> c1={0},c2={1}", c1, c2);
                Generic4.Swap(ref c1, ref c2);
                Console.WriteLine("After swap --> c1={0},c2={1}", c1, c2);
            }
            // swap instances of type Class2
            Class2 c3, c4;
            c3 = new Class2("xxxxxxxxxxxxxx");
            c4 = new Class2("xx");
            Console.WriteLine("Before swap --> c3={0},c4={1}", c3, c4);
            Generic4.Swap(ref c3, ref c4);
            Console.WriteLine("Before swap --> c3={0},c4={1}", c3, c4);
        }
    }
}
  • lines 8–14: instances of type Class1 are swapped
  • lines 16–22: instances of type Class2 are swapped

The results of the execution are as follows:

Before swap --> c1=43, c2=79
After swap --> c1=43, c2=79
Before swap --> c1=72, c2=56
After swap --> c1=56, c2=72
Before swap --> c1=92, c2=75
After swap --> c1=75, c2=92
Before swap --> c1=11, c2=47
After swap --> c1=11, c2=47
Before swap --> c1=31, c2=67
After swap --> c1=31,c2=67
Before swap --> c3=xxxxxxxxxxxxxx,c4=xx
After swap --> c3=xx,c4=xxxxxxxxxxxxxx

To illustrate the concept of a gener e interface, we will sort an array of people first by their names, then by their ages. The method that allows us to sort an array is the static Sort method of the Array class:

Image

Recall that a static method is used by prefixing the method with the class name, not the name of an instance of the class. The Sort method has different signatures (it is overloaded). We will use the following signature:

public static void Sort<T>(T[] array, IComparer<T> comparator)

Sort is a generic method where T denotes any type. The method takes two parameters:

  • T[] array: the array of elements of type T to be sorted
  • IComparer<T> comparator: a reference to an object implementing the IComparer<T> interface.

IComparer<T> is a generic interface defined as follows:

1
2
3
public interface IComparer<T>{
    int Compare(T t1, T t2);
}

The IComparer<T> interface has only a single method: the Compare method:

  • takes two elements t1 and t2 of type T as parameters
  • returns 1 if t1 > t2, 0 if t1 == t2, and -1 if t1 < t2. It is up to the developer to define the meaning of the operators <, ==, and >. For example, if p1 and p2 are two Person objects, we can say that p1 > p2 if p1’s name comes before p2’s name in alphabetical order. We will then have a sort in ascending order by the people's names. If we want a sort by age, we will say that p1 > p2 if the age of p1 is greater than the age of p2.
  • To sort in descending order, simply reverse the results of +1 and -1

We now know enough to sort an array of people. The program is as follows:


using System;
using System.Collections.Generic;

namespace Chap2 {
    class Program6 {
        static void Main(string[] args) {
            // an array of people
            Person[] people1 = { new Person("claude", "pollon", 25), new Person("valentine", "germain", 35), new Person("paul", "germain", 32) };
            // display
            Display("Array to sort", people1);
            // Sort by name
            Array.Sort(people1, new CompareNames());
            // display
            Display("Array after sorting by first and last name", people1);
            // Sort by age
            Array.Sort(people1, new CompareAges());
            // display
            Display("Array after sorting by age", people1);
        }

        static void Display(string text, Person[] people) {
            Console.WriteLine(text.PadRight(50, '-'));
            foreach (Person p in people) {
                Console.WriteLine(p);
            }
        }
    }

    // class for comparing people's first and last names
    class CompareNames : IComparer<Person> {
        public int Compare(Person p1, Person p2) {
            // compare the names
            int i = p1.Name.CompareTo(p2.Name);
            if (i != 0)
                return i;
            // If the last names are equal, compare the first names
            return p1.FirstName.CompareTo(p2.FirstName);
        }
    }

    // class for comparing people's ages
    class CompareAges : IComparer<Person> {
        public int Compare(Person p1, Person p2) {
            // compare ages
            if (p1.Age > p2.Age)
                return 1;
            else if (p1.Age == p2.Age)
                return 0;
            else
                return -1;
        }
    }

}
  • line 8: the array of people
  • line 12: sorting the array of people by first and last name. The second parameter of the generic Sort method is an instance of the CompareNames class, which implements the generic interface IComparer<Person>.
  • lines 30–39: the CompareNames class implementing the generic interface IComparer<Person>.
  • lines 31–38: implementation of the generic method int CompareTo(T, T) of the IComparer<T> interface. The method uses the String.CompareTo method, presented in section 3.3.5.4, to compare two strings.
  • Line 16: Sorting the array of people by age. The second parameter of the generic Sort method is an instance of the CompareAges class, which implements the generic IComparer<Person> interface and is defined on lines 42–51.

The results of the execution are as follows:

Array to be sorted-----------------------------------
[claude, pollon, 25]
[valentine, germain, 35]
[paul, germain, 32]
Array after sorting by first and last name------
[Paul, Germain, 32]
[Valentine, Germain, 35]
[Claude, Pollon, 25]
Table after sorting by age------------------
[claude, pollon, 25]
[Paul, Germain, 32]
[Valentine, Germain, 35]

4.9. Namespaces

To write a line on the screen, we use the statement

Console.WriteLine(...)

If we look at the definition of the Console class


Namespace: System
Assembly: Mscorlib (in Mscorlib.dll)

we see that it is part of the System namespace. This means that the Console class should be referred to as System.Console, and we should actually write:

System.Console.WriteLine(...)

We avoid this by using a using clause:

using System;
...
Console.WriteLine(...)

We say that we import the System namespace using the using clause. When the compiler encounters the name of a class (here, Console), it will look for it in the various namespaces imported by the using clauses. Here, it will find the Console class in the System namespace. Now let’s note the second piece of information associated with the Console class:

Assembly: Mscorlib (in Mscorlib.dll)

This line indicates in which "assembly" the definition of the Console class is located. When compiling outside of Visual Studio and you need to specify the references to the various DLLs containing the classes you need to use, this information can be useful. To reference the DLLs required to compile a class, you write:

csc /r:fic1.dll /r:fic2.dll ... prog.cs

where csc is the C# compiler. When creating a class, you can create it within a namespace. The purpose of these namespaces is to avoid name conflicts between classes, for example when they are sold. Consider two companies, E1 and E2, distributing classes packaged in the DLLs e1.dll and e2.dll, respectively. Suppose a client C purchases these two sets of classes, in which both companies have defined a Person class. Client C compiles a program as follows:

csc /r:e1.dll /r:e2.dll prog.cs

If the source file prog.cs uses the Person class, the compiler will not know whether to use the Person class from e1.dll or the one from e2.dll. It will report an error. If company E1 takes care to create its classes in a namespace called E1 and company E2 in a namespace called E2, the two Person classes will then be named E1.Person and E2.Person. The client must use either E1.Person or E2.Person in its classes, but not Person. The namespace resolves the ambiguity.

To create a class in a namespace, write:

namespace Namespace{
    // Class definition
}

4.10. Example Application - V2

We will revisit the tax calculation already covered in the previous chapter, section 3.6, and now implement it using classes and interfaces. Let’s review the problem:

We aim to write a program to calculate a taxpayer’s tax. We consider the simplified case of a taxpayer who has only their salary to report (2004 figures for 2003 income):

  • We calculate the number of tax brackets for the employee as nbParts = nbEnfants/2 + 1 if they are unmarried, and nbEnfants/2 + 2 if they are married, where nbEnfants is the number of children.
  • if they have at least three children, they receive an additional half-share
  • We calculate their taxable income R = 0.72 * S, where S is their annual salary
  • We calculate their family coefficient QF = R / nbParts
  • we calculate his tax I. Consider the following table:
4262
0
0
8382
0.0683
291.09
14,753
0.1914
1,322.92
23,888
0.2826
2,668.39
38,868
0.3738
4,846.98
47,932
0.4262
6,883.66
0
0.4809
9505.54

Each row has 3 fields. To calculate tax I, find the first row where QF <= field1. For example, if QF = 5000, the row found will be

    8382        0.0683        291.09

Tax I is then equal to 0.0683*R - 291.09*nbParts. If QF is such that the condition QF<=field1 is never met, then the coefficients from the last row are used. Here:

    0                0.4809    9505.54

which gives tax I = 0.4809*R - 9505.54*nbParts.

First, we define a structure capable of encapsulating a row from the previous table:


namespace Chap2 {
    // a tax bracket
    struct TaxBracket {
        public decimal Limit { get; set; }
        public decimal TaxRate { get; set; }
        public decimal CoeffN { get; set; }
    }
}

Then we define an interface IImpot capable of calculating the tax:


namespace Chap2 {
    interface IImpot {
        int calculate(bool married, int numChildren, int salary);
    }
}
  • Line 3: the method for calculating the tax based on three pieces of data: the taxpayer's marital status, the number of children, and the salary

Next, we define an abstract class that implements this interface:


namespace Chap2 {
    abstract class AbstractTax : IImpot {

        // the tax brackets needed to calculate the tax
        // come from an external source

        protected TaxBracket[] taxBrackets;

        // tax calculation
        public int calculate(bool married, int numChildren, int salary) {
            // calculation of the number of shares
            decimal numberOfShares;
            if (married) nbParts = (decimal)nbChildren / 2 + 2;
            else nbParts = (decimal)nbChildren / 2 + 1;
            if (numberOfChildren >= 3) numberOfShares += 0.5M;
            // Calculate taxable income & family quotient
            decimal income = 0.72M * salary;
            decimal familyQuotient = income / nbParts;
            // calculate tax
            taxBrackets[taxBrackets.Length - 1].Limit = QF + 1;
            int i = 0;
            while (QF > taxBrackets[i].Limit) i++;
            // return result
            return (int)(income * taxBrackets[i].TaxRate - numberOfShares * taxBrackets[i].TaxRate);
        }//calculate
    }//class

}
  • Line 2: The AbstractImpot class implements the IImpot interface.
  • Line 7: The annual tax calculation data is stored in a protected field. The AbstractImpot class does not know how this field will be initialized. It leaves this task to the derived classes. This is why it is declared abstract (line 2) to prevent any instantiation.
  • Lines 10–25: The implementation of the `calculer` method from the `IImpot` interface. Derived classes will not need to rewrite this method. The `AbstractImpot` class thus serves as a factorization class for the derived classes. It contains what is common to all derived classes.

A class implementing the IImpot interface can be created by deriving from the AbstractImpot class. This is what we will do now:


using System;

namespace Chap2 {
    class HardwiredImpot : AbstractImpot {

        // data arrays required to calculate the tax
        decimal[] limits = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
        decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
        decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };

        public HardwiredImpot() {
                // Create the tax bracket array
            taxBrackets = new TaxBracket[limits.Length];
                // populate
            for (int i = 0; i < taxBrackets.Length; i++) {
                taxBrackets[i] = new TaxBracket { Limit = limits[i], RateR = rateR[i], RateN = rateN[i] };
                }
        }
    }// class
}// namespace

The HardwiredImpot class hard-codes the data needed to calculate the tax in lines 7–9. Its constructor (lines 11–18) uses this data to initialize the protected field taxBrackets of the parent class AbstractImpot.

A test program might look like this:


using System;

namespace Chap2 {
    class Program {
        static void Main() {
            // Interactive tax calculation program
            // the user enters three pieces of data via the keyboard: married, number of children, salary
            // the program then displays the tax due

            const string syntax = "syntax: Married No. of Children Salary\n"
                            + "Married: y for married, n for not married\n"
                            + "No. of Children: number of children\n"
                            + "Salary: annual salary in F";

            // Create an IImpot object
            IImpot tax = new HardwiredImpot();

            // infinite loop
            while (true) {
                // request tax calculation parameters
                Console.Write("Tax calculation parameters in the following format: Married (y/n) No. of children Salary or 'nothing' to exit:");
                string parameters = Console.ReadLine().Trim();
                // anything to do?
                if (parameters == null || parameters == "") break;
                // Check the number of arguments in the entered line
                string[] args = parameters.Split(null);
                int numParameters = args.Length;
                if (numberOfParameters != 3) {
                    Console.WriteLine(syntax);
                    continue;
                }//if
                // Checking parameter validity
                // married
                string married = args[0].ToLower();
                if (groom != "o" && groom != "n") {
                    Console.WriteLine(syntax + "\nInvalid 'married' argument: enter 'o' or 'n'");
                    continue;
                }//if
                // numberOfChildren
                int numberOfChildren = 0;
                bool dataOk = false;
                try {
                    nbChildren = int.Parse(args[1]);
                    dataOk = nbChildren >= 0;
                } catch {
                }//if
                // Is the data valid?
                if (!dataOk) {
                    Console.WriteLine(syntax + "\nInvalid NbEnfants argument: enter a positive integer or zero");
                    continue;
                }
                // salary
                int salary = 0;
                dataOk = false;
                try {
                    salary = int.Parse(args[2]);
                    dataOk = salary >= 0;
                } catch {
                }//try-catch
                // Is the data correct?
                if (!dataOk) {
                    Console.WriteLine(syntax + "\nIncorrect salary argument: enter a positive integer or zero");
                    continue;
                }
                // parameters are correct - calculate tax
                Console.WriteLine("Tax=" + tax.calculate(married == "o", numChildren, salary) + " euros");
                // next taxpayer
            }//while
        }
    }
}

The program above allows the user to perform repeated tax calculation simulations.

  • line 16: creation of an `impot` object implementing the `IImpot` interface. This object is obtained by instantiating a `HardwiredImpot` type, a type that implements the `IImpot` interface. Note that the variable `impot` was not assigned the `HardwiredImpot` type, but rather the `IImpot` type. By writing this, we indicate that we are only interested in the `calculer` method of the `impot` object and not the rest.
  • lines 19–68: the loop for tax calculation simulations
  • line 22: the three parameters required by the `calculer` method are entered in a single line typed on the keyboard.
  • line 26: the method [string].Split(null) breaks [string] down into words. These are stored in an array called args.
  • Line 66: Call to the `calculate` method of the `impot` object, which implements the `IImpot` interface.

Here is an example of the program in action:

Tax calculation parameters in the format: Married (y/n) No. of Children Salary or nothing to stop: q s d
Syntax: Married No. of Children Salary
Married: y for married, n for unmarried
No. of Children: number of children
Salary: annual salary in euros
Incorrect "married" argument: enter y or n
Tax calculation parameters in the format: Married (o/n) No. of Children Salary or nothing to stop: o 2 d
Syntax: Married No. of Children Salary
Married: o for married, n for unmarried
No. of children: number of children
Salary: annual salary in euros
Invalid salary argument: enter a positive integer or zero
Tax calculation parameters in the format: Married (y/n) No. of Children Salary or nothing to stop: q s d f
Syntax: Married No. of Children Salary
Married: y for married, n for unmarried
No. of children: number of children
Salary: annual salary in euros
Tax calculation parameters in the following format: Married (y/n) No. of children Salary or nothing to stop at: 2 60,000
Tax = 4,282 euros