Skip to content

4. Класи, структури, інтерфейси

4.1. Об’єкт на прикладі

4.1.1. Загальні відомості

Тепер на прикладі розглянемо об'єктне програмування. Об'єкт — це суть, що містить дані, які визначають його стан (їх називають полями, атрибутами тощо), та функції (їх називають методами). Об'єкт створюється за зразком, який називається класом:

public class C1{
    Type1 p1;        // поле p1
    Type2 p2;        // поле p2
    
    Type3 m3(){        // метод m3
        
    }
    Type4 m4(){        // метод m4
        
    }
    
}

На основі попереднього класу C1 можна створити безліч об’єктів O1, O2,… Усі вони матимуть поля p1, p2,… та методи m3, m4,… Але вони матимуть різні значення для своїх полів pi, завдяки чому кожен з них матиме власний стан. Якщо o1 є об’єктом типу C1, то o1.p1 позначає властивість p1 об’єкта o1, а o1.m1 — метод m1 об’єкта O1.

Розглянемо першу модель об’єкта: клас Personne.

4.1.2. Створення проекту на C#

У попередніх прикладах у проекті був лише один вихідний файл: Program.cs. Відтепер у одному проекті може бути кілька вихідних файлів. Покажемо, як це зробити.

У [1] створіть новий проєкт. У [2] виберіть «Консольний додаток». У [3] залиште значення за замовчуванням. У [4] підтвердіть вибір. У [5] — згенерований проект. Вміст Program.cs такий:


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

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

Збережемо створений проект:

У вікні [1] — опція збереження. У вікні [2] вкажіть папку, куди потрібно зберегти проект. У вікні [3] вкажіть назву проекту. У вікні [5] вкажіть, що ви хочете створити рішення. Рішення — це сукупність проектів. У вікні [4] вкажіть назву рішення. У вікні [6] підтвердіть збереження.

У [1] відкрийте збережений проект. У [2] додайте новий елемент до проекту.

У [1] вкажіть, що ви хочете додати клас. У [2] — назву класу. У [3] підтвердіть інформацію. У [4] проект [01] має новий вихідний файл Personne.cs:


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

namespace ConsoleApplication1 {
    class Personne {
    }
}

Змінюємо простір імен кожного з вихідних файлів у Chap2 та видаляємо імпорт непотрібних просторів імен:


using System;

namespace Chap2 {
    class Personne {
    }
}

using System;

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

4.1.3. Визначення класу Personne

Визначення класу Personne у вихідному файлі [Personne.cs] буде таким:


using System;

namespace Chap2 {
    public class Personne {
        // атрибути
        private string prenom;
        private string nom;
        private int age;

        // метод
        public void Initialise(string P, string N, int age) {
            this.prenom = P;
            this.nom = N;
            this.age = age;
        }

        // метод
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }

}

Тут ми маємо визначення класу, тобто типу даних. Коли ми будемо створювати змінні цього типу, їх називатимуть об’єктами або екземплярами класу. Отже, клас — це форма, на основі якої будуються об’єкти.

Елементами або полями класу можуть бути дані (атрибути), методи (функції) та властивості. Властивості — це особливі методи, що слугують для отримання або встановлення значень атрибутів об’єкта. Ці поля можуть супроводжуватися одним із трьох таких ключових слів:

privé
Приватне поле (private) доступне лише для внутрішніх методів класу
public
До публічного поля (public) мають доступ усі методи, незалежно від того, чи визначені вони в класі, чи ні
protégé
До захищеного поля (protected) мають доступ лише внутрішні методи класу або похідного об’єкта (див. далі поняття успадкування).

Зазвичай дані класу оголошуються приватними, тоді як його методи та властивості — публічними. Це означає, що користувач об’єкта (програміст)

  • не матиме прямого доступу до приватних даних об’єкта
  • може викликати публічні методи об’єкта, зокрема ті, що надають доступ до його приватних даних.

Синтаксис оголошення класу в C такий:


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

Порядок оголошення атрибутів private, protected та public може бути довільним.

4.1.4. Метод Initialise

Повернемося до нашого класу Personne, оголошеного так:


using System;

namespace Chap2 {
    public class Personne {
        // атрибути
        private string prenom;
        private string nom;
        private int age;

        // метод
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }

        // метод
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }

}

Яка роль методу Initialise? Оскільки nom, prenom та age є приватними даними класу Personne, інструкції:

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

є недопустимими. Нам потрібно ініціалізувати об’єкт типу Personne за допомогою публічного методу. Цю роль виконує метод Initialise. Ми напишемо:

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

Запис p1.Initialise є допустимим, оскільки Initialise має публічний доступ.

4.1.5. Оператор new

Послідовність команд

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

є неправильною. Команда

    Personne p1;

оголошує p1 як посилання на об’єкт типу Personne. Цей об’єкт ще не існує, а отже p1 не ініціалізовано. Це так, ніби ми написали:

Personne p1=null;

де за допомогою ключового слова null явно вказується, що змінна p1 ще не посилається на жоден об’єкт. Коли потім записуємо

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

, ми викликаємо метод Initialise об’єкта, на який посилається p1. Однак цей об’єкт ще не існує, і компілятор повідомить про помилку. Щоб p1 посилався на об’єкт, потрібно написати:

Personne p1=new Personne();

Це призведе до створення ще не ініціалізованого об’єкта типу Personne: атрибути nom та prenom, які є посиланнями на об’єкти типу String, матимуть значення null, а age — значення 0. Отже, відбувається ініціалізація за замовчуванням. Тепер, коли p1 посилається на об’єкт, інструкція ініціалізації цього об’єкта

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

є дійсною.

4.1.6. Ключове слово this

Розглянемо код методу initialise:


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

Інструкція this.prenom=p означає, що атрибут prenom поточного об’єкта (this) отримує значення p. Ключове слово this позначає поточний об’єкт: той, у якому знаходиться метод, що виконується. Звідки ми це знаємо? Давайте подивимося, як відбувається ініціалізація об’єкта, на який посилається p1 у програмі, що викликає:

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

Викликається метод Initialise об’єкта p1. Коли в цьому методі згадується об’єкт this, насправді мається на увазі об’єкт p1. Метод Initialise також можна було б написати так:


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

Коли метод об’єкта посилається на атрибут A цього об’єкта, запис this.A є неявним. Його слід використовувати явно у разі конфлікту ідентифікаторів. Це стосується такої інструкції:


this.age=age;

де age позначає атрибут поточного об’єкта, а також параметр age, отриманий методом. У такому разі необхідно усунути неоднозначність, позначивши атрибут age як this.age.

4.1.7. Тестова програма

Ось коротка тестова програма. Вона записана у вихідному файлі [Program.cs]:


using System;

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

Перед запуском проєкту [01] може знадобитися вказати вихідний файл для виконання:

У властивостях проєкту [01] як клас для виконання вказано [1].

Результати виконання такі:

[Jean, Dupont, 30]

4.1.8. Ще один метод Initialise

Розглянемо знову клас Personne і додамо до нього такий метод:


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

Тепер ми маємо два методи з назвою Initialise: це допустимо, якщо вони приймають різні параметри. У даному випадку це саме так. Параметром тепер є посилання p на особу. Атрибути особи p при цьому присвоюються поточному об’єкту (this). Слід зауважити, що метод Initialise має прямий доступ до атрибутів об’єкта p, хоча вони мають тип private. Це завжди так: об’єкт o1 класу C завжди має доступ до атрибутів об’єктів того самого класу C.

Ось приклад тестування нового класу Personne:


using System;

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

та його результати:

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

4.1.9. Конструктори класу Personne

Конструктор — це метод, який має назву класу і викликається під час створення об’єкта. Зазвичай його використовують для ініціалізації об’єкта. Цей метод може приймати аргументи, але не повертає жодного результату. Перед його прототипом або визначенням не вказується жоден тип (навіть void).

Якщо клас C має конструктор, що приймає n аргументів argi, оголошення та ініціалізація об’єкта цього класу можуть здійснюватися у такій формі:

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

або

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

Якщо клас C має один або кілька конструкторів, для створення об’єкта цього класу обов’язково має бути використаний один із цих конструкторів. Якщо клас C не має жодного конструктора, він має конструктор за замовчуванням — конструктор без параметрів: public C(). Атрибути об’єкта тоді ініціалізуються значеннями за замовчуванням. Саме це відбувалося в попередніх програмах, де було написано:

    Personne p1;
    p1=new Personne();

Створимо два конструктори для нашого класу Personne:


using System;

namespace Chap2 {
    public class Personne {
        // атрибути
        private string prenom;
        private string nom;
        private int age;

        // конструктори
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
        }
        public Personne(Personne P) {
            Initialise(P);
        }

        // метод
        public void Initialise(string p, string n, int age) {
...
        }

        public void Initialise(Personne p) {
...
        }

        // метод
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }

}

Наші два конструктори просто викликають методи Initialise, які ми розглядали раніше. Нагадаємо, що коли в конструкторі зустрічається, наприклад, запис Initialise(p), компілятор перетворює його на this.Initialise(p). Отже, у конструкторі викликається метод Initialise для роботи з об’єктом, на який посилається this, тобто з поточним об’єктом, який саме створюється.

Ось коротка тестова програма:


using System;

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

та отримані результати:

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

4.1.10. Посилання на об’єкти

Ми як і раніше використовуємо той самий клас Personne. Тестова програма виглядає наступним чином:


using System;

namespace Chap2 {
    class Program2 {
        static void Main() {
            // p1
            Personne p1 = new Personne("Jean", "Dupont", 30);
            Console.Write("p1="); p1.Identifie();
            // p2 посилається на той самий об’єкт, що й p1
            Personne p2 = p1;
            Console.Write("p2="); p2.Identifie();
            // p3 посилається на об’єкт, який буде копією об’єкта, на який посилається p1
            Personne p3 = new Personne(p1);
            Console.Write("p3="); p3.Identifie();
            // змінюємо стан об’єкта, на який посилається p1
            p1.Initialise("Micheline", "Benoît", 67);
            Console.Write("p1="); p1.Identifie();
            // оскільки p2=p1, стан об’єкта, на який посилається p2, мав змінитися
            Console.Write("p2="); p2.Identifie();
            // оскільки p3 не посилається на той самий об’єкт, що й p1, об’єкт, на який посилається p3, не мав змінитися
            Console.Write("p3="); p3.Identifie();
        }
    }
}

Отримано такі результати:

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

Коли оголошуємо змінну p1 за допомогою

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

p1 посилається на об’єкт Personne("Jean","Dupont",30), але не є самим об’єктом. У мові C це можна було б назвати покажчиком — c.a.d — адресою створеного об’єкта. Якщо потім написати:

    p1=null;

Змінюється не сам об’єкт Personne("Jean","Dupont",30), а значення посилання p1. Об’єкт Personne("Jean","Dupont",30) буде «втрачено», якщо на нього не посилається жодна інша змінна.

Коли ми пишемо:

Personne p2=p1;

ініціалізується покажчик p2: він «вказує» на той самий об’єкт (позначає той самий об’єкт), що й покажчик p1. Отже, якщо змінити об’єкт, на який «вказує» (або на який посилається) p1, то зміниться й об’єкт, на який посилається p2.

Коли ми пишемо:

Personne p3=new Personne(p1);

створюється новий об’єкт Personne. На цей новий об’єкт буде посилатися p3. Якщо змінити об’єкт, на який «вказує» (або на який посилається) p1, це жодним чином не вплине на об’єкт, на який посилається p3. Саме це показують отримані результати.

4.1.11. Передача параметрів типу посилання на об’єкт

У попередньому розділі ми розглянули способи передачі параметрів функції, коли вони представляли простий тип C#, що відображається структурою .NET. Давайте подивимося, що відбувається, коли параметр є посиланням на об’єкт:


using System;
using System.Text;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // приклад 4
            StringBuilder sb0 = new StringBuilder("essai0"), sb1 = new StringBuilder("essai1"), sb2 = new StringBuilder("essai2"), sb3;
            Console.WriteLine("Dans fonction appelante avant appel : sb0={0}, sb1={1}, sb2={2}", sb0,sb1, sb2);
            ChangeStringBuilder(sb0, sb1, ref sb2, out sb3);
            Console.WriteLine("Dans fonction appelante après appel : sb0={0}, sb1={1}, sb2={2}, sb3={3}", sb0, sb1, sb2, sb3);

        }

        private static void ChangeStringBuilder(StringBuilder sbf0, StringBuilder sbf1, ref StringBuilder sbf2, out StringBuilder sbf3) {
            Console.WriteLine("Début fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}", sbf0,sbf1, sbf2);
            sbf0.Append("*****");
            sbf1 = new StringBuilder("essai1*****");
            sbf2 = new StringBuilder("essai2*****");
            sbf3 = new StringBuilder("essai3*****");
            Console.WriteLine("Fin fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3);
        }
    }
}
  • рядок 8: визначає 3 об’єкти типу StringBuilder. Об’єкт StringBuilder знаходиться поруч з об’єктом string.. При маніпулюванні з об’єктом string у відповідь отримуємо новий об’єкт string. Отже, у цій послідовності коду:
string s="une chaîne";
s=s.ToUpperCase();

У рядку 1 у пам’яті створюється об’єкт string, а s — це його адреса. У рядку 2 функція s.ToUpperCase() створює в пам’яті інший об’єкт string. Отже, між рядками 1 і 2 значення s змінилося (воно вказує на новий об’єкт). Клас StringBuilder, у свою чергу, дозволяє перетворювати рядок без створення другого об’єкта. Це приклад, наведений вище:

  • рядок 8: 4 посилання [sb0, sb1, sb2, sb3] на об’єкти типу StringBuilder
  • рядок 10: передані до методу ChangeStringBuilder із різними режимами: sb0, sb1 із режимом за замовчуванням, sb2 із ключовим словом ref, sb3 із ключовим словом out.
  • рядки 15–22: метод, що має формальні параметри [sbf0, sbf1, sbf2, sbf3]. Взаємозв’язки між формальними параметрами sbfi та фактичними параметрами sbi такі:
  • sbf0 та sb0 — це, на момент запуску методу, дві окремі посилання, які вказують на один і той самий об’єкт (передача за адресою)
  • те саме стосується sbf1 та sb1
  • sbf2 та sb2 на момент запуску методу є одним і тим самим посиланням на один і той самий об’єкт (ключове слово ref)
  • sbf3 та sb3 після виконання методу є одним і тим самим посиланням на один і той самий об’єкт (ключове слово out)

Отримано такі результати:

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

Пояснення:

  • sb0 та sbf0 — це два окремі посилання на один і той самий об’єкт. Цей об’єкт було змінено за допомогою sbf0 — рядок 3. Цю зміну можна переглянути за допомогою sb0 — рядок 4.
  • sb1 та sbf1 — це два окремі посилання на один і той самий об’єкт. Значення sbf1 було змінено в методі, і тепер воно вказує на новий об’єкт — рядок 3. Це жодним чином не змінює значення sb1, яке продовжує вказувати на той самий об’єкт — рядок 4.
  • sb2 та sbf2 — це одне й те саме посилання на один і той самий об’єкт. Значення sbf2 змінюється в методі, і тепер воно вказує на новий об’єкт — рядок 3. Оскільки sbf2 та sb2 є одним і тим самим об’єктом, значення sb2 також було змінено, і sb2 вказує на той самий об’єкт, що й sbf2 — рядки 3 і 4.
  • До виклику методу sb3 не мав значення. Після виклику методу sb3 отримує значення sbf3. Отже, маємо два посилання на один і той самий об’єкт — рядки 3 і 4

4.1.12. Тимчасові об’єкти

У виразі можна явно викликати конструктор об’єкта: об’єкт створюється, але ми не маємо до нього доступу (наприклад, щоб змінити його). Цей тимчасовий об’єкт створюється для обчислення виразу, а потім знищується. Пам'ять, яку він займав, згодом автоматично звільняється програмою, яка називається «збирач сміття» (garbage collector), завданням якої є звільнення пам'яті, зайнятої об'єктами, на які більше не є посилань у даних програми.

Розглянемо таку нову тестову програму:


using System;

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

і змінимо конструктори класу Personne так, щоб вони виводили повідомлення:


        // конструктори
        public Personne(String p, String n, int age) {
            Console.WriteLine("Constructeur Personne(string, string, int)");
            Initialise(p, n, age);
        }
        public Personne(Personne P) {
            Console.Out.WriteLine("Constructeur Personne(Personne)");
            Initialise(P);
}

Ми отримуємо такі результати:

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

що демонструють послідовне створення двох тимчасових об’єктів.

4.1.13. Методи читання та запису приватних атрибутів

До класу Personne додаємо методи, необхідні для читання або зміни стану атрибутів об’єктів:


using System;

namespace Chap2 {
    public class Personne {
        // атрибути
        private string prenom;
        private string nom;
        private int age;

        // конструктори
        public Personne(String p, String n, int age) {
            Console.WriteLine("Constructeur Personne(string, string, int)");
            Initialise(p, n, age);
        }
        public Personne(Personne p) {
            Console.Out.WriteLine("Constructeur Personne(Personne)");
            Initialise(p);
        }

        // метод
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }

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

        // методи доступу
        public String GetPrenom() {
            return prenom;
        }
        public String GetNom() {
            return nom;
        }
        public int GetAge() {
            return age;
        }

        //модифікатори
        public void SetPrenom(String P) {
            this.prenom = P;
        }
        public void SetNom(String N) {
            this.nom = N;
        }
        public void SetAge(int age) {
            this.age = age;
        }

        // метод
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }

}

Ми тестуємо новий клас за допомогою такої програми:


using System;

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

і отримуємо такі результати:

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

4.1.14. Властивості

Існує ще один спосіб отримати доступ до атрибутів класу — це створення властивостей. Вони дозволяють нам оперувати приватними атрибутами так, ніби вони є публічними.

Розглянемо наступний клас Personne, у якому попередні методи доступу та модифікації були замінені властивостями для читання та запису:


using System;

namespace Chap2 {
    public class Personne {
        // атрибути
        private string prenom;
        private string nom;
        private int age;

        // конструктори
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
        }
        public Personne(Personne p) {
            Initialise(p);
        }

        // метод
        public void Initialise(string p, string n, int age) {
            this.prenom = p;
            this.nom = n;
            this.age = age;
        }

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

         // властивості
        public string Prenom {
            get { return prenom; }
            set {
                // ім'я є дійсним?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
            }//if
        }//ім'я

        public string Nom {
            get { return nom; }
            set {
                // Прізвище правильне?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("nom (" + value + ") invalide");
                } else { nom = value; }
            }//якщо
        }//прізвище


        public int Age {
            get { return age; }
            set {
                // вік дійсний?
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("âge (" + value + ") invalide");
            }//якщо
        }//вік

        // метод
        public void Identifie() {
            Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
        }
    }

}

Властивість дозволяє зчитувати (get) або встановлювати (set) значення атрибута. Властивість оголошується наступним чином:

public Type Propriété{
    get {...}
    set {...}
}

де Type має бути типом атрибута, який обробляє ця властивість. Вона може мати два методи: get та set. Метод get зазвичай відповідає за повернення значення атрибута, який він обробляє (хоча він може повертати й щось інше — ніщо цьому не заважає). Метод set отримує параметр value, який зазвичай присвоює атрибуту, яким він керує. Він може скористатися цією нагодою, щоб перевірити правильність отриманого значення та, за необхідності, викинути виняток, якщо значення виявиться недійсним. Саме це і робиться тут.

Як викликаються ці методи get та set? Розглянемо таку тестову програму:


using System;

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

В операторі


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

ми намагаємося отримати значення властивостей Prenom, Nom та Age особи p. У цьому випадку викликається метод get цих властивостей, який повертає значення атрибута, яким вони керують.

У операторі

        p.Age=56;

ми хочемо встановити значення властивості Age. У цьому випадку викликається метод set цієї властивості. Він отримає значення 56 у параметрі value.

Властивість P класу C, яка визначає лише метод get, називається властивістю «тільки для читання». Якщо c є об’єктом класу C, операція c.P=значення буде відхилена компілятором.

Виконання попередньої тестової програми дає такі результати:

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

Отже, властивості дозволяють нам оперувати приватними атрибутами так, ніби вони є публічними. Ще одна особливість властивостей полягає в тому, що їх можна використовувати разом із конструктором за таким синтаксисом:

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

Цей синтаксис еквівалентний такому коду:

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

Порядок властивостей не має значення. Ось приклад.

До класу Personne додано новий конструктор без параметрів:


        public Personne() {
}

Конструктор не ініціалізує члени об’єкта. Це так званий конструктор за замовчуванням. Саме він використовується, коли клас не визначає жодного конструктора.

Наступний код створює та ініціалізує (рядок 6) новий об’єкт Personne із використанням синтаксису, наведеного вище:


using System;

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

У рядку 6 вище використовується конструктор без параметрів Personne(). У цьому конкретному випадку можна було б також написати


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

, але дужки конструктора Personne() без параметрів у цьому синтаксисі не є обов’язковими.

Результати виконання такі:

p2=(Arthur,Martin,7)

У багатьох випадках методи get та set властивості обмежуються читанням та записом приватного поля без додаткової обробки. У такому випадку можна використовувати автоматичну властивість, оголошену наступним чином:

public Type Propriété{ get ; set ; }

Приватне поле, пов’язане з цією властивістю, не оголошується. Воно автоматично генерується компілятором. Доступ до нього здійснюється виключно через саму властивість. Отже, замість того, щоб писати:


    private string prenom;
...
     // пов'язана властивість
        public string Prenom {
            get { return prenom; }
            set {
                // ім'я є дійсним?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
            }//if
        }//ім'я

можна написати:

public string Prenom {get; set;}

без оголошення приватного поля prenom. Різниця між цими двома властивостями полягає в тому, що перша перевіряє правильність імені в полі set, тоді як друга не виконує жодної перевірки.

Використання автоматичного властивості Prenom еквівалентно оголошенню поля Prenom як публічного:

public string Prenom;

Можна задатися питанням, чи є різниця між цими двома оголошеннями. Оголошувати public полем класу не рекомендується. Це суперечить концепції інкапсуляції стану об’єкта, який має бути приватним і надаватися через публічні методи.

Якщо автоматичне властивість оголошено як virtuelle,, його можна перевизначити у дочірньому класі:


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

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

У рядку 2 вище дочірній клас Class2 може розмістити в set, код, що перевіряє правильність значення, присвоєного автоматичній властивості base.Prop батьківського класу Class1.

4.1.15. Методи та атрибути класу

Припустимо, що ми хочемо підрахувати кількість об’єктів Personne, створених у додатку. Можна самостійно вести лічильник, але є ризик пропустити тимчасові об’єкти, які створюються то тут, то там. Набагато надійніше було б включити в конструктори класу Personne інструкцію, що збільшує лічильник. Проблема полягає в тому, щоб передати посилання на цей лічильник, щоб конструктор міг його інкрементувати: для цього потрібно передати їм новий параметр. Також можна включити лічильник у визначення класу. Оскільки це атрибут самого класу, а не конкретного екземпляра цього класу, його оголошують інакше за допомогою ключового слова static:


        private static long nbPersonnes;

Щоб посилатися на нього, пишемо Personne.nbPersonnes, щоб показати, що це атрибут самого класу Personne. Тут ми створили приватний атрибут, до якого не буде прямого доступу поза межами класу. Тому ми створюємо публічну властивість, щоб надати доступ до атрибута класу nbPersonnes. Щоб встановити значення nbPersonnes, метод get цієї властивості не потребує конкретного об’єкта Personne: адже nbPersonnes є атрибутом усього класу. Тому нам потрібна властивість, яка також має назву static:


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

яке ззовні викликатиметься за синтаксисом Personne.NbPersonnes. Ось приклад.

Клас Personne набуває такого вигляду:


using System;

namespace Chap2 {
    public class Personne {

        // атрибути класу
        private static long nbPersonnes;
        public static long NbPersonnes {
            get { return nbPersonnes; }
        }

        // атрибути екземпляра
        private string prenom;
        private string nom;
        private int age;

        // конструктори
        public Personne(String p, String n, int age) {
            Initialise(p, n, age);
            nbPersonnes++;
        }
        public Personne(Personne p) {
            Initialise(p);
            nbPersonnes++;
        }

...
}

У рядках 20 і 24 конструктори збільшують значення статичного поля з рядка 7.

З такою програмою:


using System;

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

отримуємо такі результати:

    Nombre de personnes créées : 3

4.1.16. Масив осіб

Об’єкт — це дані, як і будь-які інші, і тому кілька об’єктів можна об’єднати в масив:


using System;

namespace Chap2 {
    class Program {
        static void Main(string[] args) {
            // масив осіб
            Personne[] amis = new Personne[3];
            amis[0] = new Personne("Jean", "Dupont", 30);
            amis[1] = new Personne("Sylvie", "Vartan", 52);
            amis[2] = new Personne("Neil", "Armstrong", 66);
            // відображення
            foreach (Personne ami in amis) {
                ami.Identifie();
            }
        }
    }
}
  • рядок 7: створює масив із 3 елементів типу Personne. Ці 3 елементи ініціалізуються тут значеннями null, c.a.d, оскільки вони не посилаються на жодний об’єкт. Знову ж таки, через неточність формулювання ми говоримо про масив об’єктів, хоча насправді це лише масив посилань на об’єкти. Створення масиву об’єктів, який сам є об’єктом (наявність new), не створює жодного об’єкта типу його елементів: це потрібно зробити згодом.
  • рядки 8–10: створення 3 об’єктів типу Personne
  • рядки 12–14: виведення вмісту масиву amis

Отримуємо такі результати:

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

4.2. Спадкування на прикладі

4.2.1. Загальні відомості

Тут ми розглянемо поняття успадкування. Мета успадкування полягає в тому, щоб «персоналізувати» існуючий клас так, щоб він відповідав нашим потребам. Припустимо, що ми хочемо створити клас Enseignant: вчитель — це особлива людина. Він має атрибути, яких не матиме інша людина: наприклад, предмет, який він викладає. Але він також має атрибути, властиві будь-якій людині: ім’я, прізвище та вік. Отже, вчитель повністю належить до класу Personne, але має додаткові атрибути. Замість того, щоб створювати клас Enseignant з нуля, краще скористатися набором властивостей класу Personne, який ми адаптуємо до особливого характеру вчителів. Саме концепція успадкування дозволяє нам це зробити.

Щоб показати, що клас Enseignant успадковує властивості класу Personne, ми напишемо:

    public class Enseignant : Personne

Personne називається батьківським (або материнським) класом, а Enseignant — похідним (або дочірнім) класом. Об’єкт Enseignant має всі властивості об’єкта Personne: він має ті самі атрибути та ті самі методи. Ці атрибути та методи батьківського класу не повторюються у визначенні дочірнього класу: достатньо вказати лише атрибути та методи, додані дочірнім класом:

Припустимо, що клас Personne визначено так:


using System;

namespace Chap2 {
    public class Personne {

        // атрибути класу
        private static long nbPersonnes;
        public static long NbPersonnes {
            get { return nbPersonnes; }
        }

        // атрибути екземпляра
        private string prenom;
        private string nom;
        private int age;

        // конструктори
        public Personne(String prenom, String nom, int age) {
            Nom = nom;
            Prenom = prenom;
            Age = age;
            nbPersonnes++;
            Console.WriteLine("Constructeur Personne(string, string, int)");
        }
        public Personne(Personne p) {
            Nom = p.Nom;
            Prenom = p.Prenom;
            Age = p.Age;
            nbPersonnes++;
            Console.WriteLine("Constructeur Personne(Personne)");
        }

        // властивості
        public string Prenom {
            get { return prenom; }
            set {
                // ім'я є дійсним?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("prénom (" + value + ") invalide");
                } else {
                    prenom = value;
                }
            }//if
        }//ім'я

        public string Nom {
            get { return nom; }
            set {
                // Прізвище дійсне?
                if (value == null || value.Trim().Length == 0) {
                    throw new Exception("nom (" + value + ") invalide");
                } else { nom = value; }
            }//якщо
        }//прізвище

        public int Age {
            get { return age; }
            set {
                // вік правильний?
                if (value >= 0) {
                    age = value;
                } else
                    throw new Exception("âge (" + value + ") invalide");
            }//якщо
        }//вік

        // власність
        public string Identite {
            get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
        }
    }

}

Метод Identifie було замінено на властивість Identite, доступну лише для читання, яка ідентифікує особу. Створюємо клас Enseignant, що успадковує клас Personne:


using System;

namespace Chap2 {
    class Enseignant : Personne {
        // атрибути
        private int section;

        // конструктор
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
            // зберігаємо розділ за допомогою властивості Section
            Section = section;
            // відстеження
            Console.WriteLine("Construction Enseignant(string, string, int, int)");
        }//конструктор

        // властивість Section
        public int Section {
            get { return section; }
            set { section = value; }
        }// Section

    }
}

Клас Enseignant доповнює методи та атрибути класу Personne:

  • рядок 4: клас Enseignant походить від класу Personne
  • рядок 6: атрибут section, який є номером секції, до якої належить викладач у колективі викладачів (загалом одна секція на дисципліну). Цей приватний атрибут доступний через публічну властивість Section у рядках 18–21
  • рядок 9: новий конструктор, що дозволяє ініціалізувати всі атрибути викладача

4.2.2. Створення об’єкта «Викладач»

Дочірній клас не успадковує конструктори свого батьківського класу. Тому він повинен визначити власні конструктори. Конструктор класу Enseignant має такий вигляд:


        // конструктор
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
            // зберігаємо розділ
            Section = section;
            // відстеження
            Console.WriteLine("Construction enseignant(string, string, int, int)");
}//виробник

Оголошення


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

вказує, що конструктор приймає чотири параметри: prenom, nom, age, section і передає три — (prenom,nom,age) — своєму базовому класу, в даному випадку класу Personne. Відомо, що цей клас має конструктор Personne(string, string, int), який дозволить створити об’єкт «особа» з переданими параметрами (prenom,nom,age). Після завершення створення базового класу створення об’єкта Enseignant продовжується виконанням тіла конструктора:

            // зберігаємо секцію
            Section = section;

Слід зауважити, що ліворуч від знака = використовується не атрибут section об’єкта, а пов’язана з ним властивість Section. Це дозволяє конструктору скористатися можливими перевірками правильності, які може виконувати цей метод. Це дозволяє уникнути розміщення таких перевірок у двох різних місцях: у конструкторі та у властивості.

Підсумовуючи, конструктор похідного класу:

  • передає базовому класу параметри, необхідні для його створення
  • використовує інші параметри для ініціалізації власних атрибутів

Можна було б написати так:


// конструктор
  public Enseignant(string prenom, string nom, int age, int section){
    this.prenom=prenom;
        this.nom=nom;
        this.age=age;
      this.section=section;
  }

Це неможливо. У класі Personne три поля prenom, nom та age оголошені приватними (private). Прямий доступ до цих полів мають лише об’єкти того самого класу. Усі інші об’єкти, включаючи дочірні об’єкти, як у цьому випадку, повинні використовувати публічні методи, щоб отримати до них доступ. Ситуація була б іншою, якби клас Personne оголосив ці три поля захищеними (protected): тоді він дозволив би похідним класам мати прямий доступ до цих трьох полів. У нашому прикладі використання конструктора батьківського класу було, отже, правильним рішенням, і це є стандартним підходом: під час створення дочірнього об’єкта спочатку викликається конструктор батьківського об’єкта, а потім виконуються власні ініціалізації, що стосуються саме дочірнього об’єкта (section у нашому прикладі).

Спробуємо написати першу тестову програму [Program.cs]:


using System;

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

Ця програма просто створює об’єкт Enseignant (new) та ідентифікує його. Клас Enseignant не має методу Identite, але його батьківський клас має такий метод, який, до того ж, є публічним: завдяки успадкуванню він стає публічним методом класу Enseignant.

Проект у цілому виглядає так:

Отримані результати такі:

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

Видно, що:

  • об’єкт Personne (рядок 1) було створено раніше, ніж об’єкт Enseignant (рядок 2)
  • отриманий ідентифікатор належить об’єкту Personne

4.2.3. Перевизначення методу або властивості

У попередньому прикладі ми отримали ідентифікатор частини Personne, що відповідає викладачеві, але бракує певної інформації, характерної для класу Enseignant (секція). Тому доводиться написати властивість, що дозволяє ідентифікувати викладача:


using System;

namespace Chap2 {
    class Enseignant : Personne {
        // атрибути
        private int section;

        // конструктор
        public Enseignant(string prenom, string nom, int age, int section)
            : base(prenom, nom, age) {
            // зберігаємо розділ за допомогою властивості Section
            Section = section;
            // відстеження
            Console.WriteLine("Construction Enseignant(string, string, int, int)");
        }//конструктор

        // властивість Section
        public int Section {
            get { return section; }
            set { section = value; }
        }// секція

        // властивість Identite
        public new string Identite {
            get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
        }
    }
}

У рядках 24–26 властивість Identite класу Enseignant спирається на властивість Identite свого батьківського класу (base.Identite) (рядок 25) для відображення своєї частини «Personne», а потім доповнюється полем section, яке є власним для класу Enseignant. Звернімо увагу на оголошення властивості Identite:


    public new string Identite{

Нехай об’єкт enseignant — це E. Цей об’єкт містить у собі об’єкт Personne:

Властивість Identite визначена як у класі Enseignant, так і в його батьківському класі Personne. У дочірньому класі Enseignant перед властивістю Identite має стояти ключове слово new, щоб вказати, що ми перевизначаємо нову властивість Identite для класу Enseignant.


    public new string Identite{

Клас Enseignant тепер має два властивості Identite:

  • одну, успадковану від батьківського класу Personne
  • власне

Якщо E є об’єктом Enseignant, то E.Identite позначає властивість Identite класу Enseignant. Кажуть, що властивість Identite дочірнього класу перевизначає або приховує властивість Identite батьківського класу. Загалом, якщо O є об’єктом, а M — методом, для виконання методу O.M система шукає метод M у такому порядку:

  • у класі об’єкта O
  • у його батьківському класі, якщо такий є
  • у батьківському класі його батьківського класу, якщо такий існує
  • і т. д.

Отже, успадкування дозволяє у дочірньому класі перевизначити методи/властивості з тими самими іменами, що й у батьківському класі. Саме це дає змогу адаптувати дочірній клас до власних потреб. У поєднанні з поліморфізмом, про який ми поговоримо трохи пізніше, перевизначення методів/властивостей є головною перевагою успадкування.

Розглянемо ту саму тестову програму, що й раніше:


using System;

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

Цього разу отримано такі результати:

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

4.2.4. Поліморфізм

Розглянемо лінію класів: C0 C1 C2 … ←Cn

де Ci Cj вказує, що клас Cj походить від класу Ci. Звідси випливає, що клас Cj має всі властивості класу Ci, а також додаткові. Нехай Oi — об’єкти типу Ci. Допустимо записати:

    Oi=Oj avec j>i

Адже завдяки успадкуванню клас Cj має всі характеристики класу Ci, а також додаткові. Отже, об’єкт Oj типу Cj містить у собі об’єкт типу Ci. Операція

    Oi=Oj

призводить до того, що Oi є посиланням на об’єкт типу Ci, що міститься в об’єкті Oj.

Те, що змінна Oi класу Ci може фактично посилатися не лише на об’єкт класу Ci, а й на будь-який об’єкт, похідний від класу Ci,, називається поліморфізмом: здатність змінної посилатися на об’єкти різних типів.

Розглянемо приклад і проаналізуємо наступну функцію, незалежну від будь-якого класу (static):

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

Можна також написати

    Personne p;
    ...
    Affiche(p);

або

    Enseignant e;
    ...
    Affiche(e);

В останньому випадку формальний параметр p типу Personne статичного методу Affiche отримає значення типу Enseignant. Оскільки тип Enseignant походить від типу Personne, це допустимо.

4.2.5. Перевизначення та поліморфізм

Доповнимо наш метод Affiche:


        public static void Affiche(Personne p) {
            // відображає ідентифікатор p
            Console.WriteLine(p.Identite);
}//відображає

Властивість p.Identite повертає рядок, що ідентифікує об’єкт Personne p. Що відбудеться у попередньому прикладі, якщо параметр, переданий методу Affiche, є об’єктом типу Enseignant:


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

Розглянемо наступний приклад:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // вчителя
            Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
            Affiche(e);
            // людина
            Personne p = new Personne("Jean", "Dupont", 30);
            Affiche(p);
        }

        // плакат
        public static void Affiche(Personne p) {
            // показує ідентифікацію p
            Console.WriteLine(p.Identite);
        }//відображає
    }
}

Отримані результати такі:

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

Виконання показує, що інструкція p.Identite (рядок 17) щоразу виконувала властивість Identite об’єкта Personne, спочатку (у рядку 7) для об’єкта, що міститься в Enseignant e, а потім (у рядку 10) для самої Personne p. Вона не адаптувалася до об’єкта, який фактично передавався як параметр до Affiche. Було б краще мати повну ідентифікацію e об’єкта Enseignant. Для цього позначення p.Identite мало б посилатися на властивість Identite об’єкта, на який фактично вказує p, а не на властивість Identite частини «Personne» об’єкта, на який насправді вказує p.

Цього результату можна досягти, оголосивши Identite віртуальною властивістю (virtual) у базовому класі Personne:


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

Ключове слово virtual робить Identite віртуальною властивістю. Це ключове слово також може застосовуватися до методів. Дочірні класи, які перевизначають віртуальну властивість або метод, повинні використовувати ключове слово override замість new для позначення своєї перевизначеної властивості/методу. Так, у класі Enseignant властивість Identite перевизначається наступним чином:


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

Тоді попередня програма видає такі результати:

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

Цього разу, у рядку 3, ми отримали повну ідентифікацію викладача. Тепер переозначимо метод, а не властивість. Клас object (псевдонім System.Object у C#) є «батьківським» класом для всіх класів C#. Отже, коли ми пишемо:

    public class Personne

ми неявним чином пишемо:

    public class Personne : System.Object

Клас System.Object визначає віртуальний метод ToString:

Метод ToString повертає ім’я класу, до якого належить об’єкт, як показано в наступному прикладі:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // вчитель
            Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString());
            // особа
            Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString());
        }
    }
}

Отримано такі результати:

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

Слід зауважити, що, хоча ми й не перевизначили метод ToString у класах Personne та Enseignant, проте можна побачити, що метод ToString класу Object зміг відобразити справжню назву класу об’єкта.

Перевизначимо метод ToString у класах Personne та Enseignant:


        // метод ToString
        public override string ToString() {
            return Identite;
}

Визначення є однаковим у обох класах. Розглянемо таку тестову програму:


using System;
namespace Chap2 {
    class Program3 {
        public static void Main() {
            // вчитель
            Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
            Affiche(e);
            // особа
            Personne p = new Personne("Jean", "Dupont", 30);
            Affiche(p);
        }
        // плакат
        public static void Affiche(Personne p) {
            // афіша, що ідентифікує п
            Console.WriteLine(p);
        }//Афіша
    }
}

Зупинимося на методі Affiche, який приймає як параметр об’єкт типу p. У рядку 15 метод WriteLine класу Console не має жодного варіанту, який приймав би параметр типу Personne. Серед різних варіантів Writeline є один, який приймає як параметр тип Object. Компілятор використає цей метод, WriteLine(Object o), оскільки ця сигнатура означає, що параметр o може бути типу Object або похідним від нього. Оскільки Object є базовим класом для всіх класів, будь-який об’єкт може бути переданий як параметр до WriteLine, а отже, і об’єкт типу Personne або Enseignant. Метод WriteLine(Object o) записує o.ToString() у потік запису Out. Оскільки метод ToString є віртуальним, якщо об’єкт o (типу Object або похідного типу) перевизначив метод ToString, то саме останній буде використано. Саме це відбувається у випадку з класами Personne та Enseignant.

Про це свідчать результати виконання:

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

4.3. Перевизначення значення оператора для класу

4.3.1. Вступ

Розглянемо інструкцію

op1 + op2

де op1 та op2 — два операнди. Можна перевизначити значення оператора +. Якщо операнд op1 є об’єктом класу C1, потрібно визначити статичний метод у класі C1 із таким сигнатурою:

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

Коли компілятор натрапляє на інструкцію

op1 + op2

, він перетворює її на C1.operator+(op1,op2). Тип, який повертає метод operator, є важливим. Дійсно, розглянемо операцію op1+op2+op3. Компілятор перетворює її на (op1+op2)+op3. Нехай res12 — результат операції op1+op2. Наступною операцією є res12+op3. Якщо res12 має тип C1, вона також буде перетворена на C1.operator+(res12,op3). Це дозволяє послідовно виконувати операції.

Також можна перевизначити унарні оператори, що мають лише один операнд. Так, якщо op1 є об’єктом типу C1, операцію op1++ можна перевизначити за допомогою статичного методу класу C1:

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

Сказане вище справедливе для більшості операторів, однак є кілька винятків:

  • оператори == та != необхідно перевизначати одночасно
  • оператори &&, ||, [], (), +=, -=, ... не можна перевизначати

4.3.2. Приклад

Створюємо клас ListeDePersonnes, похідний від класу ArrayList. Цей клас реалізує динамічний список і розглядається в наступному розділі. З цього класу ми використовуємо лише такі елементи:

  • метод L.Add(Object o), що дозволяє додати до списку L об’єкт o. Тут об’єктом o буде об’єкт Personne.
  • властивість L.Count, яка визначає кількість елементів списку L
  • позначення L[i], яке вказує на i-й елемент списку L

Клас ListeDePersonnes успадкує всі атрибути, методи та властивості класу ArrayList. Його визначення таке:


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

namespace Chap2 {
    class ListeDePersonnes : ArrayList{
        // перевизначення оператора + для додавання особи до списку
        public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
            // додаємо особу p до ListeDePersonnes l
            l.Add(p);
            // ListeDePersonnes l
            return l;
        }// оператор +

        // ToString
        public override string ToString() {
            // повертає (елемент1, елемент2, ..., елементn)
            // відкриваюча дужка
            StringBuilder listeToString = new StringBuilder("(");
            // проходимо по списку осіб (this)
            for (int i = 0; i < Count - 1; i++) {
                listeToString.Append(this[i]).Append(",");
            }//for
            // останній елемент
            if (Count != 0) {
                listeToString.Append(this[Count-1]);
            }
            // закриваюча дужка
            listeToString.Append(")");
            // потрібно повернути рядок
            return listeToString.ToString();
        }//ToString
    }
}
  • рядок 6: клас ListeDePersonnes походить від класу ArrayList
  • рядки 8–13: визначення оператора + для операції l + p, де l має тип ListeDePersonnes, а p — тип Personne або похідний від нього.
  • рядок 10: особа p додається до списку l. Тут використовується метод Add батьківського класу ArrayList.
  • рядок 12: повертається посилання на список l, щоб можна було послідовно застосовувати оператори +, наприклад, у виразі l + p1 + p2. Операція l+p1+p2 буде інтерпретуватися (з урахуванням пріоритету операторів) як (l+p1)+p2. Операція l+p1 поверне посилання l. Операція (l+p1)+p2 перетворюється на l+p2, яка додає особу p2 до списку осіб l.
  • рядок 16: ми перевизначаємо метод ToString, щоб відобразити список осіб у вигляді (особа1, особа2, ..) де personnei сам є результатом методу ToString класу Personne.
  • рядок 19: ми використовуємо об’єкт типу StringBuilder. Цей клас краще підходить, ніж клас string, коли потрібно виконувати численні операції над символьним рядком, у даному випадку додавання. Дійсно, кожна операція над об’єктом string повертає новий об’єкт string, тоді як ті самі операції над об’єктом StringBuilder змінюють об’єкт, але не створюють нового. Ми використовуємо метод Append для об’єднання рядків.
  • рядок 21: ми перебираємо елементи списку осіб. Цей список тут позначається як this. Це поточний об’єкт, на якому виконується метод ToString. Властивість Count є властивістю батьківського класу ArrayList.
  • рядок 22: до елемента № i поточного списку this можна отримати доступ за допомогою запису this[i]. І в цьому випадку це властивість класу ArrayList. Оскільки мова йде про додавання рядків, буде використано метод this[i].ToString(). Оскільки цей метод є віртуальним, буде використано метод ToString об’єкта this, типу Personne або похідного від нього.
  • рядок 31: нам потрібно повернути об’єкт типу string (рядок 16). Клас StringBuilder має метод ToString, який дозволяє перейти від типу StringBuilder до типу string.

Слід зауважити, що клас ListeDePersonnes не має конструктора. У цьому випадку відомо, що конструктор

public ListeDePersonnes(){
}

буде використано. Цей конструктор нічого не робить, окрім виклику конструктора без параметрів свого батьківського класу:

public ArrayList(){
...
}

Тестовий клас може виглядати так:


using System;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // список осіб
            ListeDePersonnes l = new ListeDePersonnes();
            // додавання осіб
            l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
            // відображення
            Console.WriteLine("l=" + l);
            l = l + new Enseignant("camille", "germain",27,60);
            Console.WriteLine("l=" + l);
        }
    }
}
  • рядок 7: створення списку осіб
  • рядок 9: додавання 2 осіб за допомогою оператора +
  • рядок 12: додавання викладача
  • рядки 11 і 13: використання перевизначеного методу ListeDePersonnes.ToString().

Результати:

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

4.4. Визначення індексатора для класу

Тут ми продовжуємо використовувати клас ListeDePersonnes. Якщо l є об’єктом ListeDePersonnes, ми хочемо мати можливість використовувати нотацію l[i] для позначення особи № i зі списку l як під час читання (Особа p=l[i]), так і під час запису (l[i]=new Особа(...)).

Щоб мати змогу записати l[i], де l[i] позначає об’єкт Personne, нам потрібно визначити в класі ListeDePersonnes такий метод this:


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

Цей метод this[int i] називають індексатором, оскільки він надає значення виразу obj[i], що нагадує нотацію масивів, хоча obj є не масивом, а об’єктом. Метод get методу this об’єктаоб’єкта obj викликається при записі variable=obj[i], а метод set — при записі obj[i]=value.

Клас ListeDePersonnes походить від класу ArrayList, який сам має індексатор:

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

Існує конфлікт між методом this класу ListeDePersonnes:


 public Personne this[int i] 

та методом this класу ArrayList


 public object this[int i] 

оскільки вони мають однакову назву та приймають один і той самий тип параметра (int). Щоб вказати, що метод this класу ListeDePersonnes «приховує» однойменний метод класу ArrayList, необхідно додати ключове слово new до оголошення індексатора ListeDePersonnes. Отже, ми напишемо:


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

Доповнимо цей метод. Метод this.get викликається, коли, наприклад, записується variable=l[i], де l має тип ListeDePersonnes. У цьому випадку потрібно повернути особу № i зі списку l. Це здійснюється за допомогою нотації base[i], яка робить об’єкт № i класу ArrayList базовим для класу ListeDePersonnes. Оскільки повернений об’єкт має тип Object, необхідне перетворення типу до класу Personne.


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

Метод set викликається при записі l[i]=p, де p є Personne. У цьому випадку відбувається прив’язування особи p до елемента i списку l.


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

Тут особа p, представлена ключовим словом value, прив’язана до елемента № i базового класу ArrayList.

Отже, індекс класу ListeDePersonnes буде таким:


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

Тепер ми хочемо мати можливість записувати також «Особа p=l["nom"]», c.a.d, а також індексувати список l не за номером елемента, а за іменем особи. Для цього ми визначаємо новий індексер:


        // пошук за іменем
        public int this[string nom] {
            get {
                // пошук особи
                for (int i = 0; i < Count; i++) {
                    if (((Personne)base[i]).Nom == nom)
                        return i;
                }//for
                return -1;
            }//отримати
}

Перший рядок


public int this[string nom]

вказує, що клас ListeDePersonnes індексується за допомогою символьного рядка nom, а результатом l[nom] є ціле число. Це ціле число буде позицією у списку особи з іменем nom або -1, якщо цієї особи немає у списку. Ми визначаємо лише властивість get,, тим самим забороняючи запис l["nom"]=значення, який би вимагав визначення властивості set. Ключове слово new не потрібне в оголошенні індексатора, оскільки базовий клас ArrayList не визначає індексатора this[string].

У тілі функції get виконується перебір списку осіб у пошуках імені, переданого як параметр. Якщо його знайдено на позиції i, повертається i, інакше повертається -1.

Попередню тестову програму доповнюють таким чином:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // список осіб
            ListeDePersonnes l = new ListeDePersonnes();
            // додавання осіб
            l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
            // перегляд
            Console.WriteLine("l=" + l);
            l = l + new Enseignant("camille", "germain",27,60);
            Console.WriteLine("l=" + l);
            // зміна елемента 1
            l[1] = new Personne("franck", "gallon",5);
            // перегляд елемента 1
            Console.WriteLine("l[1]=" + l[1]);
            // перегляд списку l
            Console.WriteLine("l=" + l);
            // пошук осіб
            string[] noms = { "martin", "germain", "xx" };
            for (int i = 0; i < noms.Length; i++) {
                int inom = l[noms[i]];
                if (inom != -1)
                    Console.WriteLine("Personne(" + noms[i] + ")=" + l[inom]);
                else
                    Console.WriteLine("Personne(" + noms[i] + ") n'existe pas");
            }//для
        }
    }
}

Виконання цього коду дає такі результати:

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

4.5. Структури

Структура в C# аналогічна структурі мови C і дуже близька до поняття класу. Структура визначається наступним чином:

struct NomStructure{
// атрибути
    ...
// властивості
...
// конструктори
...
// методи
...
}

Незважаючи на схожість у оголошенні, між класом і структурою існують суттєві відмінності. Наприклад, поняття успадкування не існує для структур. Якщо ми пишемо клас, який не повинен бути похідним, то які відмінності між структурою та класом допоможуть нам зробити вибір між ними? Давайте розберемося в цьому на прикладі:


using System;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // структура sp1
            SPersonne sp1;
            sp1.Nom = "paul";
            sp1.Age = 10;
            Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
            // структура sp2
            SPersonne sp2 = sp1;
            Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
            // sp2 змінено
            sp2.Nom = "nicole";
            sp2.Age = 30;
            // перевірка sp1 та sp2
            Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
            Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");

            // об'єкт op1
            CPersonne op1=new CPersonne();
            op1.Nom = "paul";
            op1.Age = 10;
            Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
            // об'єкт op2
            CPersonne op2=op1;
            Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
            // op2 змінено
            op2.Nom = "nicole";
            op2.Age = 30;
            // перевірка op1 та op2
            Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
            Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
        }
    }
    // структура SPersonne
    struct SPersonne {
        public string Nom;
        public int Age;
    }

    // клас CPersonne
    class CPersonne {
        public string Nom;
        public int Age;
    }

}
  • рядки 38–41: структура з двома публічними полями: Nom, Age
  • рядки 44–47: клас із двома публічними полями: Nom, Age

Якщо запустити цю програму, отримаємо такі результати:

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

Там, де раніше використовувався клас Personne, тепер ми використовуємо структуру SPersonne:


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

Ця структура не має конструктора. Вона могла б мати його, як ми покажемо далі. За замовчуванням вона завжди має конструктор без параметрів, у даному випадку SPersonne().

  • 7-й рядок коду: оголошення

    SPersonne sp1;

еквівалентна оператору:


    SPersonne sp1=new Spersonne();

Створюється структура (Ім'я, Вік), а значенням sp1 є сама ця структура. У випадку класу створення об'єкта (Ім'я, Вік) має відбуватися явно за допомогою оператора new (рядок 22):


CPersonne op1=new CPersonne();

Попередня інструкція створює об’єкт CPersonne (грубо кажучи, еквівалент нашої структури), а значенням p1 є адреса (посилання) на цей об’єкт.

Підсумуємо

  • у випадку структури значення sp1 — це сама структура
  • у випадку класу значенням op1 є адреса створеного об’єкта

Коли в програмі в рядку 12 написано:


            SPersonne sp2 = sp1;

створюється нова структура sp2(Ім'я, Вік), яка ініціалізується значенням sp1,, тобто самою структурою.

Структура sp1 дублюється в sp2 [1]. Це копіювання значень. Тепер розглянемо інструкцію в рядку 27:


CPersonne op2=op1;

У випадку класів значення op1 копіюється в op2, але оскільки це значення насправді є адресою об’єкта, сам об’єкт не дублюється [2].

У випадку структури [1], якщо змінити значення sp2, значення sp1 не змінюється, що й демонструє програма. У випадку об’єкта [2], якщо змінити об’єкт, на який вказує op2, то об’єкт, на який вказує op1, також змінюється, оскільки це той самий об’єкт. Це також підтверджують результати роботи програми.

Отже, з цих пояснень слід винести наступне:

  • значенням змінної типу «структура» є сама структура
  • значенням змінної типу «об’єкт» є адреса об’єкта, на який вона вказує

Зрозумівши цю фундаментальну різницю, можна побачити, що структура дуже нагадує клас, як показує наступний новий приклад:


using System;

namespace Chap2 {

    // структура SPersonne
    struct SPersonne {
        // приватні атрибути
        private string nom;
        private int age;

        // властивості
        public string Nom {
            get { return nom; }
            set { nom = value; }
        }//ім'я

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

        // Виробник
        public SPersonne(string nom, int age) {
            this.nom = nom;
            this.age = age;
        }//конструктор

        // ToString
        public override string ToString() {
            return "SPersonne(" + Nom + "," + Age + ")";
        }//ToString
    }//структура
}//простір імен
  • рядки 8–9: два приватні поля
  • рядки 12–20: відповідні публічні властивості
  • рядки 23–26: визначається конструктор. Слід зауважити, що конструктор без параметрів SPersonne() завжди присутній і не потребує оголошення. Його оголошення відхиляється компілятором. У конструкторі в рядках 23–26 може виникнути спокуса ініціалізувати приватні поля nom, age за допомогою їхніх публічних властивостей Nom, Age. Компілятор це не допускає. Методи структури не можуть використовуватися під час її створення.
  • рядки 29–31: перевизначення методу ToString.

Тестова програма може виглядати так:


using System;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // особа p1
            SPersonne p1=new SPersonne();
            p1.Nom="paul";
            p1.Age= 10;
            Console.WriteLine("p1={0}",p1);
            // особа p2
            SPersonne p2 = p1;
            Console.WriteLine("p2=" + p2);
            // p2 змінено
            p2.Nom = "nicole";
            p2.Age = 30;
            // перевірка p1 та p2
            Console.WriteLine("p1=" + p1);
            Console.WriteLine("p2=" + p2);
            // особа p3
            SPersonne p3 = new SPersonne("amandin", 18);
            Console.WriteLine("p3=" + p3);
            // особа p4
            SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
            Console.WriteLine("p4=" + p4);
        }
    }
}
  • рядок 7: доводиться явно використовувати конструктор без параметрів, оскільки у структурі існує інший конструктор. Якби структура не мала жодного конструктора, оператор

            SPersonne p1;

було б достатньо для створення порожньої структури.

  • рядки 8–9: структура ініціалізується через її публічні властивості
  • рядок 10: метод p1.ToString буде використано в WriteLine.
  • рядок 21: створення структури за допомогою конструктора SPersonne(string, int)
  • рядок 24: створення структури за допомогою конструктора без параметрів SPersonne() з ініціалізацією приватних полів у фігурних дужках за допомогою їхніх публічних властивостей.

Отримуємо такі результати виконання:

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

Єдина помітна відмінність між структурою та класом полягає в тому, що у випадку класу об’єкти p1 та p2 вказували б на один і той самий об’єкт наприкінці програми.

4.6. Інтерфейси

Інтерфейс — це набір прототипів методів або властивостей, що утворюють контракт. Клас, який вирішує реалізувати інтерфейс, зобов’язується надати реалізацію всіх методів, визначених в інтерфейсі. Перевірку цієї реалізації здійснює компілятор.

Ось, наприклад, визначення інтерфейсу System.Collections.IEnumerator:

public interface System.Collections.IEnumerator 
{

    // Властивості
    Object Current { get; }

    // Методи
    bool MoveNext();
    void Reset();
}

Властивості та методи інтерфейсу визначаються лише їхніми сигнатурами. Вони не реалізовані (не мають коду). Саме класи, що реалізують інтерфейс, надають код методам та властивостям інтерфейсу.

1
2
3
4
5
6
public class C : IEnumerator{
    ...
    Object Current{ get {...}}
    bool MoveNext{...}
    void Reset(){...}
}
  • рядок 1: клас C реалізує інтерфейс IEnumerator. Зауважимо, що знак :, який використовується для реалізації інтерфейсу, є таким самим, як і той, що використовується для успадкування класу.
  • рядки 3–5: реалізація методів та властивостей інтерфейсу IEnumerator.

Розглянемо такий інтерфейс:


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

Інтерфейс IStats має:

  • властивість тільки для читання Moyenne: для обчислення середнього значення ряду значень
  • метод EcartType: для обчислення стандартного відхилення

Слід зауважити, що ніде не вказано, про який саме набір значень йдеться. Це може бути середній бал учнів класу, середній місячний обсяг продажів певного товару, середня температура в певному місці тощо. У цьому полягає принцип роботи інтерфейсів: передбачається наявність методів в об’єкті, але не конкретних даних.

Першим класом реалізації інтерфейсу IStats міг би бути клас, що слугує для зберігання оцінок учнів класу з певного предмета. Учень характеризувався б такою структурою Elève:


    public struct Elève {
        public string Nom { get; set; }
        public string Prénom { get; set; }
}//Учень

Учень ідентифікується за прізвищем та ім’ям. У рядках 2–3 містяться автоматичні властивості для цих двох атрибутів.

Оцінка характеризується такою структурою: Note:


    public struct Note {
        public Elève Elève { get; set; }
        public double Valeur { get; set; }
}//Оцінка

Оцінка ідентифікується за допомогою імені та прізвища учня, якому виставлено оцінку, та самої оцінки. У рядках 2–3 містяться автоматичні властивості для цих двох атрибутів.

Оцінки всіх учнів з певного предмета об’єднані в наступному класі TableauDeNotes:


using System;
using System.Text;

namespace Chap2 {

    public class TableauDeNotes : IStats {
        // атрибути
        public string Matière { get; set; }
        public Note[] Notes { get; set; }
        public double Moyenne { get; private set; }
        private double ecartType;

        // конструктор
        public TableauDeNotes(string matière, Note[] notes) {
            // збереження даних за допомогою загальнодоступних властивостей
            Matière = matière;
            Notes = notes;
            // обчислення середнього балу
            double somme = 0;
            for (int i = 0; i < Notes.Length; i++) {
                somme += Notes[i].Valeur;
            }
            if (Notes.Length != 0) Moyenne = somme / Notes.Length;
            else Moyenne = -1;
            // стандартне відхилення
            double carrés = 0;
            for (int i = 0; i < Notes.Length; i++) {
                carrés += Math.Pow((Notes[i].Valeur - Moyenne), 2);
            }//цикл
            if (Notes.Length != 0)
                ecartType = Math.Sqrt(carrés / Notes.Length);
            else ecartType = -1;
        }//конструктор

        public double EcartType() {
            return ecartType;
        }

        // ToString
        public override string ToString() {
            StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
            int i;
            // об'єднуємо всі оцінки
            for (i = 0; i < Notes.Length-1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
            };
            //остання нота
            if (Notes.Length != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
            }
            valeur.Append(")");
            // кінець
            return valeur.ToString();
        }//ToString

    }//клас
}
  • рядок 6: клас TableauDeNotes реалізує інтерфейс IStats. Отже, він повинен реалізувати властивість Moyenne та метод EcartType. Вони реалізовані в рядках 10 (Moyenne) та 35–37 (EcartType)
  • рядки 8–10: три автоматичні властивості
  • рядок 8: предмет, за яким об’єкт зберігає оцінки
  • рядок 9: масив оцінок учнів (Учень, Оцінка)
  • рядок 10: середній бал — властивість, що реалізує властивість Moyenne інтерфейсу IStats.
  • рядок 11: поле, що зберігає стандартне відхилення оцінок - пов'язаний метод get (EcartType) рядків 35–37 реалізує метод EcartType інтерфейсу IStats.
  • рядок 9: оцінки зберігаються в масиві. Цей масив передається під час створення класу TableauDeNotes у конструктор у рядках 14–33.
  • рядки 14–33: конструктор. Тут припускається, що оцінки, передані конструктору, надалі не змінюватимуться. Тому за допомогою конструктора одразу обчислюються середнє значення та стандартне відхилення цих оцінок і зберігаються у полях рядків 10–11. Середнє значення зберігається у приватному полі, що лежить в основі автоматичної властивості Moyenne у рядку 10, а стандартне відхилення — у приватному полі рядка 11.
  • рядок 10: метод get автоматичного властивості Moyenne поверне відповідне приватне поле.
  • рядки 35–37: метод EcartType повертає значення приватного поля з рядка 11.

У цьому коді є кілька тонкощів:

  • рядок 23: метод set властивості Moyenne використовується для присвоєння значення. Цей метод було оголошено приватним у рядку 10, щоб присвоєння значення властивості Moyenne було можливим лише всередині класу.
  • рядки 40–54: використовують об’єкт StringBuilder для побудови рядка, що представляє об’єкт TableauDeNotes, з метою підвищення продуктивності. Слід зазначити, що це значно погіршує читабельність коду. Це зворотний бік медалі.

У попередньому класі оцінки зберігалися в масиві. Після створення об’єкта TableauDeNotes додати нову оцінку було неможливо. Тепер ми пропонуємо другу реалізацію інтерфейсу IStats, яка називається ListeDeNotes, де цього разу оцінки зберігатимуться у списку, з можливістю додавання оцінок після початкового створення об’єкта ListeDeNotes.

Код класу ListeDeNotes виглядає наступним чином:


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

namespace Chap2 {

    public class ListeDeNotes : IStats {
        // атрибути
        public string Matière { get; set; }
        public List<Note> Notes { get; set; }
        public double moyenne = -1;
        public double ecartType = -1;

        // конструктор
        public ListeDeNotes(string matière, List<Note> notes) {
            // збереження через публічні властивості
            Matière = matière;
            Notes = notes;
        }//конструктор

        // додавання примітки
        public void Ajouter(Note note) {
            // додавання оцінки
            Notes.Add(note);
            // середнє значення та стандартне відхилення скинуто
            moyenne = -1;
            ecartType = -1;
        }

        // ToString
        public override string ToString() {
            StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
            int i;
            // об'єднання всіх оцінок
            for (i = 0; i < Notes.Count - 1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
            };
            //остання оцінка
            if (Notes.Count != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
            }
            valeur.Append(")");
            // кінець
            return valeur.ToString();
        }//ToString

        // середнє значення оцінок
        public double Moyenne {
            get {
                if (moyenne != -1) return moyenne;
                // розрахунок середнього значення оцінок
                double somme = 0;
                for (int i = 0; i < Notes.Count; i++) {
                    somme += Notes[i].Valeur;
                }
                // виводимо середнє
                if (Notes.Count != 0) moyenne = somme / Notes.Count;
                return moyenne;
            }
        }

        public double EcartType() {
            // стандартне відхилення
            if (ecartType != -1) return ecartType;
            // середнє значення
            double moyenne = Moyenne;
            double carrés = 0;
            for (int i = 0; i < Notes.Count; i++) {
                carrés += Math.Pow((Notes[i].Valeur - moyenne), 2);
            }//цикл
            // виводимо стандартне відхилення
            if (Notes.Count != 0)
                ecartType = Math.Sqrt(carrés / Notes.Count);
            return ecartType;
        }
    }//клас
}
  • рядок 7: клас ListeDeNotes реалізує інтерфейс IStats
  • рядок 10: примітки тепер розміщуються у списку, а не в масиві
  • рядок 11: автоматичне властивість Moyenne класу TableauDeNotes тут було замінено на приватне поле moyenne, рядок 11, пов’язане з публічним властивістю тільки для читання Moyenne з рядків 48–60
  • рядки 22–28: тепер можна додавати примітки до тих, що вже збережені, чого раніше зробити не можна було.
  • рядки 15–19: відповідно, середнє значення та стандартне відхилення більше не обчислюються у конструкторі, а в самих методах інтерфейсу: Moyenne (рядки 48–60) та EcartType (62–76). Однак перерахунок запускається лише в тому випадку, якщо середнє значення та стандартне відхилення відрізняються від -1 (рядки 50 та 64).

Тестовий клас може виглядати так:


using System;
using System.Collections.Generic;

namespace Chap2 {
    class Program1 {
        static void Main(string[] args) {
            // деякі учні та оцінки з англійської мови
            Elève[] élèves1 =  { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
            Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
            //, які записуються в об’єкт TableauDeNotes
            TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
            // відображення середнього значення та стандартного відхилення
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
            // у об’єкт ListeDeNotes заносяться учні та предмет
            ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
            // відображення середнього значення та стандартного відхилення
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
            // додаємо оцінку
            français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
            // відображення середнього значення та стандартного відхилення
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
        }
    }
}
  • рядок 8: створення масиву учнів із використанням конструктора без параметрів та ініціалізація за допомогою публічних властивостей
  • рядок 9: створення масиву оцінок за тією ж технікою
  • рядок 11: об’єкт TableauDeNotes, для якого обчислюються середнє значення та стандартне відхилення; рядок 13
  • рядок 15: об’єкт ListeDeNotes, для якого в рядку 17 обчислюються середнє значення та стандартне відхилення. Клас List<Note> має конструктор, який приймає об’єкт, що реалізує інтерфейс IEnumerable<Note>. Масив notes1 реалізує цей інтерфейс і може використовуватися для створення об’єкта List<Note>.
  • рядок 19: додавання нової оцінки
  • рядок 21: перерахунок середнього значення та стандартного відхилення

Результати виконання такі:

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

У попередньому прикладі два класи реалізують інтерфейс IStats. Однак у цьому прикладі не видно користі від інтерфейсу IStats. Перепишемо тестову програму таким чином:


using System;
using System.Collections.Generic;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // деякі учні та оцінки з англійської мови
            Elève[] élèves1 =  { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
            Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
            // які записуються в об’єкт TableauDeNotes
            TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
            // відображення середнього значення та стандартного відхилення
            AfficheStats(anglais);
            // у об’єкт ListeDeNotes заносяться учні та предмет
            ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
            // відображення середнього значення та стандартного відхилення
            AfficheStats(français);
            // додаємо оцінку
            français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
            // відображення середнього значення та стандартного відхилення
            AfficheStats(français);
        }

        // відображення середнього значення та стандартного відхилення типу IStats
        static void AfficheStats(IStats valeurs) {
            Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs);
        }
    }
}
  • рядки 25–27: статичний метод AfficheStats приймає як параметр тип IStats, тобто тип «Інтерфейс». Це означає, що фактичним параметром може бути будь-який об’єкт, що реалізує інтерфейс IStats. Коли використовуються дані, що мають тип інтерфейсу, це означає, що будуть використовуватися лише ті методи інтерфейсу, які реалізовані цими даними. Решта ігнорується. Це властивість, схожа на поліморфізм, який ми розглядали для класів. Якщо набір класів Ci, що не пов’язані між собою спадковістю (тож не можна використовувати поліморфізм спадковості), має набір методів з однаковою сигнатурою, може бути доцільним об’єднати ці методи в інтерфейс I, який реалізовуватимуть усі відповідні класи. Інстанції цих класів Ci можна тоді використовувати як фактичні параметри функцій, що приймають формальний параметр типу I, c.a.d, — функцій, які використовують лише методи об’єктів Ci, визначені в інтерфейсі I, а не конкретні атрибути та методи різних класів Ci.
  • рядок 13: метод AfficheStats викликається з типом TableauDeNotes, який реалізує інтерфейс IStats
  • рядок 17: те саме з типом ListeDeNotes

Результати виконання ідентичні результатам попереднього прикладу.

Змінна може мати тип інтерфейсу. Таким чином, можна написати:

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

Оголошення в рядку 1 вказує, що stats1 є екземпляром класу, що реалізує інтерфейс IStats. Ця декларація означає, що компілятор дозволить доступ у stats1 лише до методів інтерфейсу: властивості Moyenne та методу EcartType.

Нарешті, зауважимо, що інтерфейси можуть реалізовуватися кілька разів, наприклад, c.a.d. Це можна записати так:

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

де Ij — це інтерфейси.

4.7. Абстрактні класи

Абстрактний клас — це клас, який не можна інстанціювати. Необхідно створювати похідні класи, які можна буде інстанціювати.

Абстрактні класи можна використовувати для факторизації коду лінійки класів. Розглянемо такий приклад:


using System;

namespace Chap2 {
    abstract class Utilisateur {
        // поля
        private string login;
        private string motDePasse;
        private string role;

        // виробник
        public Utilisateur(string login, string motDePasse) {
            // інформація зберігається
            this.login = login;
            this.motDePasse = motDePasse;
            // ідентифікація користувача
            role=identifie();
            // ідентифіковано?
            if (role == null) {
                throw new ExceptionUtilisateurInconnu(String.Format("[{0},{1}]", login, motDePasse));
            }
        }

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

        // ідентифікує
        abstract public string identifie();
    }
}
  • рядки 11–21: конструктор класу Utilisateur. Цей клас зберігає інформацію про користувача веб-додатка. У ньому є різні типи користувачів, які проходять автентифікацію за допомогою логіна та пароля (рядки 6–7). Ці два параметри перевіряються за допомогою сервісу LDAP для одних користувачів, за допомогою SGBD — для інших тощо...
  • рядки 13–14: дані автентифікації зберігаються
  • рядок 16: вона перевіряється методом identifie. Оскільки метод ідентифікації невідомий, він оголошується абстрактним у рядку 29 за допомогою ключового слова abstract. Метод identifie повертає рядок символів, що вказує роль користувача (загалом те, що він має право робити). Якщо цей рядок є покажчиком null, у рядку 19 генерується виняток.
  • рядок 4: оскільки клас має абстрактний метод, сам клас оголошується абстрактним за допомогою ключового слова `abstract`.
  • рядок 29: абстрактний метод identifie не має визначення. Його визначення нададуть похідні класи.
  • рядки 24–26: метод ToString, який ідентифікує екземпляр класу.

Тут припускається, що розробник хоче мати повний контроль над створенням екземплярів класу Utilisateur та похідних класів, можливо, тому що він хоче бути впевненим, що у разі, якщо користувач не розпізнається, буде згенеровано виняток певного типу (рядок 19). Похідні класи зможуть використовувати цей конструктор. Для цього вони повинні надати метод identifie.

Клас ExceptionUtilisateurInconnu має такий вигляд:


using System;

namespace Chap2 {
    class ExceptionUtilisateurInconnu : Exception {
        public ExceptionUtilisateurInconnu(string message) : base(message){
        }
    }
}
  • рядок 3: він походить від класу Exception
  • рядки 4–6: він має лише один конструктор, який приймає як параметр повідомлення про помилку. Це повідомлення передається батьківському класу (рядок 5), який має той самий конструктор.

Тепер ми похідний клас Utilisateur від дочірнього класу Administrateur:


namespace Chap2 {
    class Administrateur : Utilisateur {
        // виробник
        public Administrateur(string login, string motDePasse)
            : base(login, motDePasse) {
        }

        // ідентифікатор
        public override string identifie() {
            // ідентифікація LDAP
            // ...
            return "admin";
        }
    }
}
  • рядки 4–6: конструктор просто передає до свого батьківського класу отримані параметри
  • рядки 9–12: метод `identifie` класу Administrateur. Припустимо, що адміністратора ідентифікує система LDAP. Цей метод перевизначає метод «identifie» свого батьківського класу. Оскільки він перевизначає абстрактний метод, немає потреби вказувати ключове слово override.

Тепер ми похідним класом Utilisateur від класу-батька Observateur:


namespace Chap2 {
    class Observateur : Utilisateur{
        // виробник
        public Observateur(string login, string motDePasse)
            : base(login, motDePasse) {
        }

        //ідентифікатор
        public override string identifie() {
            // ідентифікація SGBD
            // ...
            return "observateur";
        }
        
    }
}
  • рядки 4–6: конструктор просто передає отримані параметри своєму батьківському класу
  • рядки 9–13: метод ідентифікації класу Observateur. Припускаємо, що спостерігача ідентифікують шляхом перевірки його ідентифікаційних даних у базі даних.

У підсумку об’єкти Administrateur та Observateur інстанціюються одним і тим самим конструктором — конструктором батьківського класу Utilisateur.. Цей конструктор використовуватиме метод ідентифікації, який надають ці класи.

Третій клас Inconnu також походить від класу Utilisateur:


namespace Chap2 {
    class Inconnu : Utilisateur{

        // виробник
        public Inconnu(string login, string motDePasse)
            : base(login, motDePasse) {
        }

        //ідентифікатор
        public override string identifie() {
            // користувач невідомий
            // ...
            return null;
        }
        
    }
}
  • рядок 13: метод identifie повертає покажчик null, щоб вказати, що користувач не був розпізнаний.

Тестова програма може виглядати так:


using System;

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

Зверніть увагу, що в рядках 6, 7 та 9 метод [Utilisateur].ToString() буде викликаний методом WriteLine.

Результати виконання такі:

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

4.8. Класи, інтерфейси, генеричні методи

Припустимо, що ми хочемо написати метод, який міняє місцями два цілі числа. Цей метод може виглядати так:


        public static void Echanger1(ref int value1, ref int value2){
            // обмін посиланнями value1 та value2
            int temp = value2;
            value2 = value1;
            value1 = temp;
}

Тепер, якщо ми хочемо поміняти місцями дві посилання на об’єкти Personne, ми напишемо:


        public static void Echanger2(ref Personne value1, ref Personne value2){
            // обмінюємо посилання value1 та value2
            Personne temp = value2;
            value2 = value1;
            value1 = temp;
}

Відмінність між цими двома методами полягає в типі T параметрів: int замість Echanger1, Personne замість Echanger2. Генеральні класи та інтерфейси задовольняють потребу в методах, які відрізняються лише типом деяких своїх параметрів.

За допомогою генеричного класу метод Echanger можна переписати таким чином:


namespace Chap2 {
    class Generic1<T> {
        public static void Echanger(ref T value1, ref T value2){
            // обмінюємо посилання value1 та value2
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • рядок 2: клас Generic1 параметризується типом, позначеним як T. Йому можна дати будь-яке ім’я. Цей тип T потім повторно використовується в класі в рядках 3 і 5. Кажуть, що клас Generic1 є генеричним класом.
  • рядок 3: визначає дві посилання на тип T, які потрібно замінити
  • рядок 5: тимчасова змінна temp має тип T.

Програма для тестування класу може виглядати так:


using System;

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

        }
    }
}
  • рядок 8: при використанні узагальненого класу, параметризованого типами T1, T2, ... ці типи повинні бути «інстанційовані». У рядку 8 використовується статичний метод Echanger типу Generic1<int>, щоб вказати, що посилання, передані до методу Echanger, мають тип int.
  • рядок 12: використовується статичний метод Echanger типу Generic1<string>, щоб вказати, що посилання, передані методу Echanger, мають тип string.
  • рядок 16: використовується статичний метод Echanger типу Generic1<Personne>, щоб вказати, що посилання, передані методу Echanger, мають тип Personne.

Результати виконання такі:

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

Метод Echanger також можна було б записати таким чином:


namespace Chap2 {
    class Generic2 {
        public static void Echanger<T>(ref T value1, ref T value2){
            // обмінюємося посиланнями value1 та value2
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • рядок 2: клас Generic2 більше не є узагальненим
  • рядок 3: статичний метод Echanger є генеричним

Тоді тестова програма виглядає так:


using System;

namespace Chap2 {
    class Program2 {
        static void Main(string[] args) {
            // int
            int i1 = 1, i2 = 2;
            Generic2.Echanger<int>(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // рядок
            string s1 = "s1", s2 = "s2";
            Generic2.Echanger<string>(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
            // Особа
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic2.Echanger<Personne>(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);
        }
    }
}
  • рядки 8, 12 та 16: викликається метод Echanger із зазначенням у <> типу параметрів. Насправді компілятор здатний на основі типу фактичних параметрів визначити, який варіант методу Echanger слід використовувати. Тому такий запис є допустимим:

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

Рядки 1, 3 та 5: варіант методу Echanger, що викликається, більше не вказується. Компілятор здатний визначити його на основі характеру фактичних параметрів, що використовуються.

На загальні параметри можна накласти обмеження:

Image

Розглянемо такий новий генеричний метод Echanger:


namespace Chap2 {
    class Generic3 {
        public static void Echanger<T>(ref T value1, ref T value2) where T : class {
            // обмінюємося посиланнями value1 та value2
            T temp = value2;
            value2 = value1;
            value1 = temp;
        }
    }
}
  • рядок 3: вимагається, щоб тип T був посиланням (клас, інтерфейс)

Розглянемо таку тестову програму:


using System;

namespace Chap2 {
    class Program4 {
        static void Main(string[] args) {
            // int
            int i1 = 1, i2 = 2;
            Generic3.Echanger<int>(ref i1, ref i2);
            Console.WriteLine("i1={0},i2={1}", i1, i2);
            // рядок
            string s1 = "s1", s2 = "s2";
            Generic3.Echanger(ref s1, ref s2);
            Console.WriteLine("s1={0},s2={1}", s1, s2);
            // Особа
            Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
            Generic3.Echanger(ref p1, ref p2);
            Console.WriteLine("p1={0},p2={1}", p1, p2);

        }
    }
}

Компілятор повідомляє про помилку в рядку 8, оскільки тип int не є класом чи інтерфейсом, а є структурою:

Image

Розглянемо такий новий узагальнений метод Echanger:


namespace Chap2 {
    class Generic4 {
        public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
            // отримуємо значення двох елементів
            int value1 = element1.Value();
            int value2 = element2.Value();
            // якщо перший елемент > другий елемент, то міняємо місцями елементи
            if (value1 > value2) {
                T temp = element2;
                element2 = element1;
                element1 = temp;
            }
        }
    }
}
  • рядок 3: тип T повинен реалізовувати інтерфейс Interface1. Цей інтерфейс має метод Value,, який використовується в рядках 5 і 6 і повертає значення об’єкта типу T.
  • рядки 8–12: обидва посилання element1 та element2 обмінюються лише в тому випадку, якщо значення element1 більше за значення element2.

Інтерфейс Interface1 має такий вигляд:


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

Він реалізований наступним класом Class1:


using System;
using System.Threading;

namespace Chap2 {
    class Class1 : Interface1 {
        // значення об’єкта
        private int value;

        // конструктор
        public Class1() {
            // затримка 1 мс
            Thread.Sleep(1);
            // випадкове значення від 0 до 99
            value = new Random(DateTime.Now.Millisecond).Next(100);
        }

        // метод доступу до приватного поля value
        public int Value() {
            return value;
        }

        // стан екземпляра
        public override string ToString() {
            return value.ToString();
        }
    }
}
  • рядок 5: Class1 реалізує інтерфейс Interface1
  • рядок 7: значення екземпляра Class1
  • рядки 10–14: поле value ініціалізується випадковим значенням від 0 до 99
  • рядки 18–20: метод Value інтерфейсу Interface1
  • рядки 23–25: метод ToString класу

Інтерфейс Interface1 також реалізовано класом Class2:


using System;

namespace Chap2 {
    class Class2 : Interface1 {
        // значення об’єкта
        private int value;
        private String s;

        // конструктор
        public Class2(String s) {
            this.s = s;
            value = s.Length;
        }

        // метод доступу до приватного поля value
        public int Value() {
            return value;
        }

        // стан екземпляра
        public override string ToString() {
            return s;
        }
    }
}
  • рядок 4: Class2 реалізує інтерфейс Interface1
  • рядок 6: значення екземпляра класу Class2
  • рядки 10–13: поле value ініціалізується довжиною символьного рядка, переданого конструктору
  • рядки 16–18: метод Value інтерфейсу Interface1
  • рядки 21–22: метод ToString класу

Тестова програма може виглядати так:


using System;

namespace Chap2 {
    class Program5 {
        static void Main(string[] args) {
            // обмін екземплярами типу Class1
            Class1 c1, c2;
            for (int i = 0; i < 5; i++) {
                c1 = new Class1();
                c2 = new Class1();
                Console.WriteLine("Avant échange --> c1={0},c2={1}", c1, c2);
                Generic4.Echanger(ref c1, ref c2);
                Console.WriteLine("Après échange --> c1={0},c2={1}", c1, c2);
            }
            // обмін екземплярами типу Class2
            Class2 c3, c4;
            c3 = new Class2("xxxxxxxxxxxxxx");
            c4 = new Class2("xx");
            Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
            Generic4.Echanger(ref c3, ref c4);
            Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
        }
    }
}
  • рядки 8–14: обмінюються екземплярами типу Class1
  • рядки 16–22: обмінюються екземплярами типу Class2

Результати виконання такі:

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

Щоб проілюструвати поняття «генерального інтерфейсу», ми відсортуємо масив осіб спочатку за іменами, а потім за віком. Метод, який дозволяє нам сортувати масив, — це статичний метод Sort класу Array:

Image

Нагадаємо, що статичний метод використовується шляхом додавання до імені методу імені класу, а не імені екземпляра класу. Метод Sort має різні сигнатури (він перевантажений). Ми будемо використовувати таку сигнатуру:

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

Sort — це генеричний метод, де T позначає будь-який тип. Метод приймає два параметри:

  • T[] масив: масив елементів типу T, що підлягають сортуванню
  • IComparer<T> — компаратор: посилання на об’єкт, що реалізує інтерфейс IComparer<T>.

IComparer<T> — це узагальнений інтерфейс, який визначається так:

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

Інтерфейс IComparer<T> має лише один метод. Метод Compare:

  • приймає як параметри два елементи t1 та t2 типу T
  • повертає 1, якщо t1>t2, 0, якщо t1==t2, -1, якщо t1<t2. Розробник сам визначає значення операторів <, ==, >. Наприклад, якщо p1 і p2 — два об’єкти типу Personne, можна сказати, що p1 > p2, якщо ім’я p1 стоїть перед ім’ям p2 в алфавітному порядку. У такому разі ми отримаємо сортування за іменами осіб у порядку зростання. Якщо ж потрібно сортування за віком, то p1>p2, якщо вік p1 більший за вік p2.
  • Щоб отримати сортування у спадному порядку, достатньо поміняти місцями результати +1 і -1

Ми знаємо достатньо, щоб впорядкувати масив осіб. Програма виглядає так:


using System;
using System.Collections.Generic;

namespace Chap2 {
    class Program6 {
        static void Main(string[] args) {
            // масив осіб
            Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
            // відображення
            Affiche("Tableau à trier", personnes1);
            // сортування за іменем
            Array.Sort(personnes1, new CompareNoms());
            // відображення
            Affiche("Tableau après le tri selon les nom et prénom", personnes1);
            // сортування за віком
            Array.Sort(personnes1, new CompareAges());
            // відображення
            Affiche("Tableau après le tri selon l'âge", personnes1);
        }

        static void Affiche(string texte, Personne[] personnes) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (Personne p in personnes) {
                Console.WriteLine(p);
            }
        }
    }

    // клас порівняння прізвищ та імен осіб
    class CompareNoms : IComparer<Personne> {
        public int Compare(Personne p1, Personne p2) {
            // порівняння прізвищ
            int i = p1.Nom.CompareTo(p2.Nom);
            if (i != 0)
                return i;
            // збіг прізвищ — порівняння імен
            return p1.Prenom.CompareTo(p2.Prenom);
        }
    }

    // клас порівняння віку осіб
    class CompareAges : IComparer<Personne> {
        public int Compare(Personne p1, Personne p2) {
            // порівняння віку
            if (p1.Age > p2.Age)
                return 1;
            else if (p1.Age == p2.Age)
                return 0;
            else
                return -1;
        }
    }

}
  • рядок 8: масив осіб
  • рядок 12: сортування масиву осіб за прізвищем та ім’ям. Другий параметр генеричного методу Sort є екземпляром класу CompareNoms, що реалізує генеричний інтерфейс IComparer<Personne>.
  • рядки 30–39: клас CompareNoms, що реалізує генеричний інтерфейс IComparer<Personne>.
  • рядки 31–38: реалізація генеричного методу int CompareTo(T, T) інтерфейсу IComparer<T>. Метод використовує метод String.CompareTo, описаний у розділі 3.3.5.4, для порівняння двох символьних рядків.
  • рядок 16: сортування масиву осіб за віком. Другий параметр генеричного методу Sort є екземпляром класу CompareAges, який реалізує генеричний інтерфейс IComparer<Personne> і визначений у рядках 42–51.

Результати виконання такі:

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

4.9. Простори імен

Щоб вивести рядок на екран, ми використовуємо оператор

Console.WriteLine(...)

Якщо розглянути визначення класу Console


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

, то побачимо, що він належить до простору імен System. Це означає, що клас Console слід позначати як System.Console, і насправді слід писати:

System.Console.WriteLine(...)

Цього можна уникнути, використовуючи клаузулу using:

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

Кажуть, що простір імен System імпортується за допомогою клаузули using. Коли компілятор натрапить на ім’я класу (у даному випадку Console), він спробує знайти його в різних просторах імен, імпортованих за допомогою клаузул using. Тут він знайде клас Console у просторі імен System. Тепер звернімо увагу на другу інформацію, пов’язану з класом Console:

Assembly: Mscorlib (in Mscorlib.dll)

Цей рядок вказує, у якому «асемблі» знаходиться визначення класу Console. Ця інформація може виявитися корисною, коли компіляція відбувається поза Visual Studio і потрібно вказати посилання на різні dll, що містять класи, які необхідно використовувати. Щоб вказати dll, необхідні для компіляції класу, потрібно написати:

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

де csc — це компілятор C#. Створюючи клас, його можна розмістити в просторі імен. Мета цих просторів імен — уникнути конфліктів імен між класами, наприклад, коли їх продають. Розглянемо дві компанії E1 та E2, які розповсюджують класи, упаковані відповідно в dll, e1.dll та e2.dll. Нехай клієнт C купує ці два набори класів, у яких обидві компанії визначили клас Personne. Клієнт C компілює програму наступним чином:

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

Якщо у вихідному коді prog.cs використовується клас Personne, компілятор не зможе визначити, чи слід йому взяти клас Personne із e1.dll, чи з e2.dll. Він повідомить про помилку. Якщо компанія E1 подбає про те, щоб створити свої класи в просторі імен під назвою E1, а компанія E2 — у просторі імен під назвою E2, то обидва класи Personne матимуть назви E1.Personne та E2.Personne. Клієнт повинен використовувати у своїх класах або E1.Personne, або E2.Personne, але не Personne. Простір імен дозволяє усунути неоднозначність.

Щоб створити клас у просторі імен, потрібно написати:

namespace EspaceDeNoms{
     // визначення класу
}

4.10. Приклад застосування — V2

Повернемося до розрахунку податку, який вже розглядався в попередньому розділі, параграфі 3.6, і тепер обробимо його з використанням класів та інтерфейсів. Нагадаємо задачу:

Ми маємо намір написати програму, яка дозволить обчислити податок платника податків. Розглянемо спрощений випадок, коли платник податків має задекларувати лише свою заробітну плату (дані за 2004 рік щодо доходів за 2003 рік):

  • кількість часток працівника обчислюється за формулою nbParts = nbEnfants/2 + 1, якщо він не одружений, nbEnfants/2+2, якщо він одружений, де nbEnfants — кількість його дітей.
  • якщо у нього щонайменше троє дітей, він отримує на пів частки більше
  • розраховується його оподатковуваний дохід R = 0,72 * S, де S — його річна заробітна плата
  • розраховується його сімейний коефіцієнт QF = R / nbParts
  • розраховуємо його податок I. Розглянемо таку таблицю:
4262
0
0
8382
0,0683
291,09
14753
0,1914
1322,92
23888
0,2826
2668,39
38868
0,3738
4846,98
47932
0,4262
6883,66
0
0,4809
9505,54

Кожен рядок має 3 поля. Щоб обчислити податок I, шукаємо перший рядок, де QF <= поле1. Наприклад, якщо QF = 5000, то знайдемо рядок

    8382        0.0683        291.09

Тоді податок I дорівнює 0,0683*R - 291,09*nbParts. Якщо QF є таким, що співвідношення QF<=поле1 ніколи не виконується, то використовуються коефіцієнти останнього рядка. У даному випадку:

    0                0.4809    9505.54

що дає податок I=0,4809*R - 9505,54*nbParts.

Спочатку ми визначаємо структуру, здатну інкапсулювати рядок з попередньої таблиці:


namespace Chap2 {
    // податковий діапазон
    struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}

Потім ми визначаємо інтерфейс IImpot, здатний обчислювати податок:


namespace Chap2 {
    interface IImpot {
        int calculer(bool marié, int nbEnfants, int salaire);
    }
}
  • рядок 3: метод розрахунку податку на основі трьох даних: сімейний стан платника податків, кількість дітей та розмір заробітної плати

Далі ми визначаємо абстрактний клас, що реалізує цей інтерфейс:


namespace Chap2 {
    abstract class AbstractImpot : IImpot {

        // податкові шкали, необхідні для розрахунку податку
        // походять із зовнішнього джерела

        protected TrancheImpot[] tranchesImpot;

        // розрахунок податку
        public int calculer(bool marié, int nbEnfants, int salaire) {
            // розрахунок кількості часток
            decimal nbParts;
            if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;
            // розрахунок оподатковуваного доходу та сімейного коефіцієнта
            decimal revenu = 0.72M * salaire;
            decimal QF = revenu / nbParts;
            // розрахунок податку
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite) i++;
            // повернення результату
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
        }//розрахувати
    }// клас

}
  • рядок 2: клас AbstractImpot реалізує інтерфейс IImpot.
  • рядок 7: річні дані розрахунку податку у вигляді захищеного поля. Клас AbstractImpot не знає, як буде ініціалізовано це поле. Він залишає це на розсуд похідних класів. Саме тому він оголошений абстрактним (рядок 2), щоб заборонити будь-яке його інстанціювання.
  • рядки 10–25: реалізація методу calculer інтерфейсу IImpot. Похідним класам не доведеться переписувати цей метод. Таким чином, клас AbstractImpot слугує класом-факторизатором для похідних класів. У ньому розміщується те, що є спільним для всіх похідних класів.

Клас, що реалізує інтерфейс IImpot, можна створити шляхом успадкування від класу AbstractImpot. Саме це ми й зробимо зараз:


using System;

namespace Chap2 {
    class HardwiredImpot : AbstractImpot {

        // таблиці даних, необхідні для розрахунку податку
        decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
        decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
        decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };

        public HardwiredImpot() {
                // створення таблиці податкових шкал
            tranchesImpot = new TrancheImpot[limites.Length];
                // заповнення
            for (int i = 0; i < tranchesImpot.Length; i++) {
                tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
                }
        }
    }// клас
}// простір імен

Клас HardwiredImpot у рядках 7–9 жорстко визначає дані, необхідні для розрахунку податку. Його конструктор (рядки 11–18) використовує ці дані для ініціалізації захищеного поля tranchesImpot батьківського класу AbstractImpot.

Тестова програма може виглядати так:


using System;

namespace Chap2 {
    class Program {
        static void Main() {
            // інтерактивна програма для розрахунку податку
            // користувач вводить три значення з клавіатури: одружений nbEnfants заробітна плата
            // після чого програма відображає суму податку до сплати

            const string syntaxe = "syntaxe : Marié NbEnfants Salaire\n"
                            + "Marié : o pour marié, n pour non marié\n"
                            + "NbEnfants : nombre d'enfants\n"
                            + "Salaire : salaire annuel en F";

            // створення об’єкта IImpot
            IImpot impot = new HardwiredImpot();

            // нескінченний цикл
            while (true) {
                // запитуються параметри розрахунку податку
                Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
                string paramètres = Console.ReadLine().Trim();
                // що робити?
                if (paramètres == null || paramètres == "") break;
                // перевірка кількості аргументів у введеному рядку
                string[] args = paramètres.Split(null);
                int nbParamètres = args.Length;
                if (nbParamètres != 3) {
                    Console.WriteLine(syntaxe);
                    continue;
                }//if
                // перевірка правильності параметрів
                // одружений
                string marié = args[0].ToLower();
                if (marié != "o" && marié != "n") {
                    Console.WriteLine(syntaxe + "\nArgument marié incorrect : tapez o ou n");
                    continue;
                }//if
                // nbEnfants
                int nbEnfants = 0;
                bool dataOk = false;
                try {
                    nbEnfants = int.Parse(args[1]);
                    dataOk = nbEnfants >= 0;
                } catch {
                }//if
                // дані правильні?
                if (!dataOk) {
                    Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
                    continue;
                }
                // зарплата
                int salaire = 0;
                dataOk = false;
                try {
                    salaire = int.Parse(args[2]);
                    dataOk = salaire >= 0;
                } catch {
                }//try-catch
                // дані правильні?
                if (!dataOk) {
                    Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
                    continue;
                }
                // параметри правильні — розраховуємо податок
                Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
                // наступний платник податків
            }//while
        }
    }
}

Наведена вище програма дозволяє користувачеві виконувати багаторазові симуляції розрахунку податку.

  • рядок 16: створення об’єкта impot, що реалізує інтерфейс IImpot. Цей об’єкт отримується шляхом інстанціювання типу HardwiredImpot, який реалізує інтерфейс IImpot. Слід зауважити, що змінній impot було присвоєно не тип HardwiredImpot, а тип IImpot. Таким чином ми вказуємо, що нас цікавить лише метод calculer об’єкта impot, а не решта.
  • рядки 19–68: цикл моделювання розрахунку податку
  • рядок 22: три параметри, необхідні для методу calculer, вводяться одним рядком з клавіатури.
  • рядок 26: метод [chaine].Split(null) дозволяє розбити [chaine] на слова. Вони зберігаються у масиві args.
  • рядок 66: виклик методу calculer об’єкта impot, що реалізує інтерфейс IImpot.

Ось приклад виконання програми:

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