Skip to content

4. Classes, Stuctures, Interfaces

4.1. The object by example

4.1.1. General

We now turn to object programming by way of example. An object is an entity containing data defining its state (called fields, attributes, ...) and functions (called methods). An object is created according to a model called a class:

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

From class C1 above, you can create numerous objects O1, O2,.. All will have fields p1, p2,.. and methods m3, m4, .. But they will have different values for their fields pi each with its own state. If o1 is an object of type C1, o1.p1 designates the property p1 from o1 and o1.m1 the method m1 from O1.

Let's consider a first object model: the Person.

4.1.2. Creating a C# project

In the previous examples, we only had a single : Program.cs. From now on, we'll be able to have several source files in a single project. We'll show you how.

In [1], create a new project. In [2], choose an Application Console. In [3], leave the default value. In [4], confirm. In [5], the generated project. The contents of Program.cs is 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 created project:

In [1], the option to be saved. In [2], select the folder in which 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 set of projects. In [4], give the solution a name. In [6], validate the save.

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

In [1], indicate that you want to add a class. In [2], enter the class name. In [3], validate 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 Personne {
    }
}

Change the namespace of each source file to Chap2 and eliminates the need to import unnecessary namespaces:


using System;
 
namespace Chap2 {
    class Personne {
    }
}

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

4.1.3. Definition of the Person class

Class definition Person in the source file [Personne.cs] will be as follows:


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // method
        public void Initialise(string P, string N, int age) {
            this.prenom = P;
            this.nom = N;
            this.age = age;
        }
 
         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

We have ici the definition of a class, i.e. a type of data. When we create variables of this type, we call them objects or authorities class. A class is therefore a mold from which objects are built.

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

privé
A private field can only be accessed by the class's internal methods
public
A public field can be accessed by any method, defined or not within the
protégé
A protected field (protected) can only be accessed by the internal methods of the class or a derived object (see the concept of inheritance later).

In general, class data is declared as 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
  • will be able to call on the object's public methods, and in particular those that provide access to its private data.

The syntax for declaring a C class is as follows:


public class C{
    private  donnée ou méthode ou propriété privée;
    public  donnée ou méthode ou propriété publique;
    protected  donnée ou méthode ou propriété protégée;
}

Attribute declaration order private, protected and public is arbitrary.

4.1.4. The Initialize method

Back to our class Person declared as :


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // method
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }
 
         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

What is the role of the Initializes ? Because name, first name and age are data private class Person, instructions :

Personne p1;
p1.prenom="Jean";
p1.nom="Dupont";
p1.age=30;

are illegal. We need to initialize an object of type Person via a public method. This is the role of the Initializes. We write :

Personne p1;
p1.Initialise("Jean","Dupont",30);

Writing p1.Initialise is legal because Initializes is open to the public.

4.1.5. The new operator

The instruction sequence

Personne p1;
p1.Initialise("Jean","Dupont",30);

is incorrect. The instruction

    Personne p1;

says p1 as a reference to an object of type Person. This object does not yet exist, so p1 is not initialized. It's like writing :

Personne p1=null;

where the keyword null that the variable p1 does not yet reference any object. When you then write

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

we use the Initializes of the object referenced by p1. Or this object does not yet exist and the compiler will signal the error. To ensure that p1 object reference, write :

Personne p1=new Personne();

This creates an object of type Person not yet initialized: attributes name and first name which are references to objects of type String will have the value null, and age the value 0. So there's a default initialization. Now that p1 refers to an object, the initialization instruction for this object

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

is valid.

4.1.6. The keyword this

Let's look at the code for the initialize :


        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
}

Instruction this.prenom=p means that the first name of the current object (this) receives the value p. The keyword this designates the current object: the one in which the executed method is located. How do we know this? Let's take a look at the initialization of the object referenced by p1 in the calling program :

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

This is the Initializes object p1 method. When this method references the this, on reference in fact the object p1. The method Initializes could also have been written as follows:


        public void Initialise(string p, string n, int age) {
            prenom = p;
            nom = n;
            this.age = age;
}

When an object's method references an attribute A of this object, writing this.A is implicit. It must be used explicitly when identifiers conflict. This is the case with the :


this.age=age;

where age designates an attribute of the current object and the parameter age received by the method. The ambiguity must then be resolved by designating the attribute age by this.age.

4.1.7. A test program

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


using System;
 
namespace Chap2 {
    class P01 {
        static void Main() {
            Personne p1 = new Personne();
            p1.Initialise("Jean", "Dupont", 30);
            p1.Identifie();
        }
    }
}

Before executing project [01], you may need to specify the source file to be executed:

In project properties [01], the class to be executed is indicated in [1].

The results obtained on completion are as follows:

[Jean, Dupont, 30]

4.1.8. Another method Initialise

Let's consider the Person and add the following method:


        public void Initialise(Personne p) {
            prenom = p.prenom;
            nom = p.nom;
            age = p.age;
}

We now have two methods with the name Initializes : this is legal as long as they admit different parameters. This is the case with ici. The parameter is now a reference p to a person. Person attributes p are then assigned to the current object (this). Note that the Initializes has direct access to object attributes p although these are of the private. This is always true: an object o1 a class C always has access to the attributes of objects of the same class C.

Here's a test of the new P classerson :


using System;
 
namespace Chap2 {
    class Program {
        static void Main() {
            Personne p1 = new Personne();
            p1.Initialise("Jean", "Dupont", 30);
            p1.Identifie();
            Personne p2 = new Personne();
            p2.Initialise(p1);
            p2.Identifie();
        }
    }
}

and its results:

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

4.1.9. Constructors of the Person class

A constructor is a method named after the class and called when the object is created. It is generally used to initialize the object. It can accept arguments, but does not return any results. Its prototype or definition are not preceded by any type (not even void).

If a C class has a constructor accepting n arguments argi, the declaration and initialization of an object of this class can be done in the form :

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

or

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

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

    Personne p1;
    p1=new Personne();

Let's create two constructors for our class Person :


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
        }
        public Personne(Personne P) {
            Initialise(P);
        }
 
         // method
        public void Initialise(string p, string n, int age) {
...
        }
 
        public void Initialise(Personne p) {
...
        }
 
         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

Our two builders simply use the Initializes previously studied. Remember that when a manufacturer uses the notation Initialise(p) for example, the compiler translates this.Initialise(p). In the constructor, the Initializes is called up to work on the object referenced by this, i.e. the current object, the one under construction.

Here is a short test program:


using System;
 
namespace Chap2 {
    class Program {
        static void Main() {
            Personne p1 = new Personne("Jean", "Dupont", 30);
            p1.Identifie();
            Personne p2 = new Personne(p1);
            p2.Identifie();
        }
    }
}
 

and the results obtained:

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

4.1.10. Object references

We always use the same Person. The test program becomes :


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

The results are as follows:

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

When declaring the variable p1 by

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

p1 object reference Pperson("John", "Smith",30) but is not the object itself. In C, we'd say it's a pointer, c.a.d. the address of the object created. If you then write :

    p1=null;

This is not the purpose Person("John", "Smith",30) which is modified, is the reference p1 which changes value. The object Person("John", "Smith",30) will be "lost" if it is not referenced by any other variable.

When we write :

Personne p2=p1;

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

When we write :

Personne p3=new Personne(p1);

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

4.1.11. Passing object reference parameters

In the previous chapter, we looked at 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 a :


using System;
using System.Text;
 
namespace Chap1 {
    class P12 {
        public static void Main() {
             // example 4
            StringBuilder sb0 = new StringBuilder("essai0"), sb1 = new StringBuilder("essai1"), sb2 = new StringBuilder("essai2"), sb3;
            Console.WriteLine("Dans fonction appelante avant appel : sb0={0}, sb1={1}, sb2={2}", sb0,sb1, sb2);
            ChangeStringBuilder(sb0, sb1, ref sb2, out sb3);
            Console.WriteLine("Dans fonction appelante après appel : 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("Début fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}", sbf0,sbf1, sbf2);
            sbf0.Append("*****");
            sbf1 = new StringBuilder("essai1*****");
            sbf2 = new StringBuilder("essai2*****");
            sbf3 = new StringBuilder("essai3*****");
            Console.WriteLine("Fin fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3);
        }
    }
}
  • line 8: defines 3 StringBuilder. An object StringBuilder is close to an object string. When handling an object string, you get a new object in return string. So in the code sequence :
string s="une chaîne";
s=s.ToUpperCase();

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

  • line 8: 4 references [sb0, sb1, sb2, sb3] to objects of type StringBuilder
  • line 10: are passed to the ChangeStringBuilder with different modes: sb0, sb1 with the default mode, sb2 with the keyword ref, sb3 with the keyword out.
  • lines 15-22: a method with formal parameters [sbf0, sbf1, sbf2, sbf3]. Relationships between formal parameters sbfi and workforce sbi are as follows:
  • sbf0 and sb0 are, at the start of the method, two references separate that point to the same object (address value passing)
  • ditto for sbf1 and sb1
  • sbf2 and sb2 are, at the start of the method, a same reference on the same object (keyword ref)
  • sbf3 and sb3 are, after execution of the method, a same reference on the same object (keyword out)

The results are as follows:

1
2
3
4
Dans fonction appelante avant appel : sb0=essai0, sb1=essai1, sb2=essai2
Début fonction appelée : sbf0=essai0, sbf1=essai1, sbf2=essai2
Fin fonction appelée : sbf0=essai0*****, sbf1=essai1*****, sbf2=essai2*****, sbf3=essai3*****
Dans fonction appelante après appel : sb0=essai0*****, sb1=essai1, sb2=essai2*****, sb3=essai3*****

Explanations:

  • sb0 and sbf0 are two separate references to the same object. This one has been modified via sbf0 - line 3. This modification can be viewed via sb0 - line 4.
  • sb1 and sbf1 are two distinct references to the same object. sbf1 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 on the same object. sbf2 is modified in the method and now points to a new object - line 3. As sbf2 and sb2 are a single entity, the value of sb2 has also been modified and sb2 points to the same object as sbf2 - lines 3 and 4.
  • before calling the method, sb3 was worthless. 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, we can explicitly call up the constructor of an object: it's built, but we don't have access to it (to modify it, for example). This temporary object is built for the purposes of evaluating the expression, then abandoned. The memory space it occupies is automatically reclaimed later by a program called "garbage collector", whose role is to reclaim memory space occupied by objects no longer referenced by program data.

Consider the following new test program:


using System;
 
namespace Chap2 {
    class Program {
        static void Main() {
            new Personne(new Personne("Jean", "Dupont", 30)).Identifie();
        }
    }
}

and modify the constructors of the Person to display a message:


         // manufacturers
        public Personne(String p, String n, int age) {
            Console.WriteLine("Constructeur Personne(string, string, int)");
            Initialise(p, n, age);
        }
        public Personne(Personne P) {
            Console.Out.WriteLine("Constructeur Personne(Personne)");
            Initialise(P);
}

We obtain the following results:

1
2
3
Constructeur Personne(string, string, int)
Constructeur Personne(Personne)
[Jean, Dupont, 30]

showing the successive construction of the two temporary objects.

4.1.13. Methods for reading and writing private attributes

We add to the Person methods for reading or modifying the state of object attributes:


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String p, String n, int age) {
            Console.WriteLine("Constructeur Personne(string, string, int)");
            Initialise(p, n, age);
        }
        public Personne(Personne p) {
            Console.Out.WriteLine("Constructeur Personne(Personne)");
            Initialise(p);
        }
 
         // method
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }
 
        public void Initialise(Personne p) {
            prenom = p.prenom;
            nom = p.nom;
            age = p.age;
        }
 
         // accessors
        public String GetPrenom() {
            return prenom;
        }
        public String GetNom() {
            return nom;
        }
        public int GetAge() {
            return age;
        }
 
         //modifiers
        public void SetPrenom(String P) {
            this.prenom = P;
        }
        public void SetNom(String N) {
            this.nom = N;
        }
        public void SetAge(int age) {
            this.age = age;
        }
 
         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

We test the new class with the following program:


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

and we obtain the results :

1
2
3
Constructeur Personne(string, string, int)
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)

4.1.14. The properties

Another way of accessing the attributes of a class is to create properties. These allow us to manipulate private attributes as if they were public.

Consider the P classerson where previous accessors and modifiers have been replaced by properties in reading and writing :


using System;
 
namespace Chap2 {
    public class Personne {
         // attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
        }
        public Personne(Personne p) {
            Initialise(p);
        }
 
         // method
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }
 
        public void Initialise(Personne p) {
            prenom = p.prenom;
            nom = p.nom;
            age = p.age;
        }
 
         // properties
        public string Prenom {
            get { return prenom; }
            set {
                 // valid first name?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
             }//if
         }//first name
 
        public string Nom {
            get { return nom; }
            set {
                 // valid name?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("nom (" + value + ") invalide");
                } else { nom = value; }
             }//if
         }//name
 
 
        public int Age {
            get { return age; }
            set {
                 // valid age?
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("âge (" + value + ") invalide");
             }//if
         }//age

         // method
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }
 
}

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

public Type Propriété{
    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 method get is usually responsible for rendering the value of the attribute it manages (it could render something else, nothing prevents it). The set receives a parameter called value which it normally assigns to the attribute it manages. It can take advantage of this to check the validity of the value received and, if necessary, throw an exception if the value turns out to be invalid. This is what ici does.

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


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

In the


    Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");

we are looking for the values of the Prenown, Nom and Age of the person p. This is the get of these properties, which is then called and returns the value of the attribute it manages.

In the

        p.Age=56;

we want to set the value of property Age. This is the set which is then called. It will receive 56 in its parameter value.

A property P a class C which would only define the get is said to be read-only. If c is a class C object, the operation c.P=valeur will then be refused by the compiler.

Execution of the previous test program gives the following results:

1
2
3
p=(Jean,Michelin,34)
p=(Jean,Michelin,56)
âge (-4) invalide

Properties 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 using the following syntax:

Classe objet=new Classe (...) {Propriété1=val1, Propriété2=val2, ...}

This syntax is equivalent to the following code:

1
2
3
4
Classe objet=new Classe(...);
objet.Propriété1=val1;
objet.Propriété2=val2;
...

The order of the properties doesn't matter. Here's an example.

The class Person adds a new constructor without parameters :


        public Personne() {
}

The constructor does not initialize the object's members. This is known as the default constructor. It is used when the class does not define a constructor.

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


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p2 = new Personne { Age = 7, Prenom = "Arthur", Nom = "Martin" };
            Console.WriteLine("p2=({0},{1},{2})", p2.Prenom, p2.Nom, p2.Age);
        }
    }
}

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


            Personne p2 = new Personne() { Age = 7, Prenom = "Arthur", Nom = "Martin" };

but the parentheses of the constructeur Person() without parameters are not mandatory in this syntax.

The results are as follows:

p2=(Arthur,Martin,7)

In many cases, the get and set property simply reads and writes a private field without any further processing. In this scenario, we can use a automatic declared as follows:

public Type Propriété{ 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. So, instead of writing :


    private string prenom;
...
    // propriété associée
        public string Prenom {
            get { return prenom; }
            set {
                // prénom valide ?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
            }//if
        }//prenom

we can write :

public string Prenom {get; set;}

without declaring the private field first name. The difference between the two previous properties is that the first verifies the validity of the first name in the set, while the second does not.

Use the automatic property First name is to declare a field First name public :

public string Prenom;

We wonder whether there is any difference between the two declarations. Declare public a class field is not recommended. This breaks with the concept of encapsulating an object's state, which must be private and exposed by public methods.

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


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

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

Line 2 above, the daughter class Class2 can put in the set, code verifying the validity of the value assigned to the automatic property base.Prop of the parent class Class1.

4.1.15. Class methods and attributes

Let's suppose we want to count the number of objects Person created in an application. You can manage a counter yourself, but you run the risk of forgetting the temporary objects that are created ici or there. It would seem safer to include in class constructors Person, an instruction that increments a counter. The problem is to pass a reference to this counter so that the constructor can increment it: a new parameter must be passed. Alternatively, you can include the counter in the class definition. Since it's an attribute of the class itself and not of a particular instance of that class, we declare it differently with the keyword static :


        private static long nbPersonnes;

To reference it, we write Personne.nbPersonnes to show that it's an attribute of class Person itself. Ici, we've created a private attribute that won't be accessed directly outside the class. We therefore create a public property to give access to the class attribute nbPersonnes. To return the value of nbPersonnes the method get of this property doesn't need an object Person particular: in fact nbPersonnes is the attribute of an entire class. So we need a property declared as well static :


        public static long NbPersonnes {
            get { return nbPersonnes; }
}

which from the outside will be called with the syntax Personne.NbPersonnes. Here's an example.

The P classerson becomes as follows:


using System;
 
namespace Chap2 {
    public class Personne {
 
         // class attributes
        private static long nbPersonnes;
        public static long NbPersonnes {
            get { return nbPersonnes; }
        }
 
         // instance attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
            nbPersonnes++;
        }
        public Personne(Personne p) {
            Initialise(p);
            nbPersonnes++;
        }
 
...
}

On lines 20 and 24, the builders increment the static field on line 7.

With the following program:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Personne p1 = new Personne("Jean", "Dupont", 30);
            Personne p2 = new Personne(p1);
            new Personne(p1);
            Console.WriteLine("Nombre de personnes créées : " + Personne.NbPersonnes);
        }
    }
}

we obtain the following results:

    Nombre de personnes créées : 3

4.1.16. A table of people

An object is data like any other, and as such several objects can be brought together in a table:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
             // a table of people
            Personne[] amis = new Personne[3];
            amis[0] = new Personne("Jean", "Dupont", 30);
            amis[1] = new Personne("Sylvie", "Vartan", 52);
            amis[2] = new Personne("Neil", "Armstrong", 66);
             // display
            foreach (Personne ami in amis) {
                ami.Identifie();
            }
        }
    }
}
  • line 7: creates an array of 3 elements of type Person. These 3 elements are initialized ici with the value null, c.a.d. that they do not reference any objects. Again, this is a misuse of the term "array of objects", when in fact it's just an array of object references. The creation of the array of objects, which is an object itself (presence of new) doesn't create any objects of the same type as its elements.
  • lines 8-10: creation of 3 objects of type Person
  • lines 12-14: display of table contents friends

We obtain the following results:

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

4.2. Inheritance by example

4.2.1. General

We ici the notion of inheritance. The aim of inheritance is to "customize" an existing class to suit our needs. Suppose we want to create a class Enstructor : a teacher is a special person. He has attributes that no other person would have: the subject he teaches, for example. But he also has the attributes of any other person: first name, last name and age. A teacher is therefore fully part of the class Person but has additional attributes. Rather than writing a Enstructor from the ground up, we'd rather take what we've learned in class Person adapted to the particular character of the teachers. This is the concept ofheritage that makes this possible.

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

    public class Enseignant : Personne

Person is called the parent class, and Enstructor the derived (or daughter) class. An object Enstructor has all the qualities of an object Person : 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 indicate the attributes and methods added by the child class:

We assume that the Person is defined as follows:


using System;
 
namespace Chap2 {
    public class Personne {
 
         // class attributes
        private static long nbPersonnes;
        public static long NbPersonnes {
            get { return nbPersonnes; }
        }
 
         // instance attributes
        private string prenom;
        private string nom;
        private int age;
 
         // manufacturers
        public Personne(String prenom, String nom, int age) {
            Nom = nom;
            Prenom = prenom;
            Age = age;
            nbPersonnes++;
            Console.WriteLine("Constructeur Personne(string, string, int)");
        }
        public Personne(Personne p) {
            Nom = p.Nom;
            Prenom = p.Prenom;
            Age = p.Age;
            nbPersonnes++;
            Console.WriteLine("Constructeur Personne(Personne)");
        }
 
         // properties
        public string Prenom {
            get { return prenom; }
            set {
                 // valid first name?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
             }//if
         }//first name
 
        public string Nom {
            get { return nom; }
            set {
                 // valid name?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("nom (" + value + ") invalide");
                } else { nom = value; }
             }//if
         }//name
 
        public int Age {
            get { return age; }
            set {
                 // valid age?
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("âge (" + value + ") invalide");
             }//if
         }//age
 
         // property
        public string Identite {
            get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
        }
    }
 
}

The method Identifies has been replaced by the Identity that identifies the person. We create a Enstructor inheriting from the Person :


using System;
 
namespace Chap2 {
    class Enseignant : Personne {
         // attributes
        private int section;
 
         // manufacturer
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
             // the section is saved using the Section property
            Section = section;
             // follow-up
            Console.WriteLine("Construction Enseignant(string, string, int, int)");
         }//manufacturer
 
         // property Section
        public int Section {
            get { return section; }
            set { section = value; }
         }// Section
 
    }
}

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

  • line 4: the class Teacher class derivation Person
  • line 6: an attribute section which is the section number to which the teacher belongs in the teaching corps (roughly one section per discipline). This private attribute is accessible via the public property Section lines 18-21
  • line 9: a new constructor to initialize all teacher attributes

4.2.2. Building a Teacher object

A girls' class does not inherit constructors of its Parent class. It must then define its own constructors. The constructor of the Enstructor is as follows:


         // manufacturer
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
             // section is memorized
            Section = section;
             // follow-up
            Console.WriteLine("Construction enseignant(string, string, int, int)");
}//manufacturer

The declaration


        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {

declares that the constructor receives four parameters first name, name, age, section and three more (firstname,lastname,age) to its base class, ici the class Person. We know that this class has a constructor Person(string, string, int) which will build a person with the parameters passed (firstname,lastname,age). Once construction of the base class is complete, construction of the Teacher continues with the execution of the builder's body:


            // on mémorise la section
            Section = section;

Note that to the left of the = sign, it's not the section property of the object used, but the Section associated with it. This allows the constructor to take advantage of any validity checks that may be performed by this method. This avoids the need to place them in two different places: the constructor and the property.

In short, the constructor of a derived :

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

We might have preferred to write :


// constructeur
  public Enseignant(string prenom, string nom, int age, int section){
    this.prenom=prenom;
        this.nom=nom;
        this.age=age;
      this.section=section;
  }

It can't be done. The class Person declared private (private) its three fields first name, name and age. Only objects of the same class have direct access to these fields. All other objects, including child objects such as ici, must use public methods to access them. It would have been different if the Person had declared protected (protected) the three fields: this allowed derived classes to have direct access to the three fields. In our example, using the parent class's constructor was therefore the right solution, and this is the usual method: when constructing a child object, we first call the parent object's constructor, 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 Enseignant("Jean", "Dupont", 30, 27).Identite);
        }
    }
}

This program simply creates a Enstructor (new) and identify it. The class Enstructor has no method Identite but its parent class has one that is also public: by inheritance, it becomes a public method of class Enstructor.

The overall project is as follows:

The results are as follows:

1
2
3
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
[Jean, Dupont, 30]

We can see that :

  • an object Person (line 1) was built before object Enstructor (line 2)
  • the identity obtained is that of the object Person

4.2.3. Redefining a method or property

In the previous example, we had the identity of the part Person but some class-specific information is missing Enstructor (the section). This leads us to write a property that identifies the teacher:


using System;
 
namespace Chap2 {
    class Enseignant : Personne {
         // attributes
        private int section;
 
         // manufacturer
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
             // the section is saved using the Section property
            Section = section;
             // follow-up
            Console.WriteLine("Construction Enseignant(string, string, int, int)");
         }//manufacturer
 
         // property Section
        public int Section {
            get { return section; }
            set { section = value; }
         }// section
 
         // property Identity
        public new string Identite {
            get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
        }
    }
}

Lines 24-26, the property Identite class Enstructor is based on the Identite of its parent class (baseidentity) (line 25) to display its "Person"then complete with the section which is specific to the Enstructor. Note the declaration of the property Identity :


    public new string Identite{

Let an object teacher E. This object contains a Person :

The property Identity is defined both in the Teacher and its parent class Person. In the girls' class Teacher, the property Identity must be preceded by the keyword new to indicate that a new property is being redefined Identity for the class Teacher.


    public new string Identite{

Class Enstructor now has two properties Identite :

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

If E is an Enstructor, E.Identite designates the property Identite class Enstructor. We say that property Identite girl class redefines or hides property Identite of the parent class. In general, if O is an object and M a method, to execute the O.M, the system looks for a method M in the following order:

  • in the O
  • in its parent class if it has one
  • in the parent class of its parent class if it exists
  • etc..

Inheritance allows you to redefine methods/properties of the same name in the parent class in the daughter class. This allows you to adapt the child class to your own needs. Combined with polymorphism, which we'll look at in a moment, redefining methods/properties is the main advantage of inheritance.

Consider the same test program as above:


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

The results obtained this time are as follows:

1
2
3
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Jean, Dupont, 30],27]

4.2.4. Polymorphism

Consider a lineage of classes: C0 C1 C2 Cn

where Ci Cj indicates that the Cj is derived from the Ci. This means that the Cj has all the characteristics of the Ci plus others. Let objects Oi type Ci. It is legal to write :

    Oi=Oj avec j>i

Indeed, by inheritance, the Cj has all the characteristics of class Ci plus others. So an Oj type Cj contains an object of type Ci. The operation

    Oi=Oj

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

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

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

    public static void Affiche(Personne p){
        ….
    }

We might as well write

    Personne p;
    ...
    Affiche(p);

that

    Enseignant e;
    ...
    Affiche(e);

In the latter case, the formal parameter p type Person static method Affiche will receive a value of type Enstructor. As the Teacher type drift Person, it's legal.

4.2.5. Redefinition and polymorphism

Let's complete our A methodffiche :


        public static void Affiche(Personne p) {
             // displays identity of p
            Console.WriteLine(p.Identite);
}//poster

The property p.Identite returns a string identifying the object Person p. What happens in the previous example if the parameter passed to the Poster is an object of type Teacher :


            Enseignant e = new Enseignant(...);
            Affiche(e);

Let's look at the following example:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // a teacher
            Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
            Affiche(e);
             // a person
            Personne p = new Personne("Jean", "Dupont", 30);
            Affiche(p);
        }
 
         // poster
        public static void Affiche(Personne p) {
             // displays identity of p
            Console.WriteLine(p.Identite);
         }//poster
    }
}

The results are as follows:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
[Lucile, Dumas, 56]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

Execution shows that the p.Identite (line 17) has executed the Identity a Person, first (line 7) the person contained in theTeacher e, then (line 10) the Person p itself. It has not adapted to the object actually passed as a parameter to Poster. We would have preferred to have the full identity of theTeacher e. This would have required the notation p.Identite property reference Identity of the object actually pointed by p rather than property Identity de partie "Person"of the object actually by p.

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


public virtual string Identite {
            get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age); }
        }

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


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

The previous program then produces the following results:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Lucile, Dumas, 56],61]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

This time, on line 3, we've got the teacher's full identity. Now let's redefine a method rather than a property. The class object (C# alias of System.Object) is the "mother" class of all C# classes. So when you write :

    public class Personne

we implicitly write :

    public class Personne : System.Object

The class System.Object defines a virtual method ToString :

The method ToString 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 Enseignant("Lucile", "Dumas", 56, 61).ToString());
             // a person
            Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString());
        }
    }
}

The results are as follows:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Chap2.Enseignant
Constructeur Personne(string, string, int)
Chap2.Personne

Note that although we haven't redefined the ToString in the classroom Person and Teacher, however, we can see that the ToString class Object was able to display the object's real class name.

Let's redefine the ToString in the classroom Person and Teacher :


        // méthode ToString
        public override string ToString() {
            return Identite;
}

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
            Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
            Affiche(e);
             // a person
            Personne p = new Personne("Jean", "Dupont", 30);
            Affiche(p);
        }
         // poster
        public static void Affiche(Personne p) {
             // displays identity of p
            Console.WriteLine(p);
         }//Poster
    }
}

Let's look at the method Poster whose parameter is a person p. Line 15, the WriteLine class Console has no variant admitting a parameter of type Person. Among the various Writeline, there is one that accepts a Object. The compiler will use this method, WriteLine(Object o), because this signature means that the o can be of the Object or derivative. Since Object is the parent class of all classes, any object can be passed as a parameter to WriteLine and therefore an object of type Person or Teacher. The method WriteLine(Object o) writes o.ToString() in the writing flow Out. The method ToString being virtual, if object o (of type Object or derivative) has redefined the ToString, the latter will be used. This is ici the case with the Person and Teacher.

This is what the performance results show:

1
2
3
4
5
Constructeur Personne(string, string, int)
Construction Enseignant(string, string, int, int)
Enseignant[[Lucile, Dumas, 56],61]
Constructeur Personne(string, string, int)
[Jean, Dupont, 30]

4.3. Redéfiorr the meaning of an operator for a class

4.3.1. Introduction

Consider the instruction

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 the C1 with the following signature:

public static [type] operator +(C1 opérande1, C2 opérande2);

When the compiler encounters the

op1 + op2

he then translates it as C1.operator+(op1,op2). The type rendered by the operator is important. Consider the operation op1+op2+op3. It is translated by the compiler as (op1+op2)+op3. Let res12 the result of op1+op2. The next operation is res12+op3. If res12 type is C1, it will also be translated by C1.operator+(res12,op3). This makes it possible to chain operations.

Unary operators with a single operand can also be redefined. For example, if op1 is an object of type C1, the operation op1++ can be redefined by a static method of the C1 :

public static [type] operator ++(C1 opérande1);

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

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

4.3.2. An example

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

  • the L method.Add(Object o) to add to the L an object o. Ici object o will be an object Person.
  • property L.Count which gives the number of elements in the list L
  • notation L[i] which gives the element i of the list L

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


using System;
using System.Collections;
using System.Text;
 
namespace Chap2 {
    class ListeDePersonnes : ArrayList{
         // redefine + operator, to add a person to the list
        public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
             // person p is added to the ListeDePersonnes l
            l.Add(p);
             // we return the ListeDePersonnes l
            return l;
         }// operator +
 
         // ToString
        public override string ToString() {
             // render (él1, él2, ..., éln)
             // opening parenthesis
            StringBuilder listeToString = new StringBuilder("(");
             // browse 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(")");
             // you must return a string
            return listeToString.ToString();
         }//ToString
    }
}
  • line 6: the class ListeDePersonnes class derivation ArrayList
  • lines 8-13: definition of operator + for operation l + p, where l is of type ListeDePersonnes and p of type Person or derivative.
  • line 10: person p is added to list l. This is the Add parent class ArrayList which is ici used.
  • line 12: the reference to list l is rendered so that + operators can be concatenated, as in l + p1 + p2. The operation l+p1+p2 will be interpreted (operator priority) as (l+p1)+p2. The operation l+p1 makes the reference l. The operation (l+p1)+p2 then becomes l+p2 which adds the person p2 to the list of persons l.
  • line 16: we redefine the ToString to display a list of people as (person1, person2, ..) where personi is itself the result of the ToString class Person.
  • line 19: we use an object of type StringBuilder. This class is more suitable than the string as soon as numerous string operations are required, ici additions. In fact, each operation on a string makes a new object string, while the same operations on a StringBuilder modify the object but do not create a new one. We use the Append to concatenate strings.
  • line 21: scroll through the elements of the list of people. This list is ici designated by this. This is the current object on which the ToString. The property Count is a property of the parent class ArrayList.
  • line 22: element no. i in the current list this is accessible via the notation this[i]. Once again, this is a property of the ArrayList. As it involves adding strings, the this[i].ToString() which will be used. As this is a virtual method, the ToString object this, type Person or derivative, which will be used.
  • line 31: we need to return an object of type string (line 16). The class StringBuilder has a method ToString which allows you to switch from a StringBuilder to a type string.

Note that the ListeDePersonnes has no constructor. In this case, we know that the

public ListeDePersonnes(){
}

will be used. This constructor does nothing except 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
            ListeDePersonnes l = new ListeDePersonnes();
             // add people
            l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
             // display
            Console.WriteLine("l=" + l);
            l = l + new Enseignant("camille", "germain",27,60);
            Console.WriteLine("l=" + l);
        }
    }
}
  • line 7: creation of a list of people l
  • line 9: add 2 people with + operator
  • line 12: teacher added
  • lines 11 and 13: use the redefined method ListeDePersonnes.ToString().

The results:

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

4.4. Defining an indexer for a class

We continue ici to use the class ListeDePersonnes. If l is an object ListeDePersonnes, we want to be able to use l[i] to designate person no. i on the list l both in reading (Person p=l[i]) and in writing (l[i]=new Person(...)).

To be able to write l[i] where l[i] designates an object Person, class, we need to define ListeDePersonnes the method this next :


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

The method is called this[int i], a indexer because it gives meaning to the expression obj[i] which is reminiscent of array notation, while obj is not an array but an object. The get the method this object obj is called up when variable=obj[i] and the set when writing obj[i]=value.

The class ListeDePersonnes class derivation ArrayList which itself has an indexer :

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

There is a conflict between the this class ListeDePersonnes :


 public Personne this[int i] 

and the this class ArrayList


 public object this[int i] 

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


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

Let's complete this method. The method this.get is called up when variable=l[i] for example, where l type is ListeDePersonnes. We must then return person n° i from the list l. This is done with the notation base[i], that makes object n° i of the class ArrayList class underlying ListeDePersonnes . The returned object is of type Object, transtyping to class Person is required.


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

The method set is called up when l[i]=p where p is a Person. The aim is to affect the person p to element i in the l.


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

Ici, the person p represented by the keyword value is assigned to item no i of the base class ArrayList.

The class indexer ListeDePersonnes will therefore be as follows:


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

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


        // indexeur via un nom
        public int this[string nom] {
            get {
                // on recherche la personne
                for (int i = 0; i < Count; i++) {
                    if (((Personne)base[i]).Nom == nom)
                        return i;
                }//for
                return -1;
            }//get
}

The first line


public int this[string nom]

indicates that the ListeDePersonnes by a string name and 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 the person is not in the list. Only the get, prohibiting writing l["name"]=value which would have required the definition of set. The keyword new is not required in the indexer declaration, as the base class ArrayList does not define an indexer this[string].

In the body of the get, scans the list of people looking for the name passed in parameter. If it is found at position i, i is returned, otherwise -1 is returned.

The previous test program is completed as follows:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // a list of people
            ListeDePersonnes l = new ListeDePersonnes();
             // add people
            l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
             // display
            Console.WriteLine("l=" + l);
            l = l + new Enseignant("camille", "germain",27,60);
            Console.WriteLine("l=" + l);
             // change item 1
            l[1] = new Personne("franck", "gallon",5);
             // display element 1
            Console.WriteLine("l[1]=" + l[1]);
             // display list l
            Console.WriteLine("l=" + l);
             // people search
            string[] noms = { "martin", "germain", "xx" };
            for (int i = 0; i < noms.Length; i++) {
                int inom = l[noms[i]];
                if (inom != -1)
                    Console.WriteLine("Personne(" + noms[i] + ")=" + l[inom]);
                else
                    Console.WriteLine("Personne(" + noms[i] + ") n'existe pas");
            }//for
        }
    }
}

Its execution gives the following results:

1
2
3
4
5
6
7
l=([jean, martin, 10],[pauline, leduc, 12])
l=([jean, martin, 10],[pauline, leduc, 12],Enseignant[[camille, germain, 27],60])
l[1]=[franck, gallon, 5]
l=([jean, martin, 10],[franck, gallon, 5],Enseignant[[camille, germain, 27],60])
Personne(martin)=[jean, martin, 10]
Personne(germain)=Enseignant[[camille, germain, 27],60]
Personne(xx) n'existe pas

4.5. The structures

The C# structure is analogous to the structure of the C language and is very close to the notion of class. A structure is defined as follows:

struct NomStructure{
// attributs
    ...
// propriétés
...
// constructeurs
...
// méthodes
...
}

Despite similar declarations, there are significant differences between class and structure. For example, the notion of inheritance does not exist with structures. If we're writing a class that doesn't need to be derived, what are the differences between structure and class 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 sp1 structure
            SPersonne sp1;
            sp1.Nom = "paul";
            sp1.Age = 10;
            Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
             // a sp2 structure
            SPersonne sp2 = sp1;
            Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
             // sp2 is modified
            sp2.Nom = "nicole";
            sp2.Age = 30;
             // checking sp1 and sp2
            Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
            Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
 
             // an op1 object
            CPersonne op1=new CPersonne();
            op1.Nom = "paul";
            op1.Age = 10;
            Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
             // an op2 object
            CPersonne op2=op1;
            Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
             // op2 is modified
            op2.Nom = "nicole";
            op2.Age = 30;
             // op1 and op2 verification
            Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
            Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
        }
    }
     // structure SPersonne
    struct SPersonne {
        public string Nom;
        public int Age;
    }
 
     // class CPersonne
    class CPersonne {
        public string Nom;
        public int Age;
    }
 
}
  • lines 38-41: a structure with two public fields: Nom, Age
  • lines 44-47: a class with two public fields: Nom, Age

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

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

Where previously we used a Person, we now use a SPersonne :


    struct SPersonne {
        public string Nom;
        public int Age;
}

The structure has ici no constructor. It could have one, as we'll show later. By default, it always has the constructor without parameters, ici SPersonne().

  • line 7 of the code: the declaration

    SPersonne sp1;

is equivalent to the instruction :


    SPersonne sp1=new Spersonne();

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


CPersonne op1=new CPersonne();

The previous instruction creates a CPersonne (roughly equivalent to our structure) and the value from 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 the class, the value of op1 is theaddress of the object created

When in the program we write line 12 :


            SPersonne sp2 = sp1;

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

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


CPersonne op2=op1;

In the case of classes, the value of op1 is copied into op2, but as this value is in fact the object address, it is not duplicated [2].

In the case of structure [1], if we change the value of sp2 the value of sp1, as shown in the program. In the case of object [2], if we modify the object pointed to by op2, the one pointed by op1 is modified because it's the same. This is also shown by the program results.

These explanations show that :

  • the value of a structure variable is the structure itself
  • the value of an object variable is the address of the object pointed to

Once this fundamental difference is understood, the structure is very close to the class, as shown in the following new example:


using System;
 
namespace Chap2 {
 
     // structure SPersonne
    struct SPersonne {
         // private attributes
        private string nom;
        private int age;
 
         // properties
        public string Nom {
            get { return nom; }
            set { nom = value; }
         }//name
 
        public int Age {
            get { return age; }
            set { age = value; }
         }//age
 
         // Manufacturer
        public SPersonne(string nom, int age) {
            this.nom = nom;
            this.age = age;
         }//manufacturer
 
         // ToString
        public override string ToString() {
            return "SPersonne(" + Nom + "," + Age + ")";
         }//ToString
     }//structure
}//namespace
  • lines 8-9: two private fields
  • lines 12-20: associated public properties
  • lines 23-26: define a constructor. Note that the constructor without parameters SPersonne() is always present and does not need to be declared. Its declaration is refused by the compiler. In the constructor on lines 23-26, you may be tempted to initialize the private fields name, age via their public properties Name, Age. This is refused by the compiler. Structure methods cannot be used during structure construction.
  • lines 29-31: method redefinition ToString.

A test program might look like this:


using System;
 
namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
             // one person p1
            SPersonne p1=new SPersonne();
            p1.Nom="paul";
            p1.Age= 10;
            Console.WriteLine("p1={0}",p1);
             // one person p2
            SPersonne p2 = p1;
            Console.WriteLine("p2=" + p2);
             // p2 is modified
            p2.Nom = "nicole";
            p2.Age = 30;
             // checking p1 and p2
            Console.WriteLine("p1=" + p1);
            Console.WriteLine("p2=" + p2);
             // one person p3
            SPersonne p3 = new SPersonne("amandin", 18);
            Console.WriteLine("p3=" + p3);
             // one person p4
            SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
            Console.WriteLine("p4=" + p4);
        }
    }
}
  • line 7: we are obliged to explicitly use the constructor without parameters, because there is another constructor in the structure. If the structure had no constructor, the instruction

            SPersonne p1;

would have been enough to create an empty structure.

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

The following 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=SPersonne(amandin,18)
p4=SPersonne(x,10)

The only notable difference ici between structure and class is that, with a class, 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 prototype methods or properties that form a contract. A class that decides to implement an interface undertakes to provide an implementation of all the methods defined in the interface. The compiler verifies this implementation.

For example, here's the interface definition System.Collections.IEnumerator :

public interface System.Collections.IEnumerator 


{    // Prop
e   rties Object Curren

t    { get; } 
     // Methods 
     bool MoveNe
xt(); void Reset(); }

Interface properties and methods are defined only by their signatures. They are not implemented (have no code). It is the classes that implement the interface that give code to 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 class IEnumerator. Note that the : sign used to implement an interface is the same as that used to derive a class.
  • lines 3-5: implementation of interface methods and properties IEnumerator.

Consider the following interface:


namespace Chap2 {
    public interface IStats {
        double Moyenne { get; }
        double EcartType();
    }
}

The interface IStats presents :

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

Note that nowhere is it specified which series of values are involved. It could be the average of a class's grades, the average monthly 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 the existence of specific data.

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


    public struct Elève {
        public string Nom { get; set; }
        public string Prénom { get; set; }
}//Student

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

A note would be characterized by the structure Note next :


    public struct Note {
        public Elève Elève { get; set; }
        public double Valeur { get; set; }
}//Note

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

The grades of all students in a given subject are combined in the class TableauDeNotes next :


using System;
using System.Text;
 
namespace Chap2 {
 
    public class TableauDeNotes : IStats {
         // attributes
        public string Matière { get; set; }
        public Note[] Notes { get; set; }
        public double Moyenne { get; private set; }
        private double ecartType;
 
         // manufacturer
        public TableauDeNotes(string matière, Note[] notes) {
             // saving via public properties
            Matière = matière;
            Notes = notes;
             // calculating the average score
            double somme = 0;
            for (int i = 0; i < Notes.Length; i++) {
                somme += Notes[i].Valeur;
            }
            if (Notes.Length != 0) Moyenne = somme / Notes.Length;
            else Moyenne = -1;
             // standard deviation
            double carrés = 0;
            for (int i = 0; i < Notes.Length; i++) {
                carrés += Math.Pow((Notes[i].Valeur - Moyenne), 2);
            }//for
            if (Notes.Length != 0)
                ecartType = Math.Sqrt(carrés / Notes.Length);
            else ecartType = -1;
         }//manufacturer
 
        public double EcartType() {
            return ecartType;
        }
 
         // ToString
        public override string ToString() {
            StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
            int i;
             // concatenate all the notes
            for (i = 0; i < Notes.Length-1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
            };
             //final note
            if (Notes.Length != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
            }
            valeur.Append(")");
             // end
            return valeur.ToString();
         }//ToString
 
     }//class
}
  • line 6: the class TableauDeNotes implements the IStats. It must therefore implement the Average and the EcartType. These are implemented on line 10 (Average) and 35-37 (EcartType)
  • lines 8-10: three automatic properties
  • line 8: the material whose notes the object memorizes
  • line 9: student grades table (Student, Grade)
  • line 10: average score - property implementing the Average interface IStats.
  • line 11: field memorizing standard deviation of scores - the method get associate EcartType in lines 35-37 implements the EcartType interface IStats.
  • line 9: grades are stored in a table. This is transmitted when the class is built TableauDeNotes to the manufacturer of lines 14-33.
  • lines 14-33: the builder. It is assumed ici that the scores transmitted to the constructor will not change in the future. We therefore 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 average is stored in the private field underlying the automatic property Average on line 10 and the standard deviation in the private field on line 11.
  • line 10: the method get automatic ownership Average will render the underlying private field.
  • lines 35-37: the method EcartType returns the value of the private field on line 11.

There are a few subtleties in this code:

  • line 23: the method set property Average is used to make the assignment. This method has been declared private on line 10, so that assigning a value to the Average is only possible within the classroom.
  • lines 40-54: use an object StringBuilder to build the string representing the TableauDeNotes to improve performance. It should be noted, however, that code readability suffers considerably. This is the other side of the coin.

In the previous class, notes were stored in a table. It was not possible to add a new note once the TableauDeNotes. We now propose a second implementation of the IStats, called ListeDeNotes, where this time the notes would be saved in a list, with the possibility of adding notes after the initial construction of the object ListeDeNotes.

Class code ListeDeNotes is as follows:


using System;
using System.Text;
using System.Collections.Generic;
 
namespace Chap2 {
 
    public class ListeDeNotes : IStats {
         // attributes
        public string Matière { get; set; }
        public List<Note> Notes { get; set; }
        public double moyenne = -1;
        public double ecartType = -1;
 
         // manufacturer
        public ListeDeNotes(string matière, List<Note> notes) {
             // saving via public properties
            Matière = matière;
            Notes = notes;
         }//manufacturer
 
         // add a note
        public void Ajouter(Note note) {
             // add note
            Notes.Add(note);
             // mean and standard deviation reset
            moyenne = -1;
            ecartType = -1;
        }
 
         // ToString
        public override string ToString() {
            StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
            int i;
             // concatenate all the notes
            for (i = 0; i < Notes.Count - 1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
            };
             //final note
            if (Notes.Count != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
            }
            valeur.Append(")");
             // end
            return valeur.ToString();
         }//ToString
 
         // average score
        public double Moyenne {
            get {
                if (moyenne != -1) return moyenne;
                 // calculating the average score
                double somme = 0;
                for (int i = 0; i < Notes.Count; i++) {
                    somme += Notes[i].Valeur;
                }
                 // we return the average
                if (Notes.Count != 0) moyenne = somme / Notes.Count;
                return moyenne;
            }
        }
 
        public double EcartType() {
             // standard deviation
            if (ecartType != -1) return ecartType;
             // average
            double moyenne = Moyenne;
            double carrés = 0;
            for (int i = 0; i < Notes.Count; i++) {
                carrés += Math.Pow((Notes[i].Valeur - moyenne), 2);
            }//for
             // we return the standard deviation
            if (Notes.Count != 0)
                ecartType = Math.Sqrt(carrés / Notes.Count);
            return ecartType;
        }
     }//class
}
  • line 7: the class ListeDeNotes implements the IStats
  • line 10: notes are now displayed in a list rather than a table
  • line 11: automatic ownership Average class TableauDeNotes has been abandoned ici in favor of a private field average, line 11, associated with read-only public ownership Average lines 48-60
  • lines 22-28: you can now add a note to those already memorized, which was previously impossible.
  • lines 15-19: as a result, the mean and standard deviation are no longer calculated in the constructor, but in the interface methods themselves: Average (lines 48-60) and EcartType (62-76). However, recalculation is only restarted if the mean and standard deviation are different from -1 (lines 50 and 64).

A test class might look like this:


using System;
using System.Collections.Generic;
 
namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
             // some students & english notes
            Elève[] élèves1 =  { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
            Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
             // which we save in a TableauDeNotes object
            TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
             // average and standard deviation display
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
             // we put the students and the material in a ListeDeNotes object
            ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
             // average and standard deviation display
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
             // we add a note
            français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
             // average and standard deviation display
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
        }
    }
}
  • line 8: create an array of students using the constructor without parameters and initialization via public properties
  • line 9: creation of a table of notes using the same technique
  • line 11: an object TableauDeNotes whose mean and standard deviation are calculated on line 13
  • line 15: an object ListeDeNotes whose mean and standard deviation are calculated on line 17. The class List<Note> has a constructor admitting an object implementing the IEnumerable<Note>. The table notes1 implements this interface and can be used to build the List<Note>.
  • line 19: new note added
  • line 21: recalculation of mean and standard deviation

The results are as follows:

1
2
3
matière=anglais, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545
matière=français, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545
matière=français, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18],[Jérôme,Jaric,10]), Moyenne=14,5, Ecart-type=2,95803989154981

In the previous example, two classes implement the IStats. That said, the example doesn't show how useful the interface is IStats. 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 notes
            Elève[] élèves1 =  { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
            Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
             // which we save in a TableauDeNotes object
            TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
             // average and standard deviation display
            AfficheStats(anglais);
             // we put the students and the material in a ListeDeNotes object
            ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
             // average and standard deviation display
            AfficheStats(français);
             // we add a note
            français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
             // average and standard deviation display
            AfficheStats(français);
        }
 
         // display mean and standard deviation of a type IStats
        static void AfficheStats(IStats valeurs) {
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs);
        }
    }
}
  • lines 25-27: the static method AfficheStats receives a IStats, type Interface. This means that the effective parameter can be any object implementing the IStats. When you use data with an interface type, this means you only use the interface methods implemented by the data. The rest is ignored. This is a property similar to the polymorphism seen for classes. If a set of Ci not linked by inheritance (so you can't use the polymorphism of inheritance) presents a set of methods with the same signature, it can be interesting to group these methods in an interface I implemented by all the classes concerned. Instances of these classes Ci can then be used as effective parameters of functions admitting a formal parameter of type I, c.a.d. functions using only object methods Ci defined in the I and not the attributes and methods of individual classes Ci.
  • line 13: the method AfficheStats is called with a TableauDeNotes which implements the IStats
  • line 17: ditto with a type ListeDeNotes

The results of this run are identical to those of the previous one.

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

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

The statement on line 1 indicates that stats1 is the instance of a class implementing the IStats. This statement implies that the compiler will only allow access to stats1 interface methods: the Average and the EcartType.

Finally, it should be noted that interfaces can be implemented in multiple ways, c.a.d. which can be written as

public class ClasseDérivée:ClasseDeBase,I1,I2,..,In{
...
}

where the Ij are interfaces.

4.7. Abstract classes

An abstract class is one that cannot be instantiated. You need to create derived classes that can be instantiated.

Abstract classes can be used to factorize the code of a line of classes. Consider the following case:


using System;
 
namespace Chap2 {
    abstract class Utilisateur {
         // fields
        private string login;
        private string motDePasse;
        private string role;
 
         // manufacturer
        public Utilisateur(string login, string motDePasse) {
             // information is recorded
            this.login = login;
            this.motDePasse = motDePasse;
            // on identifie l'utilisateur
            role=identifie();
             // identified?
            if (role == null) {
                throw new ExceptionUtilisateurInconnu(String.Format("[{0},{1}]", login, motDePasse));
            }
        }
 
         // toString
        public override string ToString() {
            return String.Format("Utilisateur[{0},{1},{2}]", login, motDePasse, role);
        }
 
         // identifies
        abstract public string identifie();
    }
}
  • lines 11-21: the class builder User. This class stores information on the user of an web application. This application has various types of users authenticated by a login/password (lines 6-7). These two pieces of information are checked with a LDAP service for some users, with a SGBD for others, etc...
  • lines 13-14: authentication information is stored in memory
  • line 16: they are verified by a identifies. Because the identification method is not known, it is declared abstract on line 29 with the keyword abstract. The method identifies returns a string specifying the user's role (basically, what he's allowed to do). If this string is the null, an exception is thrown on line 19.
  • line 4: because it has an abstract method, the class itself is declared abstract with the keyword abstract.
  • line 29: abstract method identifies has no definition. Derived classes will give it one.
  • lines 24-26: the method ToString which identifies an instance of the class.

It is assumed ici that the developer wishes to control the construction of instances of the class User and derived classes, perhaps because he wants to be sure that an exception of a certain type is thrown if the user is not recognized (line 19). Derived classes can rely on this constructor. To do so, they must provide the identifies.

The class ExceptionUtilisateurInconnu is as follows:


using System;
 
namespace Chap2 {
    class ExceptionUtilisateurInconnu : Exception {
        public ExceptionUtilisateurInconnu(string message) : base(message){
        }
    }
}
  • line 3: derived from the Exception
  • lines 4-6: it has a single constructor which accepts an error message as parameter. This is passed to the parent class (line 5), which has the same constructor.

We now derive the User in the girls' class Director :


namespace Chap2 {
    class Administrateur : Utilisateur {
         // manufacturer
        public Administrateur(string login, string motDePasse)
            : base(login, motDePasse) {
        }
 
         // identifies
        public override string identifie() {
             // identification LDAP
            // ...
            return "admin";
        }
    }
}
  • lines 4-6: the constructor simply passes the parameters it receives to its parent class
  • lines 9-12: the method identifies class Director. It is assumed that an administrator is identified by a LDAP system. This method redefines the identifies of its parent class. Because it redefines a abstract, it is useless to put the keyword override.

We now derive the User in the girls' class Observer :


namespace Chap2 {
    class Observateur : Utilisateur{
         // manufacturer
        public Observateur(string login, string motDePasse)
            : base(login, motDePasse) {
        }
 
         //identifies
        public override string identifie() {
             // identification SGBD
            // ...
            return "observateur";
        }
 
    }
}
  • lines 4-6: the constructor simply passes the parameters it receives to its parent class
  • lines 9-13: the method identifies class Observer. It is assumed that an observer is identified by checking his identification data in a database.

In the end, the objects Director and Observer are instantiated by the same constructor as the parent class User. This constructor will use the identifies these classes provide.

A third class Unknown also derives from the User :


namespace Chap2 {
    class Inconnu : Utilisateur{
 
         // manufacturer
        public Inconnu(string login, string motDePasse)
            : base(login, motDePasse) {
        }
 
         //identifies
        public override string identifie() {
             // unknown user
            // ...
            return null;
        }
 
    }
}
  • line 13: the method identifies makes the pointer null to indicate that the user has not been recognized.

A test program might look like this:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Observateur("observer","mdp1"));
            Console.WriteLine(new Administrateur("admin", "mdp2"));
            try {
                Console.WriteLine(new Inconnu("xx", "yy"));
            } catch (ExceptionUtilisateurInconnu e) {
                Console.WriteLine("Utilisateur non connu : "+ e.Message);
            }
        }
    }
}

Note that lines 6, 7 and 9 use the [User].ToString() which will be used by the WriteLine.

The results are as follows:

1
2
3
Utilisateur[observer,mdp1,observateur]
Utilisateur[admin,mdp2,admin]
Utilisateur non connu : [xx,yy]

4.8. Classes, interfaces and generic methods

Let's suppose we want to write a method that permutes two integers. This method could be as follows:


        public static void Echanger1(ref int value1, ref int value2){
            // on échange les références value1 et value2
            int temp = value2;
            value2 = value1;
            value1 = temp;
}

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


        public static void Echanger2(ref Personne value1, ref Personne value2){
            // on échange les références value1 et value2
            Personne temp = value2;
            value2 = value1;
            value1 = temp;
}

The difference between the two methods is the type T of the parameters: int in Exchange1, Person in Exchange2. Generic classes and interfaces meet the need for methods that differ only in the type of some of their parameters.

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


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

A test program for the class could be as follows:


using System;
 
namespace Chap2 {
    class Program {
        static void Main(string[] args) {
             // int
            int i1 = 1, i2 = 2;
            Generic1<int>.Echanger(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // string
            string s1 = "s1", s2 = "s2";
            Generic1<string>.Echanger(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
             // Person
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic1<Personne>.Echanger(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 must be "instantiated". Line 8: use the static method Exchange type Generic1<int> to indicate that references passed to the Exchange are of the int.
  • line 12: the static method is used Exchange type Generic1<string> to indicate that references passed to the Exchange are of the string.
  • line 16: the static method is used Exchange type Generic1<Person> to indicate that references passed to the Exchange are of the Person.

The results are as follows:

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

The method Exchange could also have been written as follows:


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

The test program is then as follows:


using System;
 
namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
             // int
            int i1 = 1, i2 = 2;
            Generic2.Echanger<int>(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // string
            string s1 = "s1", s2 = "s2";
            Generic2.Echanger<string>(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
             // Person
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic2.Echanger<Personne>(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);
        }
    }
}
  • lines 8, 12 and 16: call the Exchange by specifying the parameter type in <>. In fact, the compiler is able to deduce the variant of the Exchange to use. The following entry is therefore legal:

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

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

Constraints can be placed on generic parameters:

Image

Consider the new generic method Exchange next :


namespace Chap2 {
    class Generic3 {
        public static void Echanger<T>(ref T value1, ref T value2) where T : class {
             // exchange the value1 and value2 references
            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.Echanger<int>(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // string
            string s1 = "s1", s2 = "s2";
            Generic3.Echanger(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
             // Person
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic3.Echanger(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);
 
        }
    }
}

The compiler declares an error on line 8 because the type int is not a class or an interface, it's a structure:

Image

Consider the new generic method Exchange next :


namespace Chap2 {
    class Generic4 {
        public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
             // retrieve the value of the 2 elements
            int value1 = element1.Value();
            int value2 = element2.Value();
             // if 1st element > 2nd element, exchange elements
            if (value1 > value2) {
                T temp = element2;
                element2 = element1;
                element1 = temp;
            }
        }
    }
}
  • line 3: type T must implement the Interface1. It has a method Value, used on lines 5 and 6, which gives the value of the object of type T.
  • lines 8-12: the two references element1 and element2 are exchanged only if the value of element1 is greater than the value of element2.

The interface Interface1 is as follows:


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

It is implemented by the Class1 next :


using System;
using System.Threading;
 
namespace Chap2 {
    class Class1 : Interface1 {
         // object value
        private int value;
 
         // manufacturer
        public Class1() {
             // wait 1 ms
            Thread.Sleep(1);
             // random value between 0 and 99
            value = new Random(DateTime.Now.Millisecond).Next(100);
        }
 
         // accessor private field value
        public int Value() {
            return value;
        }
 
         // instance status
        public override string ToString() {
            return value.ToString();
        }
    }
}
  • line 5: Class1 implements the Interface1
  • line 7: the value of an instance of Class1
  • lines 10-14: the field value is initialized with a random value between 0 and 99
  • lines 18-20: the method Value interface Interface1
  • lines 23-25: the method ToString class

The interface Interface1 is also implemented by the Class2 :


using System;
 
namespace Chap2 {
    class Class2 : Interface1 {
         // object values
        private int value;
        private String s;
 
         // manufacturer
        public Class2(String s) {
            this.s = s;
            value = s.Length;
        }
 
         // accessor private field value
        public int Value() {
            return value;
        }
 
         // instance status
        public override string ToString() {
            return s;
        }
    }
}
  • line 4: Class2 implements the Interface1
  • line 6: the value of an instance of Class2
  • lines 10-13: the field value is initialized with the length of the string passed to the constructor
  • lines 16-18: the method Value interface Interface1
  • lines 21-22: the method ToString 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("Avant échange --> c1={0},c2={1}", c1, c2);
                Generic4.Echanger(ref c1, ref c2);
                Console.WriteLine("Après échange --> c1={0},c2={1}", c1, c2);
            }
             // exchange Class2 instances
            Class2 c3, c4;
            c3 = new Class2("xxxxxxxxxxxxxx");
            c4 = new Class2("xx");
            Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
            Generic4.Echanger(ref c3, ref c4);
            Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
        }
    }
}
  • lines 8-14: instances of the Class1 are exchanged
  • lines 16-22: instances of type Class2 are exchanged

The results are as follows:

Avant échange --> c1=43,c2=79
Après échange --> c1=43,c2=79
Avant échange --> c1=72,c2=56
Après échange --> c1=56,c2=72
Avant échange --> c1=92,c2=75
Après échange --> c1=75,c2=92
Avant échange --> c1=11,c2=47
Après échange --> c1=11,c2=47
Avant échange --> c1=31,c2=67
Après échange --> c1=31,c2=67
Avant échange --> c3=xxxxxxxxxxxxxx,c4=xx
Après échange --> c3=xx,c4=xxxxxxxxxxxxxx

To illustrate the concept d'generic interface, we're going to sort an array of people first on their names, then on their ages. The method we use to sort an array is the static method Spell class Array :

Image

Remember that a static method is used by prefixing the method with the name of the class and not with the name of an instance of the class. The Spell has different signatures (it is overloaded). We'll use the following signature:

public static void Sort<T>(T[] tableau, IComparer<T> comparateur)

Spell a generic method where T denotes any type. The method receives two parameters:

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

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

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

The interface IComparer<T> has only one method. The method Compare :

  • receives two elements as parameters t1 and t2 type T
  • returns 1 if t1>t2, 0 if t1==t2, -1 if t1<t2. It's up to the developer to give meaning to the <, ==, > operators. For example, if p1 and p2 are two objects Person, we can say that p1>p2 if the name of p1 precedes the name of p2 in alphabetical order. We'll then sort in ascending order by name. If you want to sort by age, say p1>p2 if p1's age is greater than p2's age.
  • to sort in descending order, simply invert the +1 and -1 results

We know enough to sort a table of people. The program is as follows:


using System;
using System.Collections.Generic;
 
namespace Chap2 {
    class Program6 {
        static void Main(string[] args) {
             // a table of people
            Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
             // display
            Affiche("Tableau à trier", personnes1);
             // sort by name
            Array.Sort(personnes1, new CompareNoms());
             // display
            Affiche("Tableau après le tri selon les nom et prénom", personnes1);
             // sorted by age
            Array.Sort(personnes1, new CompareAges());
             // display
            Affiche("Tableau après le tri selon l'âge", personnes1);
        }
 
        static void Affiche(string texte, Personne[] personnes) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (Personne p in personnes) {
                Console.WriteLine(p);
            }
        }
    }
 
     // first and last name comparison class
    class CompareNoms : IComparer<Personne> {
        public int Compare(Personne p1, Personne p2) {
             // compare names
            int i = p1.Nom.CompareTo(p2.Nom);
            if (i != 0)
                return i;
             // equal names - first names are compared
            return p1.Prenom.CompareTo(p2.Prenom);
        }
    }
 
     // age comparison class
    class CompareAges : IComparer<Personne> {
        public int Compare(Personne p1, Personne p2) {
             // comparing ages
            if (p1.Age > p2.Age)
                return 1;
            else if (p1.Age == p2.Age)
                return 0;
            else
                return -1;
        }
    }
 
}
  • line 8: the table of people
  • line 12: sort the table of people by first and last name. The 2nd parameter of the generic method Spell is an instance of a CompareNoms implementing the generic IComparer<Person>.
  • lines 30-39: the class CompareNoms implementing the generic IComparer<Person>.
  • lines 31-38: implementation of the generic method int CompareTo(T,T) interface IComparer<T>. The method uses the String.CompareTo, presented byclipboard 3.3.5.4, to compare two strings.
  • line 16: sort the table of people by age. The 2nd parameter of the generic method Spell is an instance of a CompareAges implementing the generic IComparer<Person> and defined on lines 42-51.

The results are as follows:

Tableau à trier-----------------------------------
[claude, pollon, 25]
[valentine, germain, 35]
[paul, germain, 32]
Tableau après le tri selon les nom et prénom------
[paul, germain, 32]
[valentine, germain, 35]
[claude, pollon, 25]
Tableau après le tri selon l'âge------------------
[claude, pollon, 25]
[paul, germain, 32]
[valentine, germain, 35]

4.9. Namespaces

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

Console.WriteLine(...)

If we look at the definition of the Console


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

we discover that it's part of the System. This means that the Console should be designated by System.Console and we should actually write :

System.Console.WriteLine(...)

This is avoided by using a using :

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

We say we import namespace System with the clause using. When the compiler encounters the name of a class (ici Console) it will try to find it in the various namespaces imported by the using. Ici it will find the class Console in namespace System. Now let's note the second piece of information attached to the class Console :

Assembly: Mscorlib (in Mscorlib.dll)

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

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

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

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

If the source prog.cs uses the Person, class, the compiler won't know whether to take the Person of e1.dll or that of e2.dll. It will signal 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 classes Person will then be called E1.Person and E2.Personne. The customer must use either E1.Personne, or E2.Personne but not Person. The namespace removes any ambiguity.

To create a class in a namespace, write :

namespace EspaceDeNoms{
     // class definition
}

4.10. Sample application - V2

We repeat the tax calculation already studied in the previous chapter paragraphe 3.6 and we're now dealing with it using classes and interfaces. Let's recall the problem:

We propose to write a program to calculate a taxpayer's income tax. The simplified case is that of a taxpayer with only his salary to declare (2004 figures for 2003 income):

  • the number of employee shares is calculated nbParts=nbEnfants/2 +1 if unmarried, nbEnfants/2+2 if married, where nbEnfants is its number of children.
  • if he has at least three children, he gets half a share more
  • calculate your taxable income R=0.72*S where S is his annual salary
  • calculate your family coefficient QF=R/nbParts
  • calculate your tax I. Consider the following table :
4262
0
0
8382
0.0683
291.09
14753
0.1914
1322.92
23888
0.2826
2668.39
38868
0.3738
4846.98
47932
0.4262
6883.66
0
0.4809
9505.54

Each line has 3 fields. To calculate tax I, look for the first line where QF<=champ1. For example, if QF=5000 we find the line

    8382        0.0683        291.09

Tax I is then equal to 0.0683*R - 291.09*nbParts. If QF is such that the relation QF<=champ1 is never checked, then the coefficients of the last line are used. Ici :

    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 of the preceding array:


namespace Chap2 {
     // a tax bracket
    struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}
 

Then we define an interface IImpot able to calculate tax :


namespace Chap2 {
    interface IImpot {
        int calculer(bool marié, int nbEnfants, int salaire);
    }
}
  • line 3: tax calculation method based on three data: whether the taxpayer is married or not, number of children, salary

Next, we define an abstract class implementing this interface:


namespace Chap2 {
    abstract class AbstractImpot : IImpot {
 
         // tax brackets required to calculate tax
         // come from an external source
 
        protected TrancheImpot[] tranchesImpot;
 
         // tAX CALCULATION
        public int calculer(bool marié, int nbEnfants, int salaire) {
             // calculating the number of shares
            decimal nbParts;
            if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;
             // calculation of taxable income & family quota
            decimal revenu = 0.72M * salaire;
            decimal QF = revenu / nbParts;
             // tAX CALCULATION
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite) i++;
             // return result
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
         }//calculate
     }//class
 
}
  • line 2: the class AbstractImpot implements the IImpot.
  • line 7: annual tax calculation data in the form of a protected field. The class AbstractImpot doesn't know how this field will be initialized. It leaves this to the derived classes. This is why it is declared abstract (line 2), to prevent any instantiation.
  • lines 10-25: implementation of the calculate interface IImpot. Derived classes will not have to rewrite this method. The AbstractImpot serves as a factorization class for derived classes. This is where we put what is common to all derived classes.

A class implementing the IImpot class can be constructed by deriving the AbstractImpot. That's what we're doing now:


using System;
 
namespace Chap2 {
    class HardwiredImpot : AbstractImpot {
 
         // data tables for tax calculations
        decimal[] limites = { 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() {
                 // creation of tax bracket table
            tranchesImpot = new TrancheImpot[limites.Length];
                 // filling
            for (int i = 0; i < tranchesImpot.Length; i++) {
                tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
                }
        }
     }// class
}// namespace

The class HardwiredImpot defines, on lines 7-9, the hard data required for tax calculation. Its constructor (lines 11-18) uses this data to initialize the protected field tranchesImpot of the parent class AbstractImpot.

A test program could be as follows:


using System;
 
namespace Chap2 {
    class Program {
        static void Main() {
             // interactive Tax calculation program
            // l'user types three data into keyboard: married nbEnfants salary
             // the program then displays Tax payable
 
            const string syntaxe = "syntaxe : Marié NbEnfants Salaire\n"
                            + "Marié : o pour marié, n pour non marié\n"
                            + "NbEnfants : nombre d'enfants\n"
                            + "Salaire : salaire annuel en F";
 
             // creation of a IImpot object
            IImpot impot = new HardwiredImpot();
 
             // infinite loop
            while (true) {
                 // tax calculation parameters are requested
                Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
                string paramètres = Console.ReadLine().Trim();
                 // anything to do?
                if (paramètres == null || paramètres == "") break;
                 // check number of arguments in the input line
                string[] args = paramètres.Split(null);
                int nbParamètres = args.Length;
                if (nbParamètres != 3) {
                    Console.WriteLine(syntaxe);
                    continue;
                 }//if
                 // checking the validity of parameters
                 // married
                string marié = args[0].ToLower();
                if (marié != "o" && marié != "n") {
                    Console.WriteLine(syntaxe + "\nArgument marié incorrect : tapez o ou n");
                    continue;
                 }//if
                 // nbEnfants
                int nbEnfants = 0;
                bool dataOk = false;
                try {
                    nbEnfants = int.Parse(args[1]);
                    dataOk = nbEnfants >= 0;
                } catch {
                 }//if
                 // correct data?
                if (!dataOk) {
                    Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
                    continue;
                }
                 // salary
                int salaire = 0;
                dataOk = false;
                try {
                    salaire = int.Parse(args[2]);
                    dataOk = salaire >= 0;
                } catch {
                 }//try-catch
                 // correct data?
                if (!dataOk) {
                    Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
                    continue;
                }
                 // parameters are correct - Tax is calculated
                Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
                 // next taxpayer
             }//while
        }
    }
}

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

  • line 16: object creation tax implementing the IImpot. This object is obtained by instantiating a HardwiredImpot, a type that implements the IImpot. Note that we have not given the variable tax, the type HardwiredImpot but the IImpot. This indicates that we are only interested in the calculate object tax and not the rest.
  • lines 19-68: the tax calculation simulation loop
  • line 22: the three parameters required for the method calculate are requested 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 args.
  • line 66: calls the calculate object tax implementing the IImpot.

Here's an example of how to run the program:

Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :q s d
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Argument marié incorrect : tapez o ou n
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 d
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Argument salaire incorrect : tapez un entier positif ou nul
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :q s d f
syntaxe : Marié NbEnfants Salaire
Marié : o pour marié, n pour non marié
NbEnfants : nombre d'enfants
Salaire : salaire annuel en euros
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros