4. Classes, Structures, Interfaces
4.1. The Object by Example
4.1.1. General Overview
We will now explore object-oriented programming through examples. An object is an entity that contains data defining its state (referred to as fields, attributes, etc.) and functions (referred to as methods). An object is created based on a template called a class:
public class C1{
Type1 p1; // field p1
Type2 p2; // field p2
…
Type3 m3(…){ // method m3
…
}
Type4 m4(…){ // m4 method
…
}
…
}
From the previous class C1, we can create many objects O1, O2,… All will have the fields p1, p2,… and the methods m3, m4,… But they will have different values for their fields pi, thus each having its own state. If o1 is an object of type C1, o1.p1 denotes the property p1 of o1 and o1.m1 denotes the method m1 of O1.
Let’s consider a first object model: the Person class.
4.1.2. Creating the C# Project
In the previous examples, we had only a single source file in a project: Program.cs. From now on, we can have multiple source files in the same project. We’ll show you how to do this.
![]() |
In [1], create a new project. In [2], select Console Application. In [3], leave the default value. In [4], confirm. In [5], the generated project. The contents of Program.cs are as follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Program {
static void Main(string[] args) {
}
}
}
Let's save the project we created:
![]() |
In [1], select the save option. In [2], specify the folder where you want to save the project. In [3], give the project a name. In [5], indicate that you want to create a solution. A solution is a collection of projects. In [4], enter the name of the solution. In [6], confirm the save.
![]() |
In [1], the saved project. In [2], add a new item to the project.
![]() |
In [1], indicate that you want to add a class. In [2], enter the class name. In [3], confirm the information. In [4], project [01] has a new source file Personne.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Person {
}
}
We change the namespace of each source file to Chap2 and remove the imports of unnecessary namespaces:
using System;
namespace Chap2 {
class Person {
}
}
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
}
}
}
4.1.3. Definition of the Person class
The definition of the Person class in the source file [Person.cs] will be as follows:
using System;
namespace Chap2 {
public class Person {
// attributes
private string firstName;
private string lastName;
private int age;
// method
public void Initialize(string P, string N, int age) {
this.firstName = P;
this.lastName = N;
this.age = age;
}
// method
public void Identify() {
Console.WriteLine("[{0}, {1}, {2}]", firstName, lastName, age);
}
}
}
Here we have the definition of a class, which is a data type. When we create variables of this type, we call them objects or class instances. A class is therefore a template from which objects are constructed.
The members or fields of a class can be data (attributes), methods (functions), or properties. Properties are special methods used to retrieve or set the value of an object’s attributes. These fields can be accompanied by one of the following three keywords:
A private field is accessible only by the class's internal methods | |
A public field is accessible by any method, whether or not it is defined within the class | |
A protected field is accessible only by the class's internal methods or by a derived object (see the concept of inheritance later). |
Generally, a class’s data is declared private, while its methods and properties are declared public. This means that the user of an object (the programmer)
- will not have direct access to the object’s private data
- can call the object’s public methods, particularly those that provide access to its private data.
The syntax for declaring a class C is as follows:
public class C{
private data or method or private property;
public data or method or public property;
protected data or method or protected property;
}
The order in which private, protected, and public attributes are declared is arbitrary.
4.1.4. The Initialize method
Let’s return to our Person class declared as:
using System;
namespace Chap2 {
public class Person {
// attributes
private string firstName;
private string name;
private int age;
// method
public void Initialize(string p, string n, int age) {
this.firstName = p;
this.lastName = n;
this.age = age;
}
// method
public void Identify() {
Console.WriteLine("[{0}, {1}, {2}]", firstName, lastName, age);
}
}
}
What is the role of the Initialize method? Because lastName, firstName, and age are private members of the Person class, the statements:
are invalid. We must initialize an object of type Person using a public method. This is the role of the Initialize method. We will write:
The syntax p1.Initialize is valid because Initialize is public.
4.1.5. The new operator
The sequence of statements
is incorrect. The statement
declares p1 as a reference to an object of type Person. This object does not yet exist, so p1 is not initialized. It is as if we had written:
where we explicitly indicate with the keyword null that the variable p1 does not yet reference any object. When we then write
we call the Initialize method of the object referenced by p1. However, this object does not yet exist, and the compiler will report an error. To make p1 reference an object, we must write:
This creates an uninitialized Person object: the first_name and last_name attributes, which are references to String objects, will have the value null, and age will have the value 0. Thus, default initialization occurs. Now that p1 references an object, the initialization statement for this object
is valid.
4.1.6. The keyword this
Let’s look at the code for the Initialize method:
public void Initialize(string p, string n, int age) {
this.firstName = p;
this.lastName = n;
this.age = age;
}
The statement this.firstName = p means that the firstName property of the current object (this) is assigned the value p. The keyword this refers to the current object: the one in which the method being executed resides. How do we know this? Let’s look at how the object referenced by p1 is initialized in the calling program:
It is the Initialise method of the p1 object that is called. When we reference the this object within this method, we are actually referencing the p1 object. The Initialise method could also have been written as follows:
public void Initialize(string p, string n, int age) {
firstName = p;
lastName = n;
this.age = age;
}
When a method of an object references an attribute A of that object, the notation this.A is implied. It must be used explicitly when there is a conflict of identifiers. This is the case with the statement:
this.age=age;
where age refers to both an attribute of the current object and the age parameter received by the method. The ambiguity must then be resolved by referring to the age attribute as this.age.
4.1.7. A test program
Here is a short test program. It is written in the source file [Program.cs]:
using System;
namespace Chap2 {
class P01 {
static void Main() {
Person p1 = new Person();
p1.Initialize("Jean", "Dupont", 30);
p1.Identify();
}
}
}
Before running project [01], you may need to specify the source file to run:
![]() |
In the properties of project [01], specify the class to run in [1].
The results obtained upon execution are as follows:
4.1.8. Another method: Initialize
Let’s consider the Person class again and add the following method to it:
public void Initialize(Person p) {
firstName = p.firstName;
lastName = p.lastName;
age = p.age;
}
We now have two methods named Initialize: this is valid as long as they accept different parameters. That is the case here. The parameter is now a reference p to a person. The attributes of the person p are then assigned to the current object (this). Note that the Initialize method has direct access to the attributes of the object p even though they are of type private. This is always true: an object o1 of a class C always has access to the attributes of objects of the same class C.
Here is a test of the new Person class:
using System;
namespace Chap2 {
class Program {
static void Main() {
Person p1 = new Person();
p1.Initialize("Jean", "Dupont", 30);
p1.Identify();
Person p2 = new Person;
p2.Initialize(p1);
p2.Identify();
}
}
}
and its results:
4.1.9. Constructors of the Person class
A constructor is a method that bears the name of the class and is called when the object is created. It is generally used to initialize the object. It is a method that can accept arguments but returns no result. Its prototype or definition is not preceded by any type (not even void).
If a class C has a constructor that accepts n arguments args, the declaration and initialization of an object of this class can be done as follows:
or
When a class C has one or more constructors, one of these constructors must be used to create an object of that class. If a class C has no constructors, it has a default constructor, which is the constructor without parameters: public C(). The object’s attributes are then initialized with default values. This is what happened in the previous programs, where we wrote:
Let’s create two constructors for our Person class:
using System;
namespace Chap2 {
public class Person {
// attributes
private string firstName;
private string lastName;
private int age;
// constructors
public Person(String firstName, String lastName, int age) {
Initialize(p, n, age);
}
public Person(Person P) {
initialize(P);
}
// method
public void Initialize(string firstName, string lastName, int age) {
...
}
public void Initialize(Person p) {
...
}
// method
public void Identify() {
Console.WriteLine("[{0}, {1}, {2}]", firstName, lastName, age);
}
}
}
Our two constructors simply call the Initialize methods discussed earlier. Recall that when, in a constructor, we see the notation Initialize(p), for example, the compiler translates it to this.Initialize(p). In the constructor, the Initialize method is therefore called to operate on the object referenced by this, that is, the current object, the one being constructed.
Here is a short test program:
using System;
namespace Chap2 {
class Program {
static void Main() {
Person p1 = new Person("Jean", "Dupont", 30);
p1.Identify();
Person p2 = new Person(p1);
p2.Identify();
}
}
}
and the results obtained:
4.1.10. Object references
We are still using the same Person class. The test program becomes the following:
using System;
namespace Chap2 {
class Program2 {
static void Main() {
// p1
Person p1 = new Person("Jean", "Dupont", 30);
Console.Write("p1="); p1.Identify();
// p2 references the same object as p1
Person p2 = p1;
Console.Write("p2="); p2.Identify();
// p3 references an object that will be a copy of the object referenced by p1
Person p3 = new Person(p1);
Console.Write("p3="); p3.Identify();
// we change the state of the object referenced by p1
p1.SetProperties("Micheline", "Benoît", 67);
Console.Write("p1="); p1.Identify();
// since p2=p1, the object referenced by p2 must have changed state
Console.Write("p2="); p2.Identify();
// since p3 does not reference the same object as p1, the object referenced by p3 must not have changed
Console.Write("p3="); p3.Identify();
}
}
}
The results are as follows:
When declaring the variable p1 as
p1 references the object Person("Jean", "Dupont", 30) but is not the object itself. In C, we would say that it is a pointer, i.e., the address of the created object. If we then write:
It is not the Person("Jean", "Dupont", 30) object that is modified; it is the p1 reference that changes value. The Person("Jean", "Dupont", 30) object will be "lost" if it is not referenced by any other variable.
When we write:
We initialize the pointer p2: it "points" to the same object (it refers to the same object) as the pointer p1. Thus, if we modify the object "pointed to" (or referenced) by p1, we also modify the one referenced by p2.
When we write:
a new Person object is created. This new object will be referenced by p3. If we modify the object "pointed to" (or referenced) by p1, we do not modify the one referenced by p3 in any way. This is what the results show.
4.1.11. Passing Object Reference Parameters
In the previous chapter, we studied how function parameters are passed when they represent a simple C# type represented by a .NET structure. Let’s see what happens when the parameter is an object reference:
using System;
using System.Text;
namespace Chap1 {
class P12 {
public static void Main() {
// Example 4
StringBuilder sb0 = new StringBuilder("test0"), sb1 = new StringBuilder("test1"), sb2 = new StringBuilder("test2"), sb3;
Console.WriteLine("In calling function before call: sb0={0}, sb1={1}, sb2={2}", sb0, sb1, sb2);
ChangeStringBuilder(sb0, sb1, ref sb2, out sb3);
Console.WriteLine("In calling function after call: sb0={0}, sb1={1}, sb2={2}, sb3={3}", sb0, sb1, sb2, sb3);
}
private static void ChangeStringBuilder(StringBuilder sbf0, StringBuilder sbf1, ref StringBuilder sbf2, out StringBuilder sbf3) {
Console.WriteLine("Start of called function: sbf0={0}, sbf1={1}, sbf2={2}", sbf0, sbf1, sbf2);
sbf0.Append("*****");
sbf1 = new StringBuilder("test1*****");
sbf2 = new StringBuilder("test2*****");
sbf3 = new StringBuilder("test3*****");
Console.WriteLine("End of function call: sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3);
}
}
}
- Line 8: defines 3 StringBuilder objects. A StringBuilder object is similar to a string object. When manipulating a string object, a new string object is returned. Thus, in the code snippet:
Line 1 creates a string object in memory, and s is its address. On line 2, s.ToUpperCase() creates another string object in memory. Thus, between lines 1 and 2, s has changed its value (it now points to the new object). The StringBuilder class, on the other hand, allows you to modify a string without creating a second object. This is the example given above:
- line 8: 4 references [sb0, sb1, sb2, sb3] to StringBuilder objects
- line 10: are passed to the ChangeStringBuilder method with different modes: sb0 and sb1 with the default mode, sb2 with the ref keyword, and sb3 with the out keyword.
- lines 15–22: a method with formal parameters [sbf0, sbf1, sbf2, sbf3]. The relationships between the formal parameters sbf1 and the actual parameters sbi are as follows:
- sbf0 and sb0 are, at the start of the method, two distinct references pointing to the same object (passing addresses by value)
- The same applies to sbf1 and sb1
- sbf2 and sb2 are, at the start of the method, the same reference to the same object (ref keyword)
- sbf3 and sb3 are, after the method has executed, the same reference to the same object (out keyword)
The results obtained are as follows:
Explanations:
- sb0 and sbf0 are two separate references to the same object. This object was modified via sbf0—line 3. This modification can be seen via sb0—line 4.
- sb1 and sbf1 are two distinct references to the same object. sbf1’s value is modified within the method and now points to a new object—line 3. This does not change the value of sb1, which continues to point to the same object—line 4.
- sb2 and sbf2 are the same reference to the same object. sbf2’s value is modified within the method and now points to a new object—line 3. Since sbf2 and sb2 are one and the same entity, sb2’s value has also been modified, and sb2 points to the same object as sbf2—lines 3 and 4.
- Before the method call, sb3 had no value. After the method, sb3 receives the value of sbf3. We therefore have two references to the same object—lines 3 and 4
4.1.12. Temporary objects
In an expression, you can explicitly call an object’s constructor: the object is created, but you do not have access to it (to modify it, for example). This temporary object is created for the purpose of evaluating the expression and then discarded. The memory space it occupied will be automatically reclaimed later by a program called a "garbage collector," whose role is to reclaim memory space occupied by objects that are no longer referenced by program data.
Consider the following new test program:
using System;
namespace Chap2 {
class Program {
static void Main() {
new Person(new Person("Jean", "Dupont", 30)).Identify();
}
}
}
and let's modify the constructors of the Person class so that they display a message:
// constructors
public Person(String p, String n, int age) {
Console.WriteLine("Person constructor (string, string, int)");
Initialize(p, n, age);
}
public Person(Person P) {
Console.Out.WriteLine("Person constructor (Person)");
Initialize(P);
}
We get the following results:
showing the successive construction of the two temporary objects.
4.1.13. Methods for reading and writing private attributes
We add the necessary methods to the Person class to read or modify the state of the objects' attributes:
using System;
namespace Chap2 {
public class Person {
// attributes
private string firstName;
private string lastName;
private int age;
// constructors
public Person(String firstName, String lastName, int age) {
Console.WriteLine("Person constructor (string, string, int)");
Initialize(p, n, age);
}
public Person(Person p) {
Console.Out.WriteLine("Person constructor (Person)");
Initialize(p);
}
// method
public void Initialize(string p, string n, int age) {
this.firstName = p;
this.lastName = n;
this.age = age;
}
public void Initialize(Person p) {
firstName = p.firstName;
lastName = p.lastName;
age = p.age;
}
// accessors
public String GetFirstName() {
return firstName;
}
public String GetLastName() {
return lastName;
}
public int GetAge() {
return age;
}
//accessors
public void SetFirstName(String P) {
this.firstName = P;
}
public void SetLastName(String N) {
this.lastName = N;
}
public void SetAge(int age) {
this.age = age;
}
// method
public void Identify() {
Console.WriteLine("[{0}, {1}, {2}]", firstName, lastName, age);
}
}
}
We test the new class with the following program:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Person p = new Person("Jean", "Michelin", 34);
Console.Out.WriteLine("p=(" + p.GetFirstName() + "," + p.GetLastName() + "," + p.GetAge() + ")");
p.SetAge(56);
Console.Out.WriteLine("p=(" + p.GetFirstName() + "," + p.GetLastName() + "," + p.GetAge() + ")");
}
}
}
and we get the results:
4.1.14. Properties
There is another way to access a class’s attributes: by creating properties. These allow us to manipulate private attributes as if they were public.
Consider the following Person class, where the previous accessors and mutators have been replaced by read-write properties:
using System;
namespace Chap2 {
public class Person {
// attributes
private string firstName;
private string lastName;
private int age;
// constructors
public Person(String firstName, String lastName, int age) {
Initialize(p, n, age);
}
public Person(Person p) {
initialize(p);
}
// method
public void Initialize(string firstName, string lastName, int age) {
this.firstName = p;
this.lastName = n;
this.age = age;
}
public void Initialize(Person p) {
firstName = p.firstName;
lastName = p.lastName;
age = p.age;
}
// properties
public string FirstName {
get { return firstName; }
set {
// Is the first name valid?
if (value == null || value.Trim().Length == 0) {
throw new Exception("Invalid first name (" + value + ")");
} else {
firstName = value;
}
}//if
}//firstName
public string LastName {
get { return lastName; }
set {
// Is the name valid?
if (value == null || value.Trim().Length == 0) {
throw new Exception("Invalid name (" + value + ")");
} else { name = value; }
}//if
}//name
public int Age {
get { return age; }
set {
// Is the age valid?
if (value >= 0) {
age = value;
} else
throw new Exception("Invalid age (" + value + ")");
}//if
}//age
// method
public void Identify() {
Console.WriteLine("[{0}, {1}, {2}]", firstName, lastName, age);
}
}
}
A property allows you to read (get) or set the value of an attribute. A property is declared as follows:
where Type must be the type of the attribute managed by the property. It can have two methods called get and set. The get method is usually responsible for returning the value of the attribute it manages (it could return something else; there is nothing preventing it from doing so). The set method receives a parameter called value, which it normally assigns to the attribute it manages. It can use this opportunity to validate the received value and, if necessary, throw an exception if the value is invalid. This is what is done here.
How are these get and set methods called? Consider the following test program:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Person p = new Person("Jean", "Michelin", 34);
Console.Out.WriteLine("p=(" + p.FirstName + "," + p.LastName + "," + p.Age + ")");
p.Age = 56;
Console.Out.WriteLine("p=(" + p.FirstName + "," + p.LastName + "," + p.Age + ")");
try {
p.Age = -4;
} catch (Exception ex) {
Console.Error.WriteLine(ex.Message);
}//try-catch
}
}
}
In the statement
Console.Out.WriteLine("p=(" + p.FirstName + "," + p.LastName + "," + p.Age + ")");
we are trying to obtain the values of the FirstName, LastName, and Age properties of the person p. It is the get method of these properties that is then called and returns the value of the attribute they manage.
In the statement
We want to set the value of the Age property. The set method of this property is then called. It will receive 56 in its value parameter.
A property P of a class C that defines only the get method is said to be read-only. If c is an object of class C, the operation c.P=value will be rejected by the compiler.
Executing the previous test program yields the following results:
Properties therefore allow us to manipulate private attributes as if they were public. Another feature of properties is that they can be used in conjunction with a constructor according to the following syntax:
This syntax is equivalent to the following code:
The order of the properties does not matter. Here is an example.
A new constructor with no parameters is added to the Person class:
public Person() {
}
The constructor does not initialize the object’s members. This is called the default constructor. It is used when the class does not define any constructors.
The following code creates and initializes (line 6) a new Person using the syntax shown above:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Person p2 = new Person { Age = 7, FirstName = "Arthur", LastName = "Martin" };
Console.WriteLine("p2=({0},{1},{2})", p2.FirstName, p2.LastName, p2.Age);
}
}
}
In line 6 above, the parameterless constructor Person() is used. In this particular case, we could also have written
Person p2 = new Person() { Age = 7, FirstName = "Arthur", LastName = "Martin" };
but the parentheses for the parameterless Person() constructor are not required in this syntax.
The results of the execution are as follows:
In many cases, the get and set methods of a property simply read from and write to a private field without any further processing. In this scenario, we can use an automatic property declared as follows:
The private field associated with the property is not declared. It is automatically generated by the compiler. It can only be accessed via its property. Thus, instead of writing:
private string firstName;
...
// associated property
public string FirstName {
get { return firstName; }
set {
// Is the first name valid?
if (value == null || value.Trim().Length == 0) {
throw new Exception("Invalid first name (" + value + ")");
} else {
firstName = value;
}
}//if
}//firstName
we can write:
without declaring the private field firstName. The difference between the two properties above is that the first one checks the validity of the first name in the set, while the second one performs no validation.
Using the automatic FirstName property is equivalent to declaring a public FirstName field:
One might wonder if there is a difference between the two declarations. Declaring a class field as public is discouraged. This violates the concept of encapsulating an object’s state, which should be private and exposed through public methods.
If the automatic property is declared virtual, it can then be overridden in a child class:
class Class1 {
public virtual string Prop { get; set; }
}
class Class2 : Class1 {
public override string Prop { get { return base.Prop; } set {... } }
}
In line 2 above, the child class Class2 can include code in the setter that checks the validity of the value assigned to the automatic property base.Prop of the parent class Class1.
4.1.15. Class methods and attributes
Suppose we want to count the number of Person objects created in an application. We could manage a counter ourselves, but we risk overlooking temporary objects created here and there. It would seem safer to include an instruction in the Person class constructors that increments a counter. The problem is passing a reference to this counter so that the constructor can increment it: we need to pass a new parameter to them. We can also include the counter in the class definition. Since it is an attribute of the class itself and not of a particular instance of that class, we declare it differently using the static keyword:
private static long nbPeople;
To reference it, we write Person.nbPeople to show that it is an attribute of the Person class itself. Here, we have created a private attribute that cannot be accessed directly from outside the class. We therefore create a public property to provide access to the class attribute nbPeople. To return the value of nbPeople, the get method of this property does not need a specific Person object: indeed, nbPeople is an attribute of the entire class. Therefore, we need a property that is also declared as static:
public static long NbPeople {
get { return nbPeople; }
}
which will be called from outside using the syntax Person.NbPersonnes. Here is an example.
The Person class becomes the following:
using System;
namespace Chap2 {
public class Person {
// class attributes
private static long numberOfPeople;
public static long numberOfPeople {
get { return numberOfPeople; }
}
// instance attributes
private string lastName;
private string lastName;
private int age;
// constructors
public Person(String firstName, String lastName, int age) {
Initialize(p, n, age);
numberOfPeople++;
}
public Person(Person p) {
initialize(p);
numberOfPeople++;
}
...
}
Lines 20 and 24: the constructors increment the static field on line 7.
With the following program:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Person p1 = new Person("John", "Smith", 30);
Person p2 = new Person(p1);
new Person(p1);
Console.WriteLine("Number of people created: " + Person.NumberOfPeople);
}
}
}
The following results are obtained:
4.1.16. An array of people
An object is a piece of data like any other, and as such, multiple objects can be grouped together in an array:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
// an array of people
Person[] friends = new Person[3];
friends[0] = new Person("Jean", "Dupont", 30);
friends[1] = new Person("Sylvie", "Vartan", 52);
friends[2] = new Person("Neil", "Armstrong", 66);
// display
foreach (Person friend in friends) {
friend.Identify();
}
}
}
}
- Line 7: creates an array of 3 elements of type Person. These 3 elements are initialized here with the value null, meaning they do not reference any objects. Again, strictly speaking, we refer to it as an "array of objects" when it is actually just an array of object references. The creation of the object array, which is an object itself (indicated by the use of `new`), does not create any objects of the type of its elements: this must be done later.
- lines 8–10: creation of the 3 objects of type Person
- lines 12–14: display of the contents of the friends array
The following results are obtained:
4.2. Inheritance by example
4.2.1. Overview
Here we discuss the concept of inheritance. The purpose of inheritance is to "customize" an existing class so that it meets our needs. Suppose we want to create a Teacher class: a teacher is a specific person ( ). They have attributes that other people do not have: the subject they teach, for example. But they also have the attributes of any person: first name, last name, and age. A teacher is therefore a full member of the Person class but has additional attributes. Rather than writing a Teacher class from scratch, we would prefer to build upon the existing Person class and adapt it to the specific characteristics of teachers. It is the concept of inheritance that allows us to do this.
To express that the Teacher class inherits the properties of the Person class, we would write:
The Person class is called the parent (or base) class, and the Teacher class is called the derived (or child) class. A Teacher object has all the characteristics of a Person object: it has the same attributes and methods. These attributes and methods of the parent class are not repeated in the definition of the child class; we simply list the attributes and methods added by the child class:
We assume that the Person class is defined as follows:
using System;
namespace Chap2 {
public class Person {
// class attributes
private static long numberOfPeople;
public static long numberOfPeople {
get { return numberOfPeople; }
}
// instance attributes
private string firstName;
private string lastName;
private int age;
// constructors
public Person(String firstName, String lastName, int age) {
LastName = lastName;
FirstName = firstName;
Age = age;
nbPeople++;
Console.WriteLine("Person constructor (string, string, int)");
}
public Person(Person p) {
LastName = p.LastName;
FirstName = p.FirstName;
Age = p.Age;
nbPeople++;
Console.WriteLine("Person constructor (Person)");
}
// properties
public string FirstName {
get { return firstName; }
set {
// Is the first name valid?
if (value == null || value.Trim().Length == 0) {
throw new Exception("Invalid first name (" + value + ")");
} else {
firstName = value;
}
}//if
}//firstName
public string LastName {
get { return lastName; }
set {
// Is the name valid?
if (value == null || value.Trim().Length == 0) {
throw new Exception("Invalid name (" + value + ")");
} else { name = value; }
}//if
}//name
public int Age {
get { return age; }
set {
// Is the age valid?
if (value >= 0) {
age = value;
} else
throw new Exception("Invalid age (" + value + ")");
}//if
}//age
// property
public string Identity {
get { return String.Format("[{0}, {1}, {2}]", firstName, lastName, age);}
}
}
}
The Identify method has been replaced by the read-only Identity property, which identifies the person. We create a Teacher class that inherits from the Person class:
using System;
namespace Chap2 {
class Teacher : Person {
// attributes
private int section;
// constructor
public Teacher(string firstName, string lastName, int age, int section)
: base(firstName, lastName, age) {
// Store the section using the Section property
Section = section;
// tracking
Console.WriteLine("Construction Teacher(string, string, int, int)");
}//constructor
// Section property
public int Section {
get { return section; }
set { section = value; }
}// Section
}
}
The Teacher class adds to the methods and attributes of the Person class:
- line 4: the Teacher class derives from the Person class
- line 6: a `section` attribute representing the section number to which the teacher belongs within the teaching staff (roughly one section per subject). This private attribute is accessible via the public `Section` property in lines 18–21
- line 9: a new constructor that initializes all of a teacher’s attributes
4.2.2. Creating a Teacher object
A child class does not inherit constructors from its parent class. It must therefore define its own constructors. The constructor for the Teacher class is as follows:
// constructor
public Teacher(string firstName, string lastName, int age, int section)
: base(firstName, lastName, age) {
// store the section
Section = section;
// follow-up
Console.WriteLine("Constructing Teacher(string, string, int, int)");
}//constructor
The declaration
public Teacher(string firstName, string lastName, int age, int section)
: base(firstName, lastName, age) {
states that the constructor takes four parameters—firstName, lastName, age, and section—and passes three of them (firstName, lastName, age) to its base class, in this case the Person class. We know that this class has a constructor Person(string, string, int) that will allow us to construct a person using the passed parameters (firstName, lastName, age). Once the base class has been constructed, the construction of the Teacher object continues by executing the body of the constructor:
// we store the section
Section = section;
Note that on the left side of the = sign, it is not the object’s section attribute that was used, but the Section property associated with it. This allows the constructor to take advantage of any validity checks that this method might perform. This avoids having to place these checks in two different places: the constructor and the property.
In summary, the constructor of a derived class:
- passes to its base class the parameters it needs to construct itself
- uses the other parameters to initialize its own attributes
One might have preferred to write:
// constructor
public Teacher(string firstName, string lastName, int age, int section){
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.class = class;
}
That's impossible. The Person class has declared its three fields—firstName, lastName, and age—as private. Only objects of the same class have direct access to these fields. All other objects, including child objects as in this case, must use public methods to access them. This would have been different if the Person class had declared the three fields as protected: it would then have allowed derived classes to have direct access to the three fields. In our example, using the parent class’s constructor was therefore the correct solution, and this is the standard approach: when constructing a child object, we first call the parent object’s constructor and then complete the initializations specific to the child object (section in our example).
Let’s try a first test program [Program.cs]:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Teacher("Jean", "Dupont", 30, 27).Identity);
}
}
}
This program simply creates a Teacher object (new) and identifies it. The Teacher class does not have an Identity method, but its parent class does have one, which is also public: through inheritance, it becomes a public method of the Teacher class.
The entire project is as follows:
![]() |
The results obtained are as follows:
We can see that:
- a Person object (line 1) was constructed before the Teacher object (line 2)
- the identity obtained is that of the Person object
4.2.3. Redefining a method or property
In the previous example, we had the identity of the Person part of the teacher, but some information specific to the Teacher class (the section) is missing. We therefore need to write a property that allows us to identify the teacher:
using System;
namespace Chap2 {
class Teacher : Person {
// attributes
private int section;
// constructor
public Teacher(string firstName, string lastName, int age, int section)
: base(firstName, lastName, age) {
// store the section via the Section property
Section = section;
// follow-up
Console.WriteLine("Constructing Teacher(string, string, int, int)");
}//constructor
// Section property
public int Section {
get { return section; }
set { section = value; }
}// section
// Identity property
public new string Identity {
get { return String.Format("Teacher[{0},{1}]", base.Identity, Section); }
}
}
}
Lines 24–26: The Identity property of the Teacher class relies on the Identity property of its parent class (base.Identity) (line 25) to display its "Person" portion, then supplements it with the Section field, which is specific to the Teacher class. Note the declaration of the Identity property:
public new string Identity{
Let E be a Teacher object. This object contains a Person object:
![]() |
The Identity property is defined in both the Teacher class and its parent class Person. In the child class Teacher, the Identity property must be preceded by the keyword new to indicate that we are redefining a new Identity property for the Teacher class.
public new string Identity{
The Teacher class now has two Identity properties:
- the one inherited from the parent Person class
- its own
If E is a Teacher object, E.Identity refers to the Identity property of the Teacher class. We say that the Identity property of the child class redefines or hides the Identity property of the parent class. In general, if O is an object and M is a method, to execute the method O.M, the system searches for a method M in the following order:
- in the class of object O
- in its parent class, if it has one
- in the parent class of its parent class, if it exists
- etc…
Inheritance therefore allows methods and properties with the same name in the parent class to be redefined in the child class. This is what allows the child class to be adapted to its own needs. Combined with polymorphism, which we will discuss shortly, the redefinition of methods and properties is the main benefit of inheritance.
Let’s consider the same test program as before:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Teacher("Jean", "Dupont", 30, 27).Identity);
}
}
}
The results obtained this time are as follows:
4.2.4. Polymorphism
Consider a class hierarchy: C0 ← C1 ← C2 ← … ← Cn
where Ci ← Cj indicates that class Cj is derived from class Ci. This implies that class Cj has all the characteristics of class Ci plus additional ones. Let Oi be objects of type Ci. It is valid to write:
Indeed, by inheritance, class Cj has all the characteristics of class Ci plus additional ones. Therefore, an object Oj of type Cj contains within it an object of type Ci. The operation
means that Oi is a reference to the object of type Ci contained within the object Oj.
The fact that a variable Oi of class Ci can in fact refer not only to an object of class Ci but to any object derived from class Ci is called polymorphism: the ability of a variable to refer to different types of objects.
Let’s take an example and consider the following class-independent (static) function:
We could just as easily write
as
In the latter case, the formal parameter p of type Person in the static method Display will receive a value of type Teacher. Since the Teacher type derives from the Person type, this is valid.
4.2.5. Overriding and Polymorphism
Let’s complete our Display method:
public static void Display(Person p) {
// displays the identity of p
Console.WriteLine(p.Identity);
}//display
The p.Identity property returns a string that identifies the Person object p. What happens in the previous example if the parameter passed to the Display method is an object of type Teacher:
Teacher e = new Teacher(...);
Display(e);
Let's look at the following example:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// a teacher
Teacher e = new Teacher("Lucile", "Dumas", 56, 61);
Display(e);
// a person
Person p = new Person("Jean", "Dupont", 30);
Display(p);
}
// displays
public static void Display(Person p) {
// displays p's identity
Console.WriteLine(p.Identity);
}//display
}
}
The results are as follows:
The execution shows that the statement p.Identity (line 17) evaluated the Identity property of a Person each time: first (line 7) for the person contained in the Teacher e, then (line 10) for the Person p itself. It did not adapt to the object actually passed as a parameter to Display. We would have preferred to have the full identity of the Teacher e. For this to happen, the notation p.Identity would have had to refer to the Identity property of the object actually pointed to by p rather than the Identity property of the "Person" part of the object actually pointed to by p.
This result can be achieved by declaring Identity as a virtual property in the Person base class:
public virtual string Identity {
get { return String.Format("[{0}, {1}, {2}]", firstName, lastName, age); }
}
The virtual keyword makes Identity a virtual property. This keyword can also be applied to methods. Derived classes that redefine a virtual property or method must then use the override keyword instead of new to qualify their redefined property/method. Thus, in the Teacher class, the Identity property is redefined as follows:
public override string Identity {
get { return String.Format("Teacher[{0},{1}]", base.Identity, Section); }
}
The previous program then produces the following results:
This time, on line 3, we did get the teacher’s full identity. Let’s now redefine a method rather than a property. The object class (C# alias for System.Object) is the “parent” class of all C# classes. So when we write:
we are implicitly writing:
The System.Object class defines a virtual method called ToString:
![]() |
The ToString method returns the name of the class to which the object belongs, as shown in the following example:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// a teacher
Console.WriteLine(new Teacher("Lucile", "Dumas", 56, 61).ToString());
// a person
Console.WriteLine(new Person("Jean", "Dupont", 30).ToString());
}
}
}
The results are as follows:
Note that although we have not overridden the ToString method in the Person and Teacher classes, we can see that the ToString method of the Object class was able to display the actual class name of the object.
Let’s override the ToString method in the Person and Teacher classes:
// ToString method
public override string ToString() {
return Identity;
}
The definition is the same in both classes. Consider the following test program:
using System;
namespace Chap2 {
class Program3 {
public static void Main() {
// a teacher
Teacher e = new Teacher("Lucile", "Dumas", 56, 61);
Display(e);
// a person
Person p = new Person("Jean", "Dupont", 30);
Display(p);
}
// display
public static void Display(Person p) {
// displays p's identity
Console.WriteLine(p);
}//Display
}
}
Let’s focus on the Display method, which takes a Person p as a parameter. On line 15, the WriteLine method of the Console class has no variant that accepts a Person-type parameter. Among the various variants of WriteLine, there is one that accepts an Object type as a parameter. The compiler will use this method, WriteLine(Object o), because this signature means that the parameter o can be of type Object or a derived type. Since Object is the base class of all classes, any object can be passed as a parameter to WriteLine, and therefore an object of type Person or Teacher. The WriteLine(Object o) method writes o.ToString() to the Out output stream. Since the ToString method is virtual, if the object o (of type Object or a derived type) has overridden the ToString method, that method will be used instead. This is the case here with the Person and Teacher classes.
This is what the execution results show:
4.3. Redefining the meaning of an operator for a class
4.3.1. Introduction
Consider the statement
where op1 and op2 are two operands. It is possible to redefine the meaning of the + operator. If the operand op1 is an object of class C1, a static method must be defined in class C1 with the following signature:
When the compiler encounters the statement
It then translates it to C1.operator+(op1,op2). The type returned by the operator method is important. Consider the operation op1+op2+op3. The compiler translates it to (op1+op2)+op3. Let res12 be the result of op1+op2. The next operation performed is res12+op3. If res12 is of type C1, it will also be translated as C1.operator+(res12,op3). This allows operations to be chained together.
We can also redefine unary operators that have only a single operand. Thus, if op1 is an object of type C1, the operation op1++ can be redefined by a static method of the C1 class:
What has been said here is true for most operators, with a few exceptions:
- the == and != operators must be redefined together
- the operators &&, ||, [], (), +=, -=, ... cannot be redefined
4.3.2. An example
We create a PersonList class derived from the ArrayList class. This class implements a dynamic list and is presented in the following chapter. From this class, we use only the following elements:
- the method L.Add(Object o), which allows us to add an object o to the list L. Here, the object o will be a Person object.
- the property L.Count, which returns the number of elements in list L
- the notation L[i], which returns the i-th element of the list L
The PersonList class will inherit all the attributes, methods, and properties of the ArrayList class. Its definition is as follows:
using System;
using System.Collections;
using System.Text;
namespace Chap2 {
class PeopleList : ArrayList{
// Redefining the + operator to add a person to the list
public static PersonList operator +(PersonList l, Person p) {
// Add Person p to PeopleList l
l.Add(p);
// return the ListOfPeople l
return l;
}// operator +
// ToString
public override string ToString() {
// returns (el1, el2, ..., eln)
// opening parenthesis
StringBuilder listToString = new StringBuilder("(");
// iterate through the list of people (this)
for (int i = 0; i < Count - 1; i++) {
listeToString.Append(this[i]).Append(",");
}//for
// last element
if (Count != 0) {
listeToString.Append(this[Count-1]);
}
// closing parenthesis
listeToString.Append(""));
// we need to return a string
return listToString.ToString();
}//ToString
}
}
- line 6: the PeopleList class derives from the ArrayList class
- lines 8–13: definition of the + operator for the operation l + p, where l is of type ListOfPeople and p is of type Person or a derived class.
- line 10: the person p is added to the list l. The Add method of the parent class ArrayList is used here.
- line 12: the reference to the list l is returned so that + operators can be chained together, such as in l + p1 + p2. The operation l+p1+p2 will be interpreted (operator precedence) as (l+p1)+p2. The operation l+p1 will return the reference l. The operation (l+p1)+p2 then becomes l+p2, which adds person p2 to the list of people l.
- Line 16: We redefine the ToString method to display a list of people in the form (person1, person2, ...), where personi is itself the result of the ToString method of the Person class.
- Line 19: We use a StringBuilder object. This class is more suitable than the String class whenever you need to perform multiple operations on a string, in this case appending characters. This is because every operation on a String object returns a new String object, whereas the same operations on a StringBuilder object modify the existing object without creating a new one. We use the Append method to concatenate the strings.
- Line 21: We iterate through the elements of the list of people. This list is referred to here as `this`. It is the current object on which the `ToString` method is executed. The `Count` property is a property of the parent `ArrayList` class.
- Line 22: Element number i of the current list this is accessible via the notation this[i]. Again, this is a property of the ArrayList class. Since we are adding strings, the method this[i].ToString() will be used. Since this method is virtual, the ToString method of the this object, of type Person or a derived class, will be used.
- Line 31: We need to return an object of type string (line 16). The StringBuilder class has a ToString method that allows us to convert from a StringBuilder type to a string type.
Note that the ListOfPeople class does not have a constructor. In this case, we know that the constructor
will be used. This constructor does nothing other than call the parameterless constructor of its parent class:
A test class might look like this:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// a list of people
ListOfPeople l = new ListOfPeople();
// adding people
l = l + new Person("jean", "martin", 10) + new Person("pauline", "leduc", 12);
// display
Console.WriteLine("l=" + l);
l = l + new Teacher("camille", "germain", 27, 60);
Console.WriteLine("l=" + l);
}
}
}
- line 7: creating a list of people l
- line 9: adding 2 people using the + operator
- line 12: adding a teacher
- lines 11 and 13: use of the redefined method ListOfPeople.ToString().
The results:
4.4. Defining an indexer for a class
Here, we continue to use the PersonList class. If l is a PersonList object, we want to be able to use the notation l[i] to refer to the person at index i in the list l, both for reading (Person p = l[i]) and for writing (l[i] = new Person(...)).
To be able to write l[i], where l[i] refers to a Person object, we need to define the following this method in the PeopleList class:
public Person this[int i] {
get { ... }
set { ... }
}
We call the method this[int i] an indexer because it gives meaning to the expression obj[i], which resembles array notation even though obj is not an array but an object. The get method of the this method of the obj object is called when we write variable = obj[i], and the set method when we write obj[i] = value.
The PeopleList class derives from the ArrayList class, which itself has an indexer:
There is a conflict between the this method of the PersonList class:
public Person this[int i]
and the *this method of the *ArrayList class
public object this[int i]
because they have the same name and accept the same parameter type (int). To indicate that the this method of the ListeDePersonnes class "hides" the method of the same name in the *ArrayList class, we must add the new* keyword to the declaration of the *ListeDePersonnes* indexer. We will therefore write:
public new Person this[int i]{
get { ... }
set { ... }
}
Let’s complete this method. The this.get method is called when we write variable = l[i], for example, where l is of type PersonList. We must then return the person at index i from the list l. This is done using the notation base[i], which returns the object at index i from the ArrayList class underlying the PersonList class. Since the returned object is of type Object, a cast to the Person class is necessary.
public new Person this[int i]{
get { return (Person) base[i]; }
set { ... }
}
The set method is called when we write l[i] = p, where p is a Person. This involves assigning the person p to element i of the list l.
public new Person this[int i]{
get { ... }
set { base[i] = value; }
}
Here, the Person p represented by the keyword value is assigned to element i of the base class ArrayList.
The indexer for the PersonList class will therefore be as follows:
public new Person this[int i] {
get { return (Person) base[i]; }
set { base[i] = value; }
}
Now, we also want to be able to write Person p = l["name"], i.e., index the list l not by an element number but by a person's name. To do this, we define a new indexer:
// indexer via a name
public int this[string name] {
get {
// search for the person
for (int i = 0; i < Count; i++) {
if (((Person)base[i]).Name == name)
return i;
}//for
return -1;
}//get
}
The first line
public int this[string name]
indicates that the PeopleList class is indexed by a string named name and that the result of l[name] is an integer. This integer will be the position in the list of the person with the name name, or -1 if that person is not in the list. We define only the get property, thereby preventing the assignment l["name"]=value, which would have required the definition of the set property. The new keyword is not required in the indexer declaration because the base class ArrayList does not define an indexer this[string].
In the body of the get method, we iterate through the list of people looking for the name passed as a parameter. If we find it at position i, we return i; otherwise, we return -1.
The previous test program is completed as follows:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// a list of people
ListOfPeople l = new ListOfPeople();
// adding people
l = l + new Person("jean", "martin", 10) + new Person("pauline", "leduc", 12);
// display
Console.WriteLine("l=" + l);
l = l + new Teacher("Camille", "Germain", 27, 60);
Console.WriteLine("l=" + l);
// change element 1
l[1] = new Person("franck", "gallon", 5);
// display element 1
Console.WriteLine("l[1]=" + l[1]);
// display list l
Console.WriteLine("l=" + l);
// search for people
string[] names = { "martin", "germain", "xx" };
for (int i = 0; i < names.Length; i++) {
int name = l[names[i]];
if (nom != -1)
Console.WriteLine("Person(" + names[i] + ")=" + l[nom]);
else
Console.WriteLine("Person(" + names[i] + ") does not exist");
}//for
}
}
}
Running this code produces the following results:
4.5. Structures
The C# structure is analogous to the structure in the C language and is very similar to the concept of a class. A structure is defined as follows:
Despite similarities in their declarations, there are significant differences between classes and structures. For example, the concept of inheritance does not exist with structures. If we are writing a class that is not intended to be derived from another, what are the differences between structures and classes that will help us choose between the two? Let’s use the following example to find out:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// a structure sp1
SPerson sp1;
sp1.Name = "paul";
sp1.Age = 10;
Console.WriteLine("sp1=SPerson(" + sp1.Name + "," + sp1.Age + ")");
// a structure sp2
SPerson sp2 = sp1;
Console.WriteLine("sp2 = SPerson(" + sp2.Name + "," + sp2.Age + ")");
// sp2 is modified
sp2.Name = "nicole";
sp2.Age = 30;
// Checking sp1 and sp2
Console.WriteLine("sp1 = Person(" + sp1.Name + "," + sp1.Age + ")");
Console.WriteLine("sp2=SPerson(" + sp2.Name + "," + sp2.Age + ")");
// an object op1
CPerson op1 = new CPersonne();
op1.Name = "paul";
op1.Age = 10;
Console.WriteLine("op1 = CPerson(" + op1.Name + "," + op1.Age + ")");
// an object op2
CPerson op2 = op1;
Console.WriteLine("op2 = CPerson(" + op2.Name + "," + op2.Age + ")");
// op2 is modified
op2.Name = "nicole";
op2.Age = 30;
// Check op1 and op2
Console.WriteLine("op1 = CPerson(" + op1.Name + "," + op1.Age + ")");
Console.WriteLine("op2=CPerson(" + op2.Name + "," + op2.Age + ")");
}
}
// SPersonne structure
struct SPerson {
public string Name;
public int Age;
}
// CPerson class
class CPerson {
public string Name;
public int Age;
}
}
- lines 38–41: a structure with two public fields: Name, Age
- lines 44–47: a class with two public fields: Name, Age
If we run this program, we get the following results:
Where we previously used a Person class, we now use an SPersonne structure:
struct SPerson {
public string Name;
public int Age;
}
The structure does not have a constructor here. It could have one, as we will show later. By default, it always has a constructor without parameters, in this case SPersonne().
- Line 7 of the code: the declaration
SPersonne sp1;
is equivalent to the statement:
SPersonne sp1 = new SPersonne();
A structure (Name,Age) is created, and the value of sp1 is this structure itself. In the case of a class, the object (Name,Age) must be created explicitly using the new operator (line 22):
CPerson op1 = new CPerson();
The previous statement creates a CPerson object (roughly equivalent to our structure) and the value of p1 is then the address (the reference) of this object.
To summarize
- in the case of the structure, the value of sp1 is the structure itself
- in the case of a class, the value of op1 is the address of the created object
![]() |
When in the program we write line 12:
SPersonne sp2 = sp1;
A new structure sp2(Name, Age) is created and initialized with the value of sp1, i.e., the structure itself.
![]() |
The structure of sp1 is duplicated in sp2 [1]. This is a copy by value. Now consider the statement on line 27:
CPerson op2 = op1;
In the case of classes, the value of op1 is copied into op2, but since this value is actually the address of the object, the object itself is not duplicated [2].
In the case of the structure [1], if we modify the value of sp2, we do not modify the value of sp1, as the program demonstrates. In the case of the object [2], if we modify the object pointed to by op2, the one pointed to by op1 is modified since they are the same. This is also demonstrated by the program’s results.
We can therefore conclude from these explanations that:
- the value of a variable of type structure is the structure itself
- the value of a variable of type object is the address of the object it points to
Once this fundamental difference is understood, the structure proves to be very similar to a class, as shown in the following new example:
using System;
namespace Chap2 {
// structure SPerson
struct SPerson {
// private attributes
private string name;
private int age;
// properties
public string Name {
get { return name; }
set { name = value; }
}//name
public int Age {
get { return age; }
set { age = value; }
}//age
// Constructor
public SPersonne(string name, int age) {
this.name = name;
this.age = age;
}//constructor
// ToString
public override string ToString() {
return "SPerson(" + Name + "," + Age + ")";
}//ToString
}//structure
}//namespace
- lines 8-9: two private fields
- lines 12–20: the associated public properties
- lines 23–26: a constructor is defined. Note that the parameterless constructor SPersonne() is always present and does not need to be declared. Its declaration is rejected by the compiler. In the constructor on lines 23–26, one might be tempted to initialize the private fields name and age via their public properties Name and Age. This is rejected by the compiler. The structure’s methods cannot be used during its construction.
- Lines 29–31: redefinition of the ToString method.
A test program might look like this:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// a person p1
SPerson p1 = new SPersonne();
p1.Name = "paul";
p1.Age = 10;
Console.WriteLine("p1={0}", p1);
// a person p2
SPerson p2 = p1;
Console.WriteLine("p2=" + p2);
// p2 is modified
p2.Name = "nicole";
p2.Age = 30;
// Check p1 and p2
Console.WriteLine("p1=" + p1);
Console.WriteLine("p2=" + p2);
// a person p3
SPerson p3 = new SPerson("amandin", 18);
Console.WriteLine("p3=" + p3);
// a person p4
SPerson p4 = new SPerson { Name = "x", Age = 10 };
Console.WriteLine("p4=" + p4);
}
}
}
- Line 7: We are required to explicitly use the parameterless constructor because there is another constructor in the structure. If the structure had no constructor, the statement
SPerson p1;
would have been sufficient to create an empty structure.
- lines 8–9: the structure is initialized via its public properties
- line 10: the p1.ToString method will be used in the WriteLine.
- line 21: creation of a structure using the SPersonne(string, int) constructor
- line 24: creation of a structure using the parameterless constructor SPersonne() with, within curly braces, initialization of the private fields via their public properties.
The following execution results are obtained:
The only notable difference here between a structure and a class is that with a class, the objects p1 and p2 would have pointed to the same object at the end of the program.
4.6. Interfaces
An interface is a set of method or property prototypes that forms a contract. A class that decides to implement an interface commits to providing an implementation of all the methods defined in the interface. The compiler verifies this implementation.
Here, for example, is the definition of the System.Collections.IEnumerator interface:
public interface System.Collections.IEnumerator
{
// Properties
Object Current { get; }
// Methods
bool MoveNext();
void Reset();
}
The properties and methods of the interface are defined only by their signatures. They are not implemented (have no code). It is the classes that implement the interface that provide code for the interface's methods and properties.
- Line 1: Class C implements the IEnumerator interface. Note that the colon (:) used for implementing an interface is the same as the one used for class inheritance.
- Lines 3–5: The implementation of the methods and properties of the IEnumerator interface.
Consider the following interface:
namespace Chap2 {
public interface IStats {
double Mean { get; }
double StandardDev();
}
}
The IStats interface provides:
- a read-only property Average: to calculate the average of a series of values
- a StandardDev method: to calculate the standard deviation
Note that it is not specified anywhere which series of values is being referred to. It could be the average grades of a class, the monthly average sales of a particular product, the average temperature in a given location, etc. This is the principle of interfaces: we assume the existence of methods in the object but not that of specific data.
A first implementation class for the IStats interface could be a class used to store the grades of students in a class for a given subject. A student would be characterized by the following Student structure:
public struct Student {
public string LastName { get; set; }
public string LastName { get; set; }
}//Student
The student would be identified by their last name and first name. Lines 2–3 contain the automatic properties for these two attributes.
A grade would be characterized by the following Grade structure:
public struct Grade {
public Student Student { get; set; }
public double Value { get; set; }
}//Grade
The grade would be identified by the student being graded and the grade itself. Lines 2–3 contain the automatic properties for these two attributes.
The grades of all students in a given subject are collected in the following GradeTable class:
using System;
using System.Text;
namespace Chap2 {
public class GradeTable : IStats {
// attributes
public string Subject { get; set; }
public Note[] Notes { get; set; }
public double Average { get; private set; }
private double standardDeviation;
// constructor
public GradeArray(string subject, Grade[] grades) {
// Storing via public properties
Subject = subject;
Grades = grades;
// Calculate the average of the grades
double sum = 0;
for (int i = 0; i < Grades.Length; i++) {
sum += Grades[i].Value;
}
if (Grades.Length != 0) Average = sum / Grades.Length;
else Average = -1;
// standard deviation
double squared = 0;
for (int i = 0; i < Notes.Length; i++) {
squares += Math.Pow((Notes[i].Value - Average), 2);
}//for
if (Scores.Length != 0)
standardDeviation = Math.Sqrt(squares / Notes.Length);
else standardDeviation = -1;
}//constructor
public double StandardDeviation() {
return standardDeviation;
}
// ToString
public override string ToString() {
StringBuilder value = new StringBuilder(String.Format("subject={0}, grades=(", Subject));
int i;
// concatenate all grades
for (i = 0; i < Notes.Length - 1; i++) {
value.Append("[").Append(Grades[i].Student.FirstName).Append(",").Append(Grades[i].Student.LastName).Append(",").Append(Grades[i].Score).Append("],");
};
//last grade
if (Notes.Length != 0) {
value.Append("[").Append(Grades[i].Student.FirstName).Append(",").Append(Grades[i].Student.LastName).Append(",").Append(Grades[i].Grade).Append("]");
}
value.Append(")");
// end
return value.ToString();
}//ToString
}//class
}
- line 6: the TableauDeNotes class implements the IStats interface. It must therefore implement the Average property and the StandardDeviation method. These are implemented on lines 10 (Average) and 35–37 (StandardDeviation)
- lines 8–10: three automatic properties
- line 8: the subject for which the object stores grades
- line 9: the array of student grades (Student, Grade)
- line 10: the average of the grades—a property implementing the Average property of the IStats interface.
- line 11: field storing the standard deviation of the grades—the associated get method StandardDev on lines 35–37 implements the StandardDev method of the IStats interface.
- line 9: the grades are stored in an array. This array is passed to the constructor in lines 14–33 when the GradeArray class is instantiated.
- Lines 14–33: the constructor. Here, we assume that the scores passed to the constructor will not change thereafter. Therefore, we use the constructor to immediately calculate the mean and standard deviation of these scores and store them in the fields on lines 10–11. The mean is stored in the private field underlying the automatic property Moyenne in line 10, and the standard deviation in the private field in line 11.
- Line 10: The get method of the automatic property Average will return the underlying private field.
- Lines 35–37: The `EcartType` method returns the value of the private field from line 11.
There are a few subtleties in this code:
- Line 23: The set method of the Moyenne property is used to perform the assignment. This method was declared private on line 10 so that assigning a value to the Moyenne property is only possible within the class.
- Lines 40–54: use a StringBuilder object to construct the string representing the TableauDeNotes object in order to improve performance. It should be noted that code readability suffers significantly as a result. This is the flip side of the coin.
In the previous class, grades were stored in an array. It was not possible to add a new grade after the TableauDeNotes object was constructed. We now propose a second implementation of the IStats interface, called ListeDeNotes, where grades are stored in a list, allowing grades to be added after the initial construction of the ListeDeNotes object.
The code for the GradeList class is as follows:
using System;
using System.Text;
using System.Collections.Generic;
namespace Chap2 {
public class NoteList : IStats {
// properties
public string Subject { get; set; }
public List<Grade> Grades { get; set; }
public double average = -1;
public double standardDeviation = -1;
// constructor
public ListOfGrades(string subject, List<Grade> grades) {
// Storing via public properties
Subject = subject;
Notes = notes;
}//constructor
// Add a grade
public void Add(Grade grade) {
// add the grade
Notes.Add(note);
// reset average and standard deviation
mean = -1;
standardDev = -1;
}
// ToString
public override string ToString() {
StringBuilder value = new StringBuilder(String.Format("subject={0}, grades=(", Subject));
int i;
// concatenate all grades
for (i = 0; i < Grades.Count - 1; i++) {
value.Append("[").Append(Grades[i].Student.FirstName).Append(",").Append(Grades[i].Student.LastName).Append(",").Append(Grades[i].Score).Append("],");
};
//last grade
if (Notes.Count != 0) {
value.Append("[").Append(Grades[i].Student.FirstName).Append(",").Append(Grades[i].Student.LastName).Append(",").Append(Grades[i].Score).Append("]");
}
value.Append(")");
// end
return value.ToString();
}//ToString
// average of grades
public double Average {
get {
if (average != -1) return average;
// Calculate the average of the scores
double sum = 0;
for (int i = 0; i < Grades.Count; i++) {
sum += Grades[i].Value;
}
// Calculate the average
if (Scores.Count != 0) average = sum / Scores.Count;
return average;
}
}
public double StandardDeviation() {
// standard deviation
if (standardDev != -1) return standardDev;
// mean
double mean = Mean;
double sumsOfSquares = 0;
for (int i = 0; i < Scores.Count; i++) {
squares += Math.Pow((Scores[i].Value - average), 2);
}//for
// calculate the standard deviation
if (Notes.Count != 0)
standardDev = Math.Sqrt(squares / Notes.Count);
return standardDeviation;
}
}//class
}
- line 7: the GradeList class implements the IStats interface
- line 10: grades are now stored in a list rather than an array
- line 11: the automatic Average property of the GradeArray class has been dropped here in favor of a private field average, line 11, associated with the read-only public property Average in lines 48–60
- lines 22–28: we can now add a grade to those already stored, which was not possible previously.
- Lines 15–19: Consequently, the average and standard deviation are no longer calculated in the constructor but in the interface methods themselves: Average (lines 48–60) and StandardDeviation (62–76). However, the recalculation is only triggered if the mean and standard deviation are different from -1 (lines 50 and 64).
A test class could look like this:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// Some students & English grades
Student[] students1 = { new Student { FirstName = "Paul", LastName = "Martin" }, new Student { FirstName = "Maxime", LastName = "Germain" }, new Student { FirstName = "Berthine", LastName = "Samin" } };
Grade[] grades1 = { new Grade { Student = students1[0], Score = 14 }, new Grade { Student = students1[1], Score = 16 }, new Grade { Student = students1[2], Score = 18 } };
// which we store in a GradesArray object
GradesTable English = new GradesTable("English", grades1);
// display average and standard deviation
Console.WriteLine("{2}, Average={0}, StandardDeviation={1}", English.Average, English.StandardDeviation(), English);
// we put the students and the subject into a GradesList object
GradesList French = new GradesList("French", new List<Grade>(grades1));
// Display average and standard deviation
Console.WriteLine("{2}, Average={0}, StandardDeviation={1}", French.Average, French.StandardDeviation(), French);
// add a grade
French.Add(new Note { Student = new Student { FirstName = "Jérôme", LastName = "Jaric" }, Score = 10 });
// display average and standard deviation
Console.WriteLine("{2}, Mean={0}, Standard Deviation={1}", French.Mean, French.StandardDeviation(), French);
}
}
}
- Line 8: Creating an array of students using the parameterless constructor and initializing via public properties
- line 9: creation of an array of grades using the same technique
- line 11: a GradeArray object for which the mean and standard deviation are calculated line 13
- line 15: a GradeList object for which the mean and standard deviation are calculated line 17. The List<Grade> class has a constructor that accepts an object implementing the IEnumerable<Grade> interface. The grades1 array implements this interface and can be used to construct the List<Grade> object.
- line 19: adding a new note
- line 21: recalculating the mean and standard deviation
The results of the execution are as follows:
In the previous example, two classes implement the IStats interface. That said, the example does not demonstrate the value of the IStats interface. Let’s rewrite the test program as follows:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// some students & English grades
Student[] students1 = { new Student { FirstName = "Paul", LastName = "Martin" }, new Student { FirstName = "Maxime", LastName = "Germain" }, new Student { FirstName = "Berthine", LastName = "Samin" } };
Grade[] grades1 = { new Grade { Student = students1[0], Score = 14 }, new Grade { Student = students1[1], Score = 16 }, new Grade { Student = students1[2], Score = 18 } };
// which we store in a GradesArray object
GradesTable English = new GradesTable("English", grades1);
// Display mean and standard deviation
DisplayStats(English);
// we put the students and the subject into a GradeList object
GradesList French = new GradesList("French", new List<Grade>(grades1));
// Display average and standard deviation
DisplayStats(French);
// add a grade
French.Add(new Note { Student = new Student { FirstName = "Jérôme", LastName = "Jaric" }, Score = 10 });
// display average and standard deviation
DisplayStats(French);
}
// Display the mean and standard deviation of an IStats type
static void DisplayStats(IStats values) {
Console.WriteLine("{2}, Mean={0}, StandardDeviation={1}", values.Mean, values.StandardDeviation(), values);
}
}
}
- Lines 25–27: The static method DisplayStats takes an IStats type as a parameter, which is an interface type. This means that the actual parameter can be any object that implements the IStats interface. When using data of an interface type, this means that only the interface methods implemented by the data will be used. The rest is ignored. This is a property similar to the polymorphism seen with classes. If a set of classes Ci that are not related to each other by inheritance (and thus cannot use inheritance polymorphism) has a set of methods with the same signature, it may be useful to group these methods into an interface I that all the relevant classes would implement. Instances of these classes Ci can then be used as actual parameters of functions that accept a formal parameter of type I, i.e., functions that use only the methods of the Ci objects defined in the interface I and not the specific attributes and methods of the various Ci classes.
- line 13: the AfficheStats method is called with a TableauDeNotes type that implements the IStats interface
- line 17: same as above, but with a type called GradeList
The results of the execution are identical to those of the previous one.
A variable can be of the type of an interface. Thus, we can write:
The declaration on line 1 indicates that stats1 is an instance of a class that implements the IStats interface. This declaration means that the compiler will only allow access to the interface’s methods within stats1: the Moyenne property and the EcartType method.
Finally, note that interfaces can be implemented multiple times, i.e., we can write
where the Ij are interfaces.
4.7. Abstract classes
An abstract class is a class that cannot be instantiated. You must create derived classes that can be instantiated.
Abstract classes can be used to factor out code across a family of classes. Let’s examine the following case:
using System;
namespace Chap2 {
abstract class User {
// fields
private string login;
private string password;
private string role;
// constructor
public User(string login, string password) {
// save the information
this.login = login;
this.password = password;
// authenticate the user
role = identify();
// Is the user identified?
if (role == null) {
throw new UnknownUserException(String.Format("[{0},{1}]", login, password));
}
}
// toString
public override string ToString() {
return String.Format("User[{0},{1},{2}]", login, password, role);
}
// identify
abstract public string identify();
}
}
- lines 11–21: the constructor of the User class. This class stores information about a web application’s user. The application has various types of users authenticated via a username and password (lines 6–7). These credentials are verified against an LDAP service for some users, against a database management system for others, and so on...
- lines 13-14: the authentication information is stored
- Line 16: It is verified by an `identify` method. Because the identification method is not yet defined, it is declared abstract on line 29 using the `abstract` keyword. The `identify` method returns a string specifying the user’s role (basically, what they are authorized to do). If this string is a null pointer, an exception is thrown on line 19.
- Line 4: Because it has an abstract method, the class itself is declared abstract using the abstract keyword.
- Line 29: The abstract method `identify` has no definition. Derived classes will provide one.
- Lines 24–26: The ToString method, which identifies an instance of the class.
Here, we assume that the developer wants to control the construction of instances of the User class and derived classes, perhaps because they want to ensure that an exception of a certain type is thrown if the user is not recognized (line 19). Derived classes will be able to rely on this constructor. To do so, they must provide the identify method.
The UnknownUserException class is as follows:
using System;
namespace Chap2 {
class UnknownUserException : Exception {
public ExceptionUtilisateurInconnu(string message) : base(message){
}
}
}
- Line 3: It derives from the Exception class
- lines 4–6: It has a single constructor that takes an error message as a parameter. This is passed to the parent class (line 5), which has the same constructor.
We now derive the User class from the Administrator child class:
namespace Chap2 {
class Administrator : User {
// constructor
public Administrator(string login, string password)
: base(login, password) {
}
// identify
public override string identify() {
// LDAP authentication
// ...
return "admin";
}
}
}
- lines 4–6: the constructor simply passes the parameters it receives to its parent class
- lines 9-12: the identify method of the Administrator class. We assume that an administrator is identified by an LDAP system. This method overrides the identify method of its parent class. Because it overrides an abstract method, there is no need to use the override keyword.
We now derive the User class from the Observer child class:
namespace Chap2 {
class Observer : User{
// constructor
public Observer(string login, string password)
: base(login, password) {
}
//identify
public override string identify() {
// DB identification
// ...
return "observer";
}
}
}
- lines 4–6: the constructor simply passes the parameters it receives to its parent class
- lines 9–13: the `identify` method of the Observer class. We assume that an observer is identified by verifying their identification data in a database.
Ultimately, the Administrator and Observer objects are instantiated by the same constructor, that of the parent User class. This constructor will use the identify method provided by these classes.
A third class, Unknown, also derives from the User class:
namespace Chap2 {
class Unknown : User{
// constructor
public Unknown(string login, string password)
: base(login, password) {
}
//identify
public override string identify() {
// user not known
// ...
return null;
}
}
}
- Line 13: The `identify` method sets the pointer to `null` to indicate that the user was not recognized.
A test program might look like this:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Observer("observer", "mdp1"));
Console.WriteLine(new Administrator("admin", "mdp2"));
try {
Console.WriteLine(new Unknown("xx", "yy"));
} catch (UnknownUserException e) {
Console.WriteLine("Unknown user: " + e.Message);
}
}
}
}
Note that in lines 6, 7, and 9, the [User].ToString() method is used by the WriteLine method.
The results of the execution are as follows:
4.8. Classes, interfaces, generic methods
Suppose we want to write a method that swaps two integers. This method could be as follows:
public static void Swap1(ref int value1, ref int value2){
// swap the references value1 and value2
int temp = value2;
value2 = value1;
value1 = temp;
}
Now, if we wanted to swap two references to Person objects, we would write:
public static void Swap2(ref Person value1, ref Person value2){
// swap the references value1 and value2
Person temp = value2;
value2 = value1;
value1 = temp;
}
What distinguishes the two methods is the type T of the parameters: int in Swap1, Person in Swap2. Generic classes and interfaces address the need for methods that differ only in the type of some of their parameters.
With a generic class, the Swap method could be rewritten as follows:
namespace Chap2 {
class Generic1<T> {
public static void Swap(ref T value1, ref T value2){
// swap the references value1 and value2
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- Line 2: The Generic1 class is parameterized by a type denoted T. You can give it any name you want. This type T is then reused in the class on lines 3 and 5. We say that the Generic1 class is a generic class.
- line 3: defines the two references to a type T to be swapped
- line 5: the temporary variable temp is of type T.
A test program for the class could look like this:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic1<int>.Swap(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic1<string>.Swap(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Person
Person p1 = new Person("jean", "clu", 20), p2 = new Person("pauline", "dard", 55);
Generic1<Person>.Swap(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
- Line 8: When using a generic class parameterized by types T1, T2, ... these types must be "instantiated." On line 8, we use the static Swap method of type Generic1<int> to indicate that the references passed to the Swap method are of type int.
- Line 12: We use the static Echanger method of type Generic1<string> to indicate that the references passed to the Echanger method are of type string.
- Line 16: We use the static method `Echanger` of type `Generic1<Personne>` to indicate that the references passed to the `Echanger` method are of type `Personne`.
The results of the execution are as follows:
The *Echanger* method could also have been written as follows:
namespace Chap2 {
class Generic2 {
public static void Swap<T>(ref T value1, ref T value2){
// swap the references value1 and value2
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- line 2: the Generic2 class is no longer generic
- line 3: the static method Echanger is generic
The test program then becomes the following:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic2.Swap<int>(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic2.Swap<string>(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Person
Person p1 = new Person("jean", "clu", 20), p2 = new Person("pauline", "dard", 55);
Generic2.Swap<Person>(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
- Lines 8, 12, and 16: The Swap method is called by specifying the parameter types within <>. In fact, the compiler can deduce which variant of the Swap method to use based on the actual parameter types. Therefore, the following syntax is valid:
Generic2.Swap(ref i1, ref i2);
...
Generic2.Swap(ref s1, ref s2);
...
Generic2.Swap(ref p1, ref p2);
Lines 1, 3, and 5: the variant of the Swap method being called is no longer specified. The compiler is able to infer it from the nature of the actual parameters used.
Constraints can be placed on generic parameters:

Consider the following new generic Swap method:
namespace Chap2 {
class Generic3 {
public static void Swap<T>(ref T value1, ref T value2) where T : class {
// swap the references value1 and value2
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- Line 3: Type T must be a reference (class, interface)
Consider the following test program:
using System;
namespace Chap2 {
class Program4 {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic3.Swap<int>(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic3.Swap(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Person
Person p1 = new Person("jean", "clu", 20), p2 = new Person("pauline", "dard", 55);
Generic3.Swap(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
The compiler reports an error on line 8 because the type int is not a class or an interface; it is a struct:

Consider the following new generic method Exchange:
namespace Chap2 {
class Generic4 {
public static void Swap<T>(ref T element1, ref T element2) where T : Interface1 {
// retrieve the values of the two elements
int value1 = element1.Value();
int value2 = element2.Value();
// if the first element is greater than the second element, swap the elements
if (value1 > value2) {
T temp = element2;
element2 = element1;
element1 = temp;
}
}
}
}
- line 3: The type T must implement the Interface1 interface. This interface has a Value method, used in lines 5 and 6, which returns the value of the object of type T.
- lines 8–12: the two references element1 and element2 are swapped only if the value of element1 is greater than the value of element2.
The Interface1 interface is as follows:
namespace Chap2 {
interface Interface1 {
int Value();
}
}
It is implemented by the following Class1 class:
using System;
using System.Threading;
namespace Chap2 {
class Class1 : Interface1 {
// object value
private int value;
// constructor
public Class1() {
// wait 1 ms
Thread.Sleep(1);
// random value between 0 and 99
value = new Random(DateTime.Now.Millisecond).Next(100);
}
// private field accessor value
public int Value() {
return value;
}
// instance state
public override string ToString() {
return value.ToString();
}
}
}
- line 5: Class1 implements the Interface1 interface
- line 7: the value of an instance of Class1
- lines 10–14: the value field is initialized with a random value between 0 and 99
- lines 18–20: the Value method of the Interface1 interface
- lines 23–25: the ToString method of the class
The Interface1 interface is also implemented by the Class2 class:
using System;
namespace Chap2 {
class Class2 : Interface1 {
// object values
private int value;
private String s;
// constructor
public Class2(String s) {
this.s = s;
value = s.Length;
}
// private field accessor value
public int Value() {
return value;
}
// instance state
public override string ToString() {
return s;
}
}
}
- line 4: Class2 implements the Interface1 interface
- line 6: the value of an instance of Class2
- lines 10–13: the value field is initialized with the length of the string passed to the constructor
- lines 16–18: the Value method of the Interface1 interface
- lines 21–22: the ToString method of the class
A test program might look like this:
using System;
namespace Chap2 {
class Program5 {
static void Main(string[] args) {
// exchange instances of type Class1
Class1 c1, c2;
for (int i = 0; i < 5; i++) {
c1 = new Class1();
c2 = new Class1();
Console.WriteLine("Before swap --> c1={0},c2={1}", c1, c2);
Generic4.Swap(ref c1, ref c2);
Console.WriteLine("After swap --> c1={0},c2={1}", c1, c2);
}
// swap instances of type Class2
Class2 c3, c4;
c3 = new Class2("xxxxxxxxxxxxxx");
c4 = new Class2("xx");
Console.WriteLine("Before swap --> c3={0},c4={1}", c3, c4);
Generic4.Swap(ref c3, ref c4);
Console.WriteLine("Before swap --> c3={0},c4={1}", c3, c4);
}
}
}
- lines 8–14: instances of type Class1 are swapped
- lines 16–22: instances of type Class2 are swapped
The results of the execution are as follows:
To illustrate the concept of a gener e interface, we will sort an array of people first by their names, then by their ages. The method that allows us to sort an array is the static Sort method of the Array class:

Recall that a static method is used by prefixing the method with the class name, not the name of an instance of the class. The Sort method has different signatures (it is overloaded). We will use the following signature:
Sort is a generic method where T denotes any type. The method takes two parameters:
- T[] array: the array of elements of type T to be sorted
- IComparer<T> comparator: a reference to an object implementing the IComparer<T> interface.
IComparer<T> is a generic interface defined as follows:
The IComparer<T> interface has only a single method: the Compare method:
- takes two elements t1 and t2 of type T as parameters
- returns 1 if t1 > t2, 0 if t1 == t2, and -1 if t1 < t2. It is up to the developer to define the meaning of the operators <, ==, and >. For example, if p1 and p2 are two Person objects, we can say that p1 > p2 if p1’s name comes before p2’s name in alphabetical order. We will then have a sort in ascending order by the people's names. If we want a sort by age, we will say that p1 > p2 if the age of p1 is greater than the age of p2.
- To sort in descending order, simply reverse the results of +1 and -1
We now know enough to sort an array of people. The program is as follows:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program6 {
static void Main(string[] args) {
// an array of people
Person[] people1 = { new Person("claude", "pollon", 25), new Person("valentine", "germain", 35), new Person("paul", "germain", 32) };
// display
Display("Array to sort", people1);
// Sort by name
Array.Sort(people1, new CompareNames());
// display
Display("Array after sorting by first and last name", people1);
// Sort by age
Array.Sort(people1, new CompareAges());
// display
Display("Array after sorting by age", people1);
}
static void Display(string text, Person[] people) {
Console.WriteLine(text.PadRight(50, '-'));
foreach (Person p in people) {
Console.WriteLine(p);
}
}
}
// class for comparing people's first and last names
class CompareNames : IComparer<Person> {
public int Compare(Person p1, Person p2) {
// compare the names
int i = p1.Name.CompareTo(p2.Name);
if (i != 0)
return i;
// If the last names are equal, compare the first names
return p1.FirstName.CompareTo(p2.FirstName);
}
}
// class for comparing people's ages
class CompareAges : IComparer<Person> {
public int Compare(Person p1, Person p2) {
// compare ages
if (p1.Age > p2.Age)
return 1;
else if (p1.Age == p2.Age)
return 0;
else
return -1;
}
}
}
- line 8: the array of people
- line 12: sorting the array of people by first and last name. The second parameter of the generic Sort method is an instance of the CompareNames class, which implements the generic interface IComparer<Person>.
- lines 30–39: the CompareNames class implementing the generic interface IComparer<Person>.
- lines 31–38: implementation of the generic method int CompareTo(T, T) of the IComparer<T> interface. The method uses the String.CompareTo method, presented in section 3.3.5.4, to compare two strings.
- Line 16: Sorting the array of people by age. The second parameter of the generic Sort method is an instance of the CompareAges class, which implements the generic IComparer<Person> interface and is defined on lines 42–51.
The results of the execution are as follows:
4.9. Namespaces
To write a line on the screen, we use the statement
If we look at the definition of the Console class
Namespace: System
Assembly: Mscorlib (in Mscorlib.dll)
we see that it is part of the System namespace. This means that the Console class should be referred to as System.Console, and we should actually write:
We avoid this by using a using clause:
We say that we import the System namespace using the using clause. When the compiler encounters the name of a class (here, Console), it will look for it in the various namespaces imported by the using clauses. Here, it will find the Console class in the System namespace. Now let’s note the second piece of information associated with the Console class:
This line indicates in which "assembly" the definition of the Console class is located. When compiling outside of Visual Studio and you need to specify the references to the various DLLs containing the classes you need to use, this information can be useful. To reference the DLLs required to compile a class, you write:
where csc is the C# compiler. When creating a class, you can create it within a namespace. The purpose of these namespaces is to avoid name conflicts between classes, for example when they are sold. Consider two companies, E1 and E2, distributing classes packaged in the DLLs e1.dll and e2.dll, respectively. Suppose a client C purchases these two sets of classes, in which both companies have defined a Person class. Client C compiles a program as follows:
If the source file prog.cs uses the Person class, the compiler will not know whether to use the Person class from e1.dll or the one from e2.dll. It will report an error. If company E1 takes care to create its classes in a namespace called E1 and company E2 in a namespace called E2, the two Person classes will then be named E1.Person and E2.Person. The client must use either E1.Person or E2.Person in its classes, but not Person. The namespace resolves the ambiguity.
To create a class in a namespace, write:
4.10. Example Application - V2
We will revisit the tax calculation already covered in the previous chapter, section 3.6, and now implement it using classes and interfaces. Let’s review the problem:
We aim to write a program to calculate a taxpayer’s tax. We consider the simplified case of a taxpayer who has only their salary to report (2004 figures for 2003 income):
- We calculate the number of tax brackets for the employee as nbParts = nbEnfants/2 + 1 if they are unmarried, and nbEnfants/2 + 2 if they are married, where nbEnfants is the number of children.
- if they have at least three children, they receive an additional half-share
- We calculate their taxable income R = 0.72 * S, where S is their annual salary
- We calculate their family coefficient QF = R / nbParts
- we calculate his tax I. Consider the following table:
4262 | 0 | 0 |
8382 | 0.0683 | 291.09 |
14,753 | 0.1914 | 1,322.92 |
23,888 | 0.2826 | 2,668.39 |
38,868 | 0.3738 | 4,846.98 |
47,932 | 0.4262 | 6,883.66 |
0 | 0.4809 | 9505.54 |
Each row has 3 fields. To calculate tax I, find the first row where QF <= field1. For example, if QF = 5000, the row found will be
Tax I is then equal to 0.0683*R - 291.09*nbParts. If QF is such that the condition QF<=field1 is never met, then the coefficients from the last row are used. Here:
which gives tax I = 0.4809*R - 9505.54*nbParts.
First, we define a structure capable of encapsulating a row from the previous table:
namespace Chap2 {
// a tax bracket
struct TaxBracket {
public decimal Limit { get; set; }
public decimal TaxRate { get; set; }
public decimal CoeffN { get; set; }
}
}
Then we define an interface IImpot capable of calculating the tax:
namespace Chap2 {
interface IImpot {
int calculate(bool married, int numChildren, int salary);
}
}
- Line 3: the method for calculating the tax based on three pieces of data: the taxpayer's marital status, the number of children, and the salary
Next, we define an abstract class that implements this interface:
namespace Chap2 {
abstract class AbstractTax : IImpot {
// the tax brackets needed to calculate the tax
// come from an external source
protected TaxBracket[] taxBrackets;
// tax calculation
public int calculate(bool married, int numChildren, int salary) {
// calculation of the number of shares
decimal numberOfShares;
if (married) nbParts = (decimal)nbChildren / 2 + 2;
else nbParts = (decimal)nbChildren / 2 + 1;
if (numberOfChildren >= 3) numberOfShares += 0.5M;
// Calculate taxable income & family quotient
decimal income = 0.72M * salary;
decimal familyQuotient = income / nbParts;
// calculate tax
taxBrackets[taxBrackets.Length - 1].Limit = QF + 1;
int i = 0;
while (QF > taxBrackets[i].Limit) i++;
// return result
return (int)(income * taxBrackets[i].TaxRate - numberOfShares * taxBrackets[i].TaxRate);
}//calculate
}//class
}
- Line 2: The AbstractImpot class implements the IImpot interface.
- Line 7: The annual tax calculation data is stored in a protected field. The AbstractImpot class does not know how this field will be initialized. It leaves this task to the derived classes. This is why it is declared abstract (line 2) to prevent any instantiation.
- Lines 10–25: The implementation of the `calculer` method from the `IImpot` interface. Derived classes will not need to rewrite this method. The `AbstractImpot` class thus serves as a factorization class for the derived classes. It contains what is common to all derived classes.
A class implementing the IImpot interface can be created by deriving from the AbstractImpot class. This is what we will do now:
using System;
namespace Chap2 {
class HardwiredImpot : AbstractImpot {
// data arrays required to calculate the tax
decimal[] limits = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
public HardwiredImpot() {
// Create the tax bracket array
taxBrackets = new TaxBracket[limits.Length];
// populate
for (int i = 0; i < taxBrackets.Length; i++) {
taxBrackets[i] = new TaxBracket { Limit = limits[i], RateR = rateR[i], RateN = rateN[i] };
}
}
}// class
}// namespace
The HardwiredImpot class hard-codes the data needed to calculate the tax in lines 7–9. Its constructor (lines 11–18) uses this data to initialize the protected field taxBrackets of the parent class AbstractImpot.
A test program might look like this:
using System;
namespace Chap2 {
class Program {
static void Main() {
// Interactive tax calculation program
// the user enters three pieces of data via the keyboard: married, number of children, salary
// the program then displays the tax due
const string syntax = "syntax: Married No. of Children Salary\n"
+ "Married: y for married, n for not married\n"
+ "No. of Children: number of children\n"
+ "Salary: annual salary in F";
// Create an IImpot object
IImpot tax = new HardwiredImpot();
// infinite loop
while (true) {
// request tax calculation parameters
Console.Write("Tax calculation parameters in the following format: Married (y/n) No. of children Salary or 'nothing' to exit:");
string parameters = Console.ReadLine().Trim();
// anything to do?
if (parameters == null || parameters == "") break;
// Check the number of arguments in the entered line
string[] args = parameters.Split(null);
int numParameters = args.Length;
if (numberOfParameters != 3) {
Console.WriteLine(syntax);
continue;
}//if
// Checking parameter validity
// married
string married = args[0].ToLower();
if (groom != "o" && groom != "n") {
Console.WriteLine(syntax + "\nInvalid 'married' argument: enter 'o' or 'n'");
continue;
}//if
// numberOfChildren
int numberOfChildren = 0;
bool dataOk = false;
try {
nbChildren = int.Parse(args[1]);
dataOk = nbChildren >= 0;
} catch {
}//if
// Is the data valid?
if (!dataOk) {
Console.WriteLine(syntax + "\nInvalid NbEnfants argument: enter a positive integer or zero");
continue;
}
// salary
int salary = 0;
dataOk = false;
try {
salary = int.Parse(args[2]);
dataOk = salary >= 0;
} catch {
}//try-catch
// Is the data correct?
if (!dataOk) {
Console.WriteLine(syntax + "\nIncorrect salary argument: enter a positive integer or zero");
continue;
}
// parameters are correct - calculate tax
Console.WriteLine("Tax=" + tax.calculate(married == "o", numChildren, salary) + " euros");
// next taxpayer
}//while
}
}
}
The program above allows the user to perform repeated tax calculation simulations.
- line 16: creation of an `impot` object implementing the `IImpot` interface. This object is obtained by instantiating a `HardwiredImpot` type, a type that implements the `IImpot` interface. Note that the variable `impot` was not assigned the `HardwiredImpot` type, but rather the `IImpot` type. By writing this, we indicate that we are only interested in the `calculer` method of the `impot` object and not the rest.
- lines 19–68: the loop for tax calculation simulations
- line 22: the three parameters required by the `calculer` method are entered in a single line typed on the keyboard.
- line 26: the method [string].Split(null) breaks [string] down into words. These are stored in an array called args.
- Line 66: Call to the `calculate` method of the `impot` object, which implements the `IImpot` interface.
Here is an example of the program in action:









