4. Klasy, struktury, interfejsy
4.1. Obiekt na przykładzie
4.1.1. Informacje ogólne
Teraz zajmiemy się programowaniem obiektowym na przykładzie. Obiekt to jednostka zawierająca dane określające jego stan (nazywane polami, atrybutami itp.) oraz funkcje (nazywane metodami). Obiekt jest tworzony na podstawie wzorca zwanego klasą:
public class C1{
Type1 p1; // pole p1
Type2 p2; // pole p2
…
Type3 m3(…){ // metoda m3
…
}
Type4 m4(…){ // metoda m4
…
}
…
}
Na podstawie poprzedniej klasy C1 można utworzyć wiele obiektów O1, O2,… Wszystkie będą miały pola p1, p2,… oraz metody m3, m4,… Jednak będą miały różne wartości w swoich polach pi, dzięki czemu każdy z nich będzie miał swój własny stan. Jeśli o1 jest obiektem typu C1, o1.p1 oznacza właściwość p1 obiektu o1, a o1.m1 oznacza metodę m1 obiektu O1.
Rozważmy pierwszy model obiektu: klasę Personne.
4.1.2. Tworzenie projektu w języku C#
W poprzednich przykładach w projekcie mieliśmy tylko jeden plik źródłowy: Program.cs. Od tej pory w jednym projekcie możemy mieć kilka plików źródłowych. Pokażemy, jak to zrobić.
![]() |
W pliku [1] utwórz nowy projekt. W pliku [2] wybierz opcję „Aplikacja konsolowa”. W pliku [3] pozostaw wartość domyślną. W pliku [4] zatwierdź zmiany. W [5] znajduje się wygenerowany projekt. Zawartość pliku Program.cs jest następująca:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Program {
static void Main(string[] args) {
}
}
}
Zapiszmy utworzony projekt:
![]() |
W oknie [1] wybierz opcję zapisywania. W oknie [2] wskaż folder, w którym chcesz zapisać projekt. W oknie [3] nadaj nazwę projektowi. W oknie [5] zaznacz, że chcesz utworzyć rozwiązanie. Rozwiązanie to zbiór projektów. W oknie [4] nadaj nazwę rozwiązaniu. W oknie [6] zatwierdź zapisanie.
![]() |
W oknie [1] wyświetli się zapisany projekt. W oknie [2] dodaj nowy element do projektu.
![]() |
W [1] należy wskazać, że chcesz dodać klasę. W [2] należy podać nazwę klasy. W [3] zatwierdź informacje. W polu [4] projekt [01] ma nowy plik źródłowy Personne.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Personne {
}
}
Zmieniamy przestrzeń nazw każdego z plików źródłowych na Chap2 i usuwamy import niepotrzebnych przestrzeni nazw:
using System;
namespace Chap2 {
class Personne {
}
}
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
}
}
}
4.1.3. Definicja klasy Personne
Definicja klasy Personne w pliku źródłowym [Personne.cs] będzie wyglądać następująco:
using System;
namespace Chap2 {
public class Personne {
// atrybuty
private string prenom;
private string nom;
private int age;
// metoda
public void Initialise(string P, string N, int age) {
this.prenom = P;
this.nom = N;
this.age = age;
}
// metoda
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Mamy tu do czynienia z definicją klasy, a więc typu danych. Tworząc zmienne tego typu, będziemy nazywać je obiektami lub instancjami klasy. Klasa jest zatem szablonem, na podstawie którego tworzone są obiekty.
Członkowie lub pola klasy mogą być danymi (atrybutami), metodami (funkcjami) lub właściwościami. Właściwości to specjalne metody służące do sprawdzania lub ustalania wartości atrybutów obiektu. Pola te mogą być opatrzone jednym z trzech następujących słów kluczowych:
Pole prywatne (private) jest dostępne wyłącznie dla wewnętrznych metod klasy | |
Pole publiczne (public) jest dostępne dla każdej metody, niezależnie od tego, czy została zdefiniowana w tej klasie, czy nie | |
Pole chronione (protected) jest dostępne wyłącznie za pośrednictwem wewnętrznych metod klasy lub obiektu pochodnego (patrz dalej: koncepcja dziedziczenia). |
Zazwyczaj dane klasy są deklarowane jako prywatne, podczas gdy jej metody i właściwości są deklarowane jako publiczne. Oznacza to, że użytkownik obiektu (programista)
- nie będzie miał bezpośredniego dostępu do danych prywatnych obiektu
- będzie mógł korzystać z publicznych metod obiektu, a w szczególności z tych, które zapewniają dostęp do jego danych prywatnych.
Składnia deklaracji klasy w języku C jest następująca:
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;
}
Kolejność deklaracji atrybutów private, protected i public jest dowolna.
4.1.4. Metoda Initialise
Wróćmy do naszej klasy Personne zadeklarowanej jako:
using System;
namespace Chap2 {
public class Personne {
// atrybuty
private string prenom;
private string nom;
private int age;
// metoda
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
// metoda
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Jaka jest rola metody Initialise? Ponieważ nom, prenom i age są danymi prywatnymi klasy Personne, instrukcje:
są niedozwolone. Musimy zainicjować obiekt typu Personne za pomocą metody publicznej. Do tego służy metoda Initialise. Napiszemy:
Zapis p1.Initialise jest poprawny, ponieważ Initialise jest publicznie dostępny.
4.1.5. Operator new
Sekwencja instrukcji
jest nieprawidłowa. Instrukcja
deklaruje p1 jako odwołanie do obiektu typu Personne. Obiekt ten jeszcze nie istnieje, a zatem p1 nie jest zainicjowany. To tak, jakbyśmy napisali:
gdzie za pomocą słowa kluczowego null wyraźnie wskazujemy, że zmienna p1 nie odwołuje się jeszcze do żadnego obiektu. Gdy następnie zapisujemy
wywołujemy metodę Initialise obiektu, do którego odwołuje się zmienna p1. Jednak ten obiekt jeszcze nie istnieje, więc kompilator zgłosi błąd. Aby zmienna p1 odwoływała się do obiektu, należy zapisać:
Spowoduje to utworzenie jeszcze niezinicjalizowanego obiektu typu Personne: atrybuty nom i prenom, które są odwołaniami do obiektów typu String, będą miały wartość null, a age – wartość 0. Ma więc miejsce inicjalizacja domyślna. Teraz, gdy p1 odwołuje się do obiektu, instrukcja inicjalizacji tego obiektu
jest poprawna.
4.1.6. Słowo kluczowe „this”
Przyjrzyjmy się kodowi metody initialise:
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
Instrukcja this.prenom=p oznacza, że atrybut prenom bieżącego obiektu (this) otrzymuje wartość p. Słowo kluczowe this określa obiekt bieżący: ten, w którym znajduje się wykonywana metoda. Skąd to wiemy? Przyjrzyjmy się, jak przebiega inicjalizacja obiektu, do którego odwołuje się p1 w programie wywołującym:
Wywoływana jest metoda Initialise obiektu p1. Kiedy w tej metodzie odwołujemy się do obiektu this, w rzeczywistości odwołujemy się do obiektu p1. Metoda Initialise mogłaby również zostać zapisana w następujący sposób:
public void Initialise(string p, string n, int age) {
prenom = p;
nom = n;
this.age = age;
}
Gdy metoda obiektu odwołuje się do atrybutu A tego obiektu, zapis this.A jest domyślny. Należy go użyć jawnie w przypadku konfliktu identyfikatorów. Dotyczy to następującej instrukcji:
this.age=age;
gdzie age oznacza atrybut bieżącego obiektu, a także parametr age przekazany do metody. Należy wówczas usunąć niejednoznaczność, oznaczając atrybut age jako this.age.
4.1.7. Program testowy
Oto krótki program testowy. Znajduje się on w pliku źródłowym [Program.cs]:
using System;
namespace Chap2 {
class P01 {
static void Main() {
Personne p1 = new Personne();
p1.Initialise("Jean", "Dupont", 30);
p1.Identifie();
}
}
}
Przed uruchomieniem projektu [01] może być konieczne wskazanie pliku źródłowego do wykonania:
![]() |
W właściwościach projektu [01] jako klasę do wykonania wskazuje się [1].
Wyniki uzyskane podczas uruchomienia są następujące:
4.1.8. Kolejna metoda Initialise
Rozważmy ponownie klasę Personne i dodajmy do niej następującą metodę:
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
Mamy teraz dwie metody o nazwie Initialise: jest to dopuszczalne, o ile przyjmują one różne parametry. Tak właśnie jest w tym przypadku. Parametrem jest teraz odwołanie p do osoby. Atrybuty osoby p są następnie przypisywane do bieżącego obiektu (this). Warto zauważyć, że metoda Initialise ma bezpośredni dostęp do atrybutów obiektu p, mimo że są one typu private. Zasada ta zawsze obowiązuje: obiekt o1 klasy C zawsze ma dostęp do atrybutów obiektów tej samej klasy C.
Oto test nowej klasy 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();
}
}
}
oraz jego wyniki:
4.1.9. Konstruktory klasy Personne
Konstruktor to metoda o nazwie zgodnej z nazwą klasy, wywoływana podczas tworzenia obiektu. Zazwyczaj służy ona do zainicjowania obiektu. Jest to metoda, która może przyjmować argumenty, ale nie zwraca żadnego wyniku. Jej prototyp lub definicja nie są poprzedzone żadnym typem (nawet void).
Jeśli klasa C posiada konstruktor przyjmujący n argumentów argi, deklaracja i inicjalizacja obiektu tej klasy może odbywać się w następującej postaci:
lub
Gdy klasa C posiada jeden lub więcej konstruktorów, do utworzenia obiektu tej klasy należy obowiązkowo użyć jednego z tych konstruktorów. Jeśli klasa C nie posiada żadnego konstruktora, ma ona domyślny konstruktor bez parametrów: public C(). Atrybuty obiektu są wówczas inicjowane wartościami domyślnymi. Tak właśnie było w poprzednich programach, w których napisaliśmy:
Stwórzmy dwa konstruktory dla naszej klasy Personne:
using System;
namespace Chap2 {
public class Personne {
// atrybuty
private string prenom;
private string nom;
private int age;
// konstruktory
public Personne(String p, String n, int age) {
Initialise(p, n, age);
}
public Personne(Personne P) {
Initialise(P);
}
// metoda
public void Initialise(string p, string n, int age) {
...
}
public void Initialise(Personne p) {
...
}
// metoda
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Nasze dwa konstruktory ograniczają się do wywoływania metod Initialise, które omówiliśmy wcześniej. Przypominamy, że gdy w konstruktorze pojawia się na przykład zapis Initialise(p), kompilator przekształca go na this.Initialise(p). W konstruktorze wywoływana jest zatem metoda Initialise w celu przetworzenia obiektu, do którego odwołuje się this, czyli obiektu bieżącego, który jest właśnie tworzony.
Oto krótki program testowy:
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();
}
}
}
oraz uzyskane wyniki:
4.1.10. Odwołania do obiektów
Zawsze używamy tej samej klasy Personne. Program testowy wygląda następująco:
using System;
namespace Chap2 {
class Program2 {
static void Main() {
// p1
Personne p1 = new Personne("Jean", "Dupont", 30);
Console.Write("p1="); p1.Identifie();
// p2 odwołuje się do tego samego obiektu co p1
Personne p2 = p1;
Console.Write("p2="); p2.Identifie();
// p3 odwołuje się do obiektu, który będzie kopią obiektu, do którego odwołuje się p1
Personne p3 = new Personne(p1);
Console.Write("p3="); p3.Identifie();
// zmieniamy stan obiektu, do którego odwołuje się p1
p1.Initialise("Micheline", "Benoît", 67);
Console.Write("p1="); p1.Identifie();
// ponieważ p2 = p1, obiekt, do którego odwołuje się p2, musiał zmienić swój stan
Console.Write("p2="); p2.Identifie();
// ponieważ p3 nie odwołuje się do tego samego obiektu co p1, obiekt, do którego odwołuje się p3, nie musiał ulec zmianie
Console.Write("p3="); p3.Identifie();
}
}
}
Otrzymane wyniki są następujące:
Gdy zadeklarujemy zmienną p1 za pomocą
p1 odwołuje się do obiektu Personne("Jean","Dupont",30), ale nie jest samym obiektem. W języku C można by powiedzieć, że jest to wskaźnik, c.a.d, wskazujący adres utworzonego obiektu. Jeśli następnie wpiszemy:
To nie obiekt Personne("Jean","Dupont",30) zostaje zmodyfikowany, lecz zmienia się wartość odwołania p1. Obiekt Personne("Jean","Dupont",30) zostanie „utracony”, jeśli nie będzie odwołany przez żadną inną zmienną.
Gdy piszemy:
inicjujemy wskaźnik p2: „wskazuje” on na ten sam obiekt (oznacza ten sam obiekt), co wskaźnik p1. Zatem jeśli zmodyfikujemy obiekt, na który „wskazuje” (lub do którego odwołuje się) wskaźnik p1, zmodyfikujemy również obiekt, do którego odwołuje się wskaźnik p2.
Gdy zapisujemy:
tworzony jest nowy obiekt o nazwie Personne. Ten nowy obiekt będzie odwołany przez p3. Jeśli zmodyfikujemy obiekt, na który „wskazuje” (lub do którego odwołuje się) p1, nie wpłynie to w żaden sposób na obiekt, do którego odwołuje się p3. Potwierdzają to uzyskane wyniki.
4.1.11. Przekazywanie parametrów typu odwołania do obiektu
W poprzednim rozdziale omówiliśmy sposoby przekazywania parametrów funkcji, gdy reprezentowały one prosty typ języka C# w postaci struktury .NET. Zobaczmy, co się dzieje, gdy parametr jest odwołaniem do obiektu:
using System;
using System.Text;
namespace Chap1 {
class P12 {
public static void Main() {
// przykład 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);
}
}
}
- wiersz 8: definiuje 3 obiekty typu StringBuilder. Obiekt typu StringBuilder znajduje się w pobliżu obiektu typu string.. Podczas operacji na obiekcie typu string otrzymujemy w zamian nowy obiekt typu string. Tak więc w poniższym fragmencie kodu:
Wiersz 1 tworzy w pamięci obiekt string, a s jest jego adresem. W wierszu 2 funkcja s.ToUpperCase() tworzy w pamięci kolejny obiekt string. W ten sposób między wierszami 1 i 2 wartość s uległa zmianie (wskazuje on teraz na nowy obiekt). Klasa StringBuilder pozwala natomiast na przekształcenie ciągu znaków bez tworzenia drugiego obiektu. Jest to przykład podany powyżej:
- wiersz 8: 4 odwołania [sb0, sb1, sb2, sb3] do obiektów typu StringBuilder
- wiersz 10: zostały przekazane do metody ChangeStringBuilder w różnych trybach: sb0, sb1 z trybem domyślnym, sb2 ze słowem kluczowym ref, sb3 ze słowem kluczowym out.
- wiersze 15–22: metoda posiadająca parametry formalne [sbf0, sbf1, sbf2, sbf3]. Związki między parametrami formalnymi sbfi a rzeczywistymi sbi są następujące:
- sbf0 i sb0 to, w momencie uruchomienia metody, dwa odrębne odwołania wskazujące na ten sam obiekt (przekazywanie adresów przez wartość)
- to samo dotyczy sbf1 i sb1
- sbf2 i sb2 stanowią, w momencie uruchomienia metody, to samo odwołanie do tego samego obiektu (słowo kluczowe ref)
- sbf3 i sb3 są, po wykonaniu metody, tym samym odwołaniem do tego samego obiektu (słowo kluczowe out)
Uzyskane wyniki są następujące:
Wyjaśnienia:
- sb0 i sbf0 to dwa odrębne odwołania do tego samego obiektu. Obiekt ten został zmodyfikowany za pomocą sbf0 – wiersz 3. Modyfikację tę można sprawdzić za pomocą sb0 – wiersz 4.
- sb1 i sbf1 to dwa odrębne odwołania do tego samego obiektu. Wartość sbf1 została zmieniona w metodzie i obecnie wskazuje na nowy obiekt – wiersz 3. Nie ma to żadnego wpływu na wartość sb1, która nadal wskazuje na ten sam obiekt – wiersz 4.
- sb2 i sbf2 to ta sama referencja do tego samego obiektu. Wartość sbf2 zostaje zmieniona w metodzie i teraz wskazuje na nowy obiekt – wiersz 3. Ponieważ sbf2 i sb2 są tą samą jednostką, wartość sb2 również uległa zmianie, a sb2 wskazuje na ten sam obiekt co sbf2 – wiersze 3 i 4.
- Przed wywołaniem metody sb3 nie miało żadnej wartości. Po wywołaniu metody sb3 otrzymuje wartość sbf3. Mamy zatem dwa odwołania do tego samego obiektu – wiersze 3 i 4
4.1.12. Obiekty tymczasowe
W wyrażeniu można jawnie wywołać konstruktor obiektu: obiekt ten jest tworzony, ale nie mamy do niego dostępu (na przykład w celu jego modyfikacji). Ten obiekt tymczasowy jest tworzony na potrzeby obliczenia wyrażenia, a następnie porzucany. Zajmowana przez niego przestrzeń pamięci zostanie później automatycznie odzyskana przez program zwany „zbieraczem śmieci”, którego zadaniem jest odzyskiwanie przestrzeni pamięci zajmowanej przez obiekty, do których nie odwołują się już dane programu.
Rozważmy następujący nowy program testowy:
using System;
namespace Chap2 {
class Program {
static void Main() {
new Personne(new Personne("Jean", "Dupont", 30)).Identifie();
}
}
}
i zmodyfikujmy konstruktory klasy Personne tak, aby wyświetlały komunikat:
// konstruktory
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);
}
Otrzymujemy następujące wyniki:
pokazujące kolejną konstrukcję obu obiektów tymczasowych.
4.1.13. Metody odczytu i zapisu atrybutów prywatnych
Dodajemy do klasy Personne metody niezbędne do odczytu lub modyfikacji stanu atrybutów obiektów:
using System;
namespace Chap2 {
public class Personne {
// atrybuty
private string prenom;
private string nom;
private int age;
// konstruktory
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);
}
// metoda
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;
}
// metody dostępu
public String GetPrenom() {
return prenom;
}
public String GetNom() {
return nom;
}
public int GetAge() {
return age;
}
//modyfikatory
public void SetPrenom(String P) {
this.prenom = P;
}
public void SetNom(String N) {
this.nom = N;
}
public void SetAge(int age) {
this.age = age;
}
// metoda
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Testujemy nową klasę za pomocą następującego programu:
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() + ")");
}
}
}
i otrzymujemy następujące wyniki:
4.1.14. Właściwości
Istnieje jeszcze jeden sposób uzyskania dostępu do atrybutów klasy – poprzez utworzenie właściwości. Pozwalają one na manipulowanie atrybutami prywatnymi tak, jakby były one publiczne.
Rozważmy następującą klasę Personne, w której poprzednie metody dostępu i modyfikacji zostały zastąpione właściwościami do odczytu i zapisu:
using System;
namespace Chap2 {
public class Personne {
// atrybuty
private string prenom;
private string nom;
private int age;
// konstruktory
public Personne(String p, String n, int age) {
Initialise(p, n, age);
}
public Personne(Personne p) {
Initialise(p);
}
// metoda
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;
}
// właściwości
public string Prenom {
get { return prenom; }
set {
// czy imię jest prawidłowe?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//imię
public string Nom {
get { return nom; }
set {
// czy nazwisko jest prawidłowe?
if (value == null || value.Trim().Length == 0) {
throw new Exception("nom (" + value + ") invalide");
} else { nom = value; }
}//if
}//nazwisko
public int Age {
get { return age; }
set {
// czy wiek jest prawidłowy?
if (value >= 0) {
age = value;
} else
throw new Exception("âge (" + value + ") invalide");
}//if
}//wiek
// metoda
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Właściwość pozwala odczytać (get) lub ustawić (set) wartość atrybutu. Właściwość deklaruje się w następujący sposób:
gdzie Type musi być typem atrybutu zarządzanego przez tę właściwość. Może ona posiadać dwie metody o nazwach get i set. Metoda get zazwyczaj służy do zwracania wartości atrybutu, którym zarządza (może jednak zwracać coś innego – nic jej w tym nie przeszkadza). Metoda set przyjmuje parametr o nazwie value, który zazwyczaj przypisuje do zarządzanego przez siebie atrybutu. Może przy tej okazji sprawdzić poprawność otrzymanej wartości i ewentualnie zgłosić wyjątek, jeśli wartość okaże się nieprawidłowa. Tak właśnie dzieje się w tym przypadku.
W jaki sposób wywoływane są metody get i set? Rozważmy następujący program testowy:
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
}
}
}
W instrukcji
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
próbujemy uzyskać wartości właściwości Prenom, Nom i Age osoby p. Wywoływana jest wówczas metoda get tych właściwości, która zwraca wartość atrybutu, którym one zarządzają.
W instrukcji
chcemy ustawić wartość właściwości Age. Wywoływana jest wówczas metoda set tej właściwości. Otrzyma ona wartość 56 w parametrze value.
Właściwość P klasy C, która definiuje wyłącznie metodę get, jest określana jako tylko do odczytu. Jeśli c jest obiektem klasy C, operacja c.P=wartość zostanie odrzucona przez kompilator.
Wykonanie powyższego programu testowego daje następujące wyniki:
Właściwości pozwalają nam zatem na manipulowanie atrybutami prywatnymi tak, jakby były one publiczne. Inną cechą właściwości jest to, że można je stosować łącznie z konstruktorem zgodnie z następującą składnią:
Ta składnia jest równoważna poniższemu kodowi:
Kolejność właściwości nie ma znaczenia. Oto przykład.
Do klasy Personne dodano nowy konstruktor bez parametrów:
public Personne() {
}
Konstruktor nie inicjuje elementów obiektu. Nazywa się to konstruktorem domyślnym. Jest on używany, gdy klasa nie definiuje żadnego konstruktora.
Poniższy kod tworzy i inicjuje (wiersz 6) nowy obiekt klasy Personne przy użyciu składni przedstawionej wcześniej:
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);
}
}
}
W wierszu 6 powyżej użyto konstruktora bez parametrów Personne(). W tym konkretnym przypadku można było również zapisać
Personne p2 = new Personne() { Age = 7, Prenom = "Arthur", Nom = "Martin" };
, ale nawiasy konstruktoru bezparametrowego Personne() nie są obowiązkowe w tej składni.
Wyniki wykonania są następujące:
W wielu przypadkach metody get i set danej właściwości ograniczają się jedynie do odczytu i zapisu pola prywatnego bez dalszej obróbki. W takim scenariuszu można zatem użyć właściwości automatycznej zadeklarowanej w następujący sposób:
Pole prywatne powiązane z właściwością nie jest deklarowane. Jest ono automatycznie generowane przez kompilator. Dostęp do niego można uzyskać wyłącznie poprzez właściwość. Zatem zamiast pisać:
private string prenom;
...
// powiązana właściwość
public string Prenom {
get { return prenom; }
set {
// czy imię jest prawidłowe?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//imię
można zapisać:
bez deklarowania pola prywatnego prenom. Różnica między tymi dwiema właściwościami polega na tym, że pierwsza sprawdza poprawność imienia w polu set, podczas gdy druga nie przeprowadza żadnej weryfikacji.
Użycie właściwości automatycznej Prenom jest równoznaczne z zadeklarowaniem pola Prenom jako publicznego:
Można się zastanawiać, czy istnieje różnica między tymi dwoma deklaracjami. Deklarowanie public jako pola klasy jest odradzane. Narusza to koncepcję enkapsulacji stanu obiektu, który powinien być prywatny i udostępniany za pośrednictwem metod publicznych.
Jeśli właściwość automatyczna zostanie zadeklarowana jako virtuelle,, można ją następnie przedefiniować w klasie potomnej:
class Class1 {
public virtual string Prop { get; set; }
}
class Class2 : Class1 {
public override string Prop { get { return base.Prop; } set {... } }
}
W wierszu 2 powyżej klasa potomna Class2 może umieścić w klasie set, kod sprawdzający poprawność wartości przypisanej do automatycznej właściwości base.Prop klasy nadrzędnej Class1.
4.1.15. Metody i atrybuty klasy
Załóżmy, że chcemy policzyć liczbę obiektów Personne utworzonych w aplikacji. Można samodzielnie zarządzać licznikiem, ale istnieje ryzyko pominięcia obiektów tymczasowych, które powstają tu i ówdzie. Bezpieczniejszym rozwiązaniem wydaje się umieszczenie w konstruktorach klasy Personne instrukcji zwiększającej wartość licznika. Problem polega na przekazaniu odwołania do tego licznika, aby konstruktor mógł go zwiększyć: należy przekazać im nowy parametr. Można również umieścić licznik w definicji klasy. Ponieważ jest to atrybut samej klasy, a nie konkretnej instancji tej klasy, deklaruje się go inaczej, używając słowa kluczowego static:
private static long nbPersonnes;
Aby do niego odwołać się, piszemy Personne.nbPersonnes, aby pokazać, że jest to atrybut samej klasy Personne. W tym przypadku utworzyliśmy atrybut prywatny, do którego nie będziemy mieli bezpośredniego dostępu spoza klasy. Tworzymy zatem właściwość publiczną, aby zapewnić dostęp do atrybutu klasy nbPersonnes. Aby ustawić wartość nbPersonnes, metoda get tej właściwości nie wymaga konkretnego obiektu Personne: w rzeczywistości nbPersonnes jest atrybutem całej klasy. Potrzebna jest zatem właściwość o nazwie static:
public static long NbPersonnes {
get { return nbPersonnes; }
}
która z zewnątrz będzie wywoływana za pomocą składni Personne.NbPersonnes. Oto przykład.
Klasa Personne przyjmuje następujący kształt:
using System;
namespace Chap2 {
public class Personne {
// atrybuty klasy
private static long nbPersonnes;
public static long NbPersonnes {
get { return nbPersonnes; }
}
// atrybuty instancji
private string prenom;
private string nom;
private int age;
// konstruktory
public Personne(String p, String n, int age) {
Initialise(p, n, age);
nbPersonnes++;
}
public Personne(Personne p) {
Initialise(p);
nbPersonnes++;
}
...
}
W wierszach 20 i 24 konstruktory zwiększają wartość pola statycznego z wiersza 7.
W poniższym programie:
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);
}
}
}
uzyskuje się następujące wyniki:
4.1.16. Tablica osób
Obiekt jest danymi takimi jak inne, dlatego też wiele obiektów można zgromadzić w tablicy:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
// tabela osób
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);
// wyświetlanie
foreach (Personne ami in amis) {
ami.Identifie();
}
}
}
}
- wiersz 7: tworzy tablicę składającą się z 3 elementów typu Personne. Te 3 elementy są tutaj zainicjowane wartościami null, c.a.d, co oznacza, że nie odwołują się one do żadnego obiektu. Ponownie, w wyniku nieprecyzyjnego sformułowania, mówi się o tablicy obiektów, podczas gdy jest to jedynie tablica odwołań do obiektów. Utworzenie tablicy obiektów, która sama w sobie jest obiektem (obecność new), nie powoduje utworzenia żadnego obiektu typu jej elementów: należy to zrobić później.
- wiersze 8–10: utworzenie 3 obiektów typu Personne
- wiersze 12–14: wyświetlenie zawartości tablicy amis
Otrzymujemy następujące wyniki:
4.2. Dziedziczenie na przykładzie
4.2.1. Informacje ogólne
W tym miejscu poruszamy kwestię dziedziczenia. Celem dziedziczenia jest „dostosowanie” istniejącej klasy tak, aby spełniała nasze potrzeby. Załóżmy, że chcemy utworzyć klasę Enseignant: nauczyciel to szczególna osoba. Posiada on atrybuty, których nie ma żadna inna osoba: na przykład przedmiot, którego naucza. Ma jednak również atrybuty charakterystyczne dla każdej osoby: imię, nazwisko i wiek. Nauczyciel jest zatem w pełni częścią klasy Personne, ale posiada dodatkowe atrybuty. Zamiast tworzyć klasę Enseignant od podstaw, lepiej byłoby wykorzystać istniejące elementy klasy Personne i dostosować je do specyficznego charakteru nauczycieli. Umożliwia nam to koncepcja dziedziczenia.
Aby wyrazić, że klasa Enseignant dziedziczy właściwości klasy Personne, napiszemy:
Klasa Personne nazywana jest klasą nadrzędną (lub macierzystą), a klasa Enseignant – klasą pochodną (lub potomną). Obiekt Enseignant posiada wszystkie cechy obiektu Personne: ma te same atrybuty i te same metody. Atrybuty i metody klasy nadrzędnej nie są powtarzane w definicji klasy podrzędnej: wystarczy wskazać atrybuty i metody dodane przez klasę podrzędną:
Zakładamy, że klasa Personne jest zdefiniowana w następujący sposób:
using System;
namespace Chap2 {
public class Personne {
// atrybuty klasy
private static long nbPersonnes;
public static long NbPersonnes {
get { return nbPersonnes; }
}
// atrybuty instancji
private string prenom;
private string nom;
private int age;
// konstruktory
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)");
}
// właściwości
public string Prenom {
get { return prenom; }
set {
// czy imię jest prawidłowe?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//imię
public string Nom {
get { return nom; }
set {
// czy nazwisko jest prawidłowe?
if (value == null || value.Trim().Length == 0) {
throw new Exception("nom (" + value + ") invalide");
} else { nom = value; }
}//if
}//nazwisko
public int Age {
get { return age; }
set {
// wiek prawidłowy?
if (value >= 0) {
age = value;
} else
throw new Exception("âge (" + value + ") invalide");
}//if
}//wiek
// właściwość
public string Identite {
get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
}
}
}
Metoda Identifie została zastąpiona przez właściwość Identite, dostępną tylko do odczytu, która identyfikuje osobę. Tworzymy klasę Enseignant dziedziczącą po klasie Personne:
using System;
namespace Chap2 {
class Enseignant : Personne {
// atrybuty
private int section;
// konstruktor
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// sekcję zapisuje się za pomocą właściwości Section
Section = section;
// śledzenie
Console.WriteLine("Construction Enseignant(string, string, int, int)");
}//konstruktor
// właściwość Section
public int Section {
get { return section; }
set { section = value; }
}// Sekcja
}
}
Klasa Enseignant rozszerza metody i atrybuty klasy Personne:
- wiersz 4: klasa Enseignant wywodzi się z klasy Personne
- wiersz 6: atrybut section, który jest numerem sekcji, do której należy nauczyciel w gronie pedagogicznym (ogólnie rzecz biorąc, jedna sekcja na jeden przedmiot). Dostęp do tego atrybutu prywatnego można uzyskać poprzez właściwość publiczną Section z wierszy 18–21
- wiersz 9: nowy konstruktor umożliwiający zainicjowanie wszystkich atrybutów nauczyciela
4.2.2. Tworzenie obiektu Nauczyciel
Klasa potomna nie dziedziczy konstruktorów po swojej klasie nadrzędnej. Musi zatem zdefiniować własne konstruktory. Konstruktor klasy Enseignant wygląda następująco:
// konstruktor
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// zapisujemy sekcję
Section = section;
// śledzenie
Console.WriteLine("Construction enseignant(string, string, int, int)");
}//producent
Deklaracja
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
oznacza, że konstruktor przyjmuje cztery parametry: prenom, nom, age, section oraz przekazuje trzy parametry (prenom,nom,age) do swojej klasy bazowej, w tym przypadku klasy Personne. Wiadomo, że ta klasa posiada konstruktor Personne(string, string, int), który umożliwi utworzenie osoby na podstawie przekazanych parametrów (prenom,nom,age). Po zakończeniu tworzenia klasy bazowej tworzenie obiektu Enseignant jest kontynuowane poprzez wykonanie treści konstruktora:
Należy zauważyć, że po lewej stronie znaku = nie wykorzystano atrybutu obiektu o nazwie section, lecz powiązaną z nim właściwość Section. Dzięki temu konstruktor może skorzystać z ewentualnych kontroli poprawności, które ta metoda mogłaby przeprowadzić. Dzięki temu nie trzeba umieszczać tych kontroli w dwóch różnych miejscach: w konstruktorze i we właściwości.
Podsumowując, konstruktor klasy pochodnej:
- przekazuje do swojej klasy bazowej parametry, których ta potrzebuje do skonstruowania się
- wykorzystuje pozostałe parametry do zainicjowania swoich własnych atrybutów
Można by było zapisać to w następujący sposób:
// konstruktor
public Enseignant(string prenom, string nom, int age, int section){
this.prenom=prenom;
this.nom=nom;
this.age=age;
this.section=section;
}
To niemożliwe. Klasa Personne zadeklarowała jako prywatne (private) swoje trzy pola: prenom, nom i age. Tylko obiekty tej samej klasy mają bezpośredni dostęp do tych pól. Wszystkie pozostałe obiekty, w tym obiekty potomne, jak w tym przypadku, muszą korzystać z metod publicznych, aby uzyskać do nich dostęp. Sytuacja wyglądałaby inaczej, gdyby klasa Personne zadeklarowała te trzy pola jako chronione (protected): wówczas zezwalałaby klasom pochodnym na bezpośredni dostęp do tych trzech pól. W naszym przykładzie użycie konstruktora klasy nadrzędnej było zatem właściwym rozwiązaniem i jest to standardowa procedura: podczas tworzenia obiektu potomnego najpierw wywołuje się konstruktor obiektu nadrzędnego, a następnie uzupełnia się inicjalizacje właściwe dla obiektu potomnego (w naszym przykładzie section).
Spróbujmy napisać pierwszy program testowy [Program.cs]:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
}
}
}
Program ten ogranicza się do utworzenia obiektu Enseignant (new) i zidentyfikowania go. Klasa Enseignant nie posiada metody Identite, ale jej klasa nadrzędna posiada taką metodę, która ponadto jest publiczna: staje się ona poprzez dziedziczenie publiczną metodą klasy Enseignant.
Cały projekt wygląda następująco:
![]() |
Uzyskane wyniki są następujące:
Widać, że:
- obiekt Personne (wiersz 1) został utworzony przed obiektem Enseignant (wiersz 2)
- uzyskana tożsamość należy do obiektu Personne
4.2.3. Przedefiniowanie metody lub właściwości
W poprzednim przykładzie uzyskaliśmy tożsamość części Personne dotyczącej nauczyciela, ale brakuje pewnych informacji właściwych dla klasy Enseignant (sekcja). Musimy zatem zdefiniować właściwość umożliwiającą identyfikację nauczyciela:
using System;
namespace Chap2 {
class Enseignant : Personne {
// atrybuty
private int section;
// konstruktor
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// zapisujemy sekcję za pomocą właściwości Section
Section = section;
// śledzenie
Console.WriteLine("Construction Enseignant(string, string, int, int)");
}//konstruktor
// właściwość Section
public int Section {
get { return section; }
set { section = value; }
}// sekcja
// właściwość Identite
public new string Identite {
get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}
}
}
W wierszach 24–26 właściwość Identite klasy Enseignant opiera się na właściwości Identite swojej klasy nadrzędnej (base.Identite) (wiersz 25) w celu wyświetlenia swojej części „Personne”, a następnie uzupełnia ją polem section, które jest właściwe dla klasy Enseignant. Zwróćmy uwagę na deklarację właściwości Identite:
public new string Identite{
Niech E będzie obiektem enseignant. Obiekt ten zawiera w sobie obiekt Personne:
![]() |
Właściwość „Identite” jest zdefiniowana zarówno w klasie Enseignant, jak i w jej klasie nadrzędnej Personne. W klasie potomnej Enseignant właściwość Identite musi być poprzedzona słowem kluczowym „new”, aby wskazać, że redefiniujemy nową właściwość Identite dla klasy Enseignant.
public new string Identite{
Klasa Enseignant posiada teraz dwie właściwości Identite:
- jedną odziedziczoną po klasie nadrzędnej Personne
- własną
Jeśli E jest obiektem Enseignant, to E.Identite oznacza właściwość Identite klasy Enseignant. Mówi się, że właściwość Identite klasy potomnej redefiniuje lub ukrywa właściwość Identite klasy nadrzędnej. Ogólnie rzecz biorąc, jeśli O jest obiektem, a M metodą, to w celu wykonania metody O.M system szuka metody M w następującej kolejności:
- w klasie obiektu O
- w jej klasie nadrzędnej, jeśli taka istnieje
- w klasie nadrzędnej klasy nadrzędnej, jeśli taka istnieje
- itd…
Dziedziczenie pozwala zatem na przedefiniowanie w klasie potomnej metod/właściwości o tej samej nazwie, co w klasie nadrzędnej. To właśnie umożliwia dostosowanie klasy potomnej do jej własnych potrzeb. W połączeniu z polimorfizmem, który omówimy nieco później, przedefiniowanie metod/właściwości stanowi główną zaletę dziedziczenia.
Rozważmy ten sam program testowy, co poprzednio:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
}
}
}
Tym razem uzyskano następujące wyniki:
4.2.4. Polimorfizm
Rozważmy linię klas: C0 ← C1 ← C2 ← … ←Cn
gdzie Ci ← Cj oznacza, że klasa Cj wywodzi się z klasy Ci. Wynika z tego, że klasa Cj posiada wszystkie cechy klasy Ci oraz dodatkowe cechy. Niech Oi będą obiektami typu Ci. Dopuszczalne jest zapisanie:
W rzeczywistości, dzięki dziedziczeniu, klasa Cj posiada wszystkie cechy klasy Ci oraz dodatkowe. Zatem obiekt Oj typu Cj zawiera w sobie obiekt typu Ci. Operacja
sprawia, że Oi jest odwołaniem do obiektu typu Ci zawartego w obiekcie Oj.
Fakt, że zmienna Oi klasy Ci może w rzeczywistości odwoływać się nie tylko do obiektu klasy Ci, ale do dowolnego obiektu pochodnego od klasy Ci,, nazywany jest polimorfizmem: zdolność zmiennej do odwoływania się do różnych typów obiektów.
Weźmy przykład i rozważmy następującą funkcję niezależną od jakiejkolwiek klasy (static):
Równie dobrze można by zapisać
jak i
W tym ostatnim przypadku parametr formalny p typu Personne w metodzie statycznej Affiche otrzyma wartość typu Enseignant. Ponieważ typ Enseignant wywodzi się z typu Personne, jest to dopuszczalne.
4.2.5. Przedefiniowanie i polimorfizm
Uzupełnijmy naszą metodę Affiche:
public static void Affiche(Personne p) {
// wyświetla tożsamość p
Console.WriteLine(p.Identite);
}//wyświetla
Właściwość p.Identite zwraca ciąg znaków identyfikujący obiekt Personne p. Co się stanie w powyższym przykładzie, jeśli parametr przekazany do metody Affiche jest obiektem typu Enseignant:
Enseignant e = new Enseignant(...);
Affiche(e);
Przyjrzyjmy się następującemu przykładowi:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// nauczyciela
Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
Affiche(e);
// osoba
Personne p = new Personne("Jean", "Dupont", 30);
Affiche(p);
}
// wyświetla
public static void Affiche(Personne p) {
// wyświetla tożsamość p
Console.WriteLine(p.Identite);
}//wyświetla
}
}
Otrzymane wyniki są następujące:
Wykonanie pokazuje, że instrukcja p.Identite (wiersz 17) za każdym razem uruchamiała właściwość Identite obiektu Personne, najpierw (w wierszu 7) dla osoby zawartej w Enseignant e, a następnie (w wierszu 10) dla samej Personne p. Nie dostosowała się ona do obiektu faktycznie przekazanego jako parametr do Affiche. Wolelibyśmy uzyskać pełną tożsamość obiektu Enseignant e. W tym celu notacja p.Identite musiałaby odnosić się do właściwości Identite obiektu, na który faktycznie wskazuje p, a nie do właściwości Identite części „Personne” obiektu, na który faktycznie wskazuje p.
Wynik ten można uzyskać, deklarując Identite jako właściwość wirtualną (virtual) w klasie bazowej Personne:
public virtual string Identite {
get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age); }
}
Słowo kluczowe „virtual” sprawia, że Identite staje się właściwością wirtualną. Słowo to można również zastosować do metod. Klasy potomne, które redefiniują wirtualną właściwość lub metodę, muszą wówczas użyć słowa kluczowego override zamiast **new, aby określić swoją redefiniowaną właściwość/metodę. Tak więc w klasie Enseignant właściwość Identite jest redefiniowana w następujący sposób:
public override string Identite {
get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}
Powyższy program generuje wówczas następujące wyniki:
Tym razem, w wierszu 3, uzyskaliśmy pełną tożsamość nauczyciela. Zredefinujmy teraz metodę zamiast właściwości. Klasa object (alias C# dla System.Object) jest klasą „nadrzędną” wszystkich klas C#. Tak więc, gdy piszemy:
piszemy domyślnie:
Klasa System.Object definiuje metodę wirtualną ToString:
![]() |
Metoda ToString zwraca nazwę klasy, do której należy obiekt, jak pokazano w poniższym przykładzie:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// nauczyciel
Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString());
// osoba
Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString());
}
}
}
Wyniki są następujące:
Warto zauważyć, że chociaż nie zdefiniowaliśmy na nowo metody ToString w klasach Personne i Enseignant, można jednak zauważyć, że metoda ToString klasy Object była w stanie wyświetlić rzeczywistą nazwę klasy obiektu.
Zdefiniujmy ponownie metodę ToString w klasach Personne i Enseignant:
// metoda ToString
public override string ToString() {
return Identite;
}
Definicja jest taka sama w obu klasach. Rozważmy następujący program testowy:
using System;
namespace Chap2 {
class Program3 {
public static void Main() {
// nauczyciel
Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
Affiche(e);
// osoba
Personne p = new Personne("Jean", "Dupont", 30);
Affiche(p);
}
// plakat
public static void Affiche(Personne p) {
// plakat przedstawiający tożsamość p
Console.WriteLine(p);
}//Plakat
}
}
Zwróćmy uwagę na metodę Affiche, która przyjmuje jako parametr osobę p. W wierszu 15 metoda WriteLine klasy Console nie posiada żadnego wariantu przyjmującego parametr typu Personne. Wśród różnych wariantów metody Writeline istnieje jeden, który przyjmuje jako parametr typ Object. Kompilator wykorzysta tę metodę, WriteLine(Object o), ponieważ ta sygnatura oznacza, że parametr o może być typu Object lub pochodnego. Ponieważ Object jest klasą nadrzędną wszystkich klas, jako parametr do metody WriteLine można przekazać dowolny obiekt, a zatem również obiekt typu Personne lub Enseignant. Metoda WriteLine(Object o) zapisuje o.ToString() w strumieniu zapisu Out. Ponieważ metoda ToString jest wirtualna, jeśli obiekt o (typu Object lub pochodnego) nadpisał metodę ToString, to właśnie ta ostatnia zostanie użyta. Tak właśnie jest w przypadku klas Personne i Enseignant.
Wyniki wykonania to potwierdzają:
4.3. Zmiana znaczenia operatora dla klasy
4.3.1. Wprowadzenie
Rozważmy instrukcję
gdzie op1 i op2 to dwa operandy. Możliwe jest przedefiniowanie znaczenia operatora +. Jeśli operand op1 jest obiektem klasy C1, należy zdefiniować metodę statyczną w klasie C1 o następującej sygnaturze:
Gdy kompilator napotka instrukcję
, przekształca ją na C1.operator+(op1,op2). Typ zwracany przez metodę operator ma istotne znaczenie. Rozważmy bowiem operację op1+op2+op3. Kompilator przekształca ją na (op1+op2)+op3. Niech res12 będzie wynikiem operacji op1+op2. Następną wykonywaną operacją jest res12+op3. Jeśli res12 jest typu C1, zostanie ona również przekształcona na C1.operator+(res12,op3). Pozwala to na łańcuchowanie operacji.
Można również na nowo zdefiniować operatory jednoargumentowe, które mają tylko jeden argument. Jeśli więc op1 jest obiektem typu C1, operację op1++ można przedefiniować za pomocą metody statycznej klasy C1:
To, co zostało tutaj powiedziane, dotyczy większości operatorów, z kilkoma jednak wyjątkami:
- operatory == i != muszą być redefiniowane jednocześnie
- operatorów &&, ||, [], (), +=, -=, ... nie można redefiniować
4.3.2. Przykład
Tworzymy klasę ListeDePersonnes wywodzącą się z klasy ArrayList. Klasa ta implementuje dynamiczną listę i została omówiona w następnym rozdziale. Z tej klasy wykorzystujemy jedynie następujące elementy:
- metoda L.Add(Object o), umożliwiająca dodanie obiektu o do listy L. W tym przypadku obiektem o będzie obiekt Personne.
- właściwość L.Count, która podaje liczbę elementów listy L
- notacja L[i], która wskazuje element i listy L
Klasa ListeDePersonnes odziedziczy wszystkie atrybuty, metody i właściwości klasy ArrayList. Jej definicja jest następująca:
using System;
using System.Collections;
using System.Text;
namespace Chap2 {
class ListeDePersonnes : ArrayList{
// przeformułowanie operatora „+” w celu dodania osoby do listy
public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
// dodajemy osobę p do ListeDePersonnes l
l.Add(p);
// ustawia się listę ListeDePersonnes l
return l;
}// operator +
// ToString
public override string ToString() {
// zwraca (element1, element2, ..., elementn)
// nawias otwierający
StringBuilder listeToString = new StringBuilder("(");
// przeglądamy listę osób (this)
for (int i = 0; i < Count - 1; i++) {
listeToString.Append(this[i]).Append(",");
}//for
// ostatni element
if (Count != 0) {
listeToString.Append(this[Count-1]);
}
// nawias zamykający
listeToString.Append(")");
// należy zwrócić ciąg znaków
return listeToString.ToString();
}//ToString
}
}
- wiersz 6: klasa ListeDePersonnes wywodzi się z klasy ArrayList
- wiersze 8–13: definicja operatora + dla operacji l + p, gdzie l jest typu ListeDePersonnes, a p jest typu Personne lub pochodnego.
- wiersz 10: osoba p jest dodawana do listy l. Wykorzystywana jest tutaj metoda Add klasy nadrzędnej ArrayList.
- wiersz 12: zwracany jest odwołanie do listy l, aby umożliwić łańcuchowanie operatorów +, np. w wyrażeniu l + p1 + p2. Operacja l+p1+p2 zostanie zinterpretowana (zgodnie z priorytetem operatorów) jako (l+p1)+p2. Operacja l+p1 zwróci odwołanie l. Operacja (l+p1)+p2 staje się wówczas l+p2, co powoduje dodanie osoby p2 do listy osób l.
- wiersz 16: redefiniujemy metodę ToString w celu wyświetlenia listy osób w postaci (osoba1, osoba2, ...) gdzie personnei jest z kolei wynikiem działania metody ToString klasy Personne.
- wiersz 19: używamy obiektu typu StringBuilder. Klasa ta jest bardziej odpowiednia niż klasa string, gdy konieczne jest wykonanie wielu operacji na ciągu znaków, w tym przypadku dodawania. W rzeczywistości każda operacja na obiekcie typu string zwraca nowy obiekt typu string, podczas gdy te same operacje na obiekcie typu StringBuilder modyfikują obiekt, ale nie tworzą nowego. Używamy metody Append do łączenia ciągów znaków.
- wiersz 21: przeglądamy elementy listy osób. Lista ta jest tutaj oznaczona jako this. Jest to bieżący obiekt, na którym wykonywana jest metoda ToString. Właściwość Count jest właściwością klasy nadrzędnej ArrayList.
- wiersz 22: dostęp do elementu nr i bieżącej listy this można uzyskać za pomocą notacji this[i]. Również w tym przypadku jest to właściwość klasy ArrayList. Ponieważ chodzi o dodawanie ciągów znaków, zostanie użyta metoda this[i].ToString(). Ponieważ ta metoda jest wirtualna, zostanie użyta metoda ToString obiektu this, typu Personne lub pochodnego.
- wiersz 31: musimy zwrócić obiekt typu string (wiersz 16). Klasa StringBuilder posiada metodę ToString, która umożliwia przejście z typu StringBuilder do typu string.
Należy zauważyć, że klasa ListeDePersonnes nie posiada konstruktora. W tym przypadku wiadomo, że konstruktor
zostanie użyty. Konstruktor ten nie wykonuje żadnych czynności poza wywołaniem konstruktora bez parametrów swojej klasy nadrzędnej:
Klasa testowa mogłaby wyglądać następująco:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// lista osób
ListeDePersonnes l = new ListeDePersonnes();
// dodawanie osób
l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
// wyświetlanie
Console.WriteLine("l=" + l);
l = l + new Enseignant("camille", "germain",27,60);
Console.WriteLine("l=" + l);
}
}
}
- wiersz 7: utworzenie listy osób w
- wiersz 9: dodanie 2 osób za pomocą operatora +
- wiersz 12: dodanie nauczyciela
- wiersze 11 i 13: użycie przedefiniowanej metody ListeDePersonnes.ToString().
Wyniki:
4.4. Zdefiniowanie indeksatora dla klasy
W dalszej części nadal korzystamy z klasy ListeDePersonnes. Jeśli l jest obiektem klasy ListeDePersonnes, chcemy móc używać notacji l[i] do określenia osoby nr i z listy l zarówno podczas odczytu (Osoba p=l[i]), jak i podczas zapisu (l[i]=new Osoba(...)).
Aby móc zapisać l[i], gdzie l[i] oznacza obiekt Personne, musimy zdefiniować w klasie ListeDePersonnes następującą metodę this:
public Personne this[int i] {
get { ... }
set { ... }
}
Metodę this[int i] nazywamy indeksatorem, ponieważ nadaje ona znaczenie wyrażeniu obj[i], które przypomina notację tablic, podczas gdy obj nie jest tablicą, lecz obiektem. Metoda get, będąca metodą metody this obiektuobiektu obj jest wywoływana, gdy zapisuje się zmienna = obj[i], a metoda set – gdy zapisuje się *obj[i] = wartość*.
Klasa ListeDePersonnes wywodzi się z klasy ArrayList, która sama posiada indeksator:
Występuje konflikt między metodą this klasy ListeDePersonnes:
public Personne this[int i]
a metodą this klasy ArrayList
public object this[int i]
ponieważ mają tę samą nazwę i przyjmują ten sam typ parametru (int). Aby wskazać, że metoda this klasy ListeDePersonnes „maskuje” metodę o tej samej nazwie klasy ArrayList, należy dodać słowo kluczowe new do deklaracji indeksatora klasy ListeDePersonnes. Należy zatem zapisać:
public new Personne this[int i]{
get { ... }
set { ... }
}
Uzupełnijmy tę metodę. Metoda this.get jest wywoływana, gdy wpisujemy na przykład variable=l[i], gdzie l jest typu ListeDePersonnes. Należy wówczas zwrócić osobę o numerze i z listy l. Odbywa się to za pomocą notacji base[i], która zwraca obiekt o numerze i z klasy ArrayList, będącej klasą bazową dla klasy ListeDePersonnes. Ponieważ zwracany obiekt jest typu Object, konieczna jest konwersja typu do klasy Personne.
public new Personne this[int i]{
get { return (Personne) base[i]; }
set { ... }
}
Metoda set jest wywoływana, gdy zapisuje się l[i]=p, gdzie p jest Personne. Chodzi wówczas o przypisanie osoby o identyfikatorze p do elementu i na liście o identyfikatorze l.
public new Personne this[int i]{
get { ... }
set { base[i]=value; }
}
W tym przypadku osoba p, reprezentowana przez słowo kluczowe value, jest przypisana do elementu nr i klasy bazowej ArrayList.
Indeksator klasy ListeDePersonnes będzie zatem wyglądał następująco:
public new Personne this[int i]{
get { return (Personne) base[i]; }
set { base[i]=value; }
}
Teraz chcemy mieć możliwość zapisania również osoby p=l["nom"], c.a.d oraz indeksowania listy l nie za pomocą numeru elementu, ale nazwy osoby. W tym celu definiujemy nowy indeksator:
// wyszukiwanie według nazwiska
public int this[string nom] {
get {
// wyszukiwanie osoby
for (int i = 0; i < Count; i++) {
if (((Personne)base[i]).Nom == nom)
return i;
}//for
return -1;
}//pobierz
}
Pierwszy wiersz
public int this[string nom]
wskazuje, że indeksujemy klasę ListeDePersonnes według ciągu znaków nom, a wynikiem l[nom] jest liczba całkowita. Ta liczba całkowita będzie pozycją na liście osoby o nazwisku nom lub -1, jeśli tej osoby nie ma na liście. Definiujemy jedynie właściwość get,, uniemożliwiając w ten sposób zapis l["nom"]=wartość, który wymagałby zdefiniowania właściwości set. Słowo kluczowe new nie jest konieczne w deklaracji indeksatora, ponieważ klasa bazowa ArrayList nie definiuje indeksatora this[string].
W treści klasy get przeglądana jest lista osób w poszukiwaniu nazwiska przekazanego jako parametr. Jeśli zostanie ono znalezione na pozycji i, zwracana jest wartość i, w przeciwnym razie zwracana jest wartość -1.
Poprzedni program testowy uzupełnia się w następujący sposób:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// lista osób
ListeDePersonnes l = new ListeDePersonnes();
// dodawanie osób
l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
// wyświetlanie
Console.WriteLine("l=" + l);
l = l + new Enseignant("camille", "germain",27,60);
Console.WriteLine("l=" + l);
// zmiana elementu 1
l[1] = new Personne("franck", "gallon",5);
// wyświetlanie elementu 1
Console.WriteLine("l[1]=" + l[1]);
// wyświetlanie listy l
Console.WriteLine("l=" + l);
// wyszukiwanie osób
string[] noms = { "martin", "germain", "xx" };
for (int i = 0; i < noms.Length; i++) {
int inom = l[noms[i]];
if (inom != -1)
Console.WriteLine("Personne(" + noms[i] + ")=" + l[inom]);
else
Console.WriteLine("Personne(" + noms[i] + ") n'existe pas");
}//for
}
}
}
Jego wykonanie daje następujące wyniki:
4.5. Struktury
Struktura w języku C# jest analogiczna do struktury w języku C i bardzo zbliżona do pojęcia klasy. Struktura jest definiowana w następujący sposób:
Pomimo podobieństwa w deklaracji istnieją istotne różnice między klasą a strukturą. Na przykład w strukturach nie ma pojęcia dziedziczenia. Jeśli tworzymy klasę, która nie ma być klasą pochodną, jakie różnice między strukturą a klasą pomogą nam dokonać wyboru między nimi? Posłużmy się poniższym przykładem, aby to sprawdzić:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// struktura sp1
SPersonne sp1;
sp1.Nom = "paul";
sp1.Age = 10;
Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
// struktura sp2
SPersonne sp2 = sp1;
Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
// sp2 zostaje zmodyfikowana
sp2.Nom = "nicole";
sp2.Age = 30;
// weryfikacja sp1 i sp2
Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
// obiekt op1
CPersonne op1=new CPersonne();
op1.Nom = "paul";
op1.Age = 10;
Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
// obiekt op2
CPersonne op2=op1;
Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
// op2 został zmodyfikowany
op2.Nom = "nicole";
op2.Age = 30;
// weryfikacja op1 i op2
Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
}
}
// struktura SPersonne
struct SPersonne {
public string Nom;
public int Age;
}
// klasa CPersonne
class CPersonne {
public string Nom;
public int Age;
}
}
- wiersze 38–41: struktura z dwoma polami publicznymi: Nom, Age
- wiersze 44–47: klasa z dwoma polami publicznymi: Nom, Age
Po uruchomieniu tego programu otrzymujemy następujące wyniki:
Tam, gdzie wcześniej używaliśmy klasy Personne, teraz używamy struktury SPersonne:
struct SPersonne {
public string Nom;
public int Age;
}
Struktura ta nie posiada tutaj konstruktora. Mogłaby go jednak posiadać, co pokażemy w dalszej części. Domyślnie zawsze posiada konstruktor bez parametrów, w tym przypadku SPersonne().
- wiersz 7 kodu: deklaracja
SPersonne sp1;
jest równoważna instrukcji:
SPersonne sp1=new Spersonne();
Tworzona jest struktura (Nazwa,Wiek), a wartością sp1 jest sama ta struktura. W przypadku klasy utworzenie obiektu (Nazwa,Wiek) musi nastąpić jawnie za pomocą operatora new (wiersz 22):
CPersonne op1=new CPersonne();
Poprzednia instrukcja tworzy obiekt CPersonne (w przybliżeniu odpowiadający naszej strukturze), a wartość p1 jest wówczas adresem (odwołaniem) tego obiektu.
Podsumujmy
- w przypadku struktury wartość sp1 to sama struktura
- w przypadku klasy wartość op1 stanowi adres utworzonego obiektu
![]() |
Gdy w programie zapisujemy w wierszu 12:
SPersonne sp2 = sp1;
tworzona jest nowa struktura sp2(Nazwa,Wiek) i inicjowana wartością sp1,, czyli samą strukturą.
![]() |
Struktura sp1 jest duplikowana w sp2 jako [1]. Jest to skopiowanie wartości. Rozważmy teraz instrukcję w wierszu 27:
CPersonne op2=op1;
W przypadku klas wartość op1 jest kopiowana do op2, ale ponieważ ta wartość jest w rzeczywistości adresem obiektu, obiekt ten nie jest powielany: [2].
W przypadku struktury [1] zmiana wartości sp2 nie powoduje zmiany wartości sp1, co pokazuje program. W przypadku obiektu [2], jeśli zmodyfikuje się obiekt, na który wskazuje op2, to obiekt, na który wskazuje op1, również ulega zmianie, ponieważ jest to ten sam obiekt. Potwierdzają to również wyniki programu.
Z powyższych wyjaśnień wynika zatem, że:
- wartością zmiennej typu struktura jest sama struktura
- wartością zmiennej typu obiekt jest adres obiektu, na który wskazuje
Po zrozumieniu tej fundamentalnej różnicy struktura okazuje się bardzo zbliżona do klasy, co ilustruje poniższy przykład:
using System;
namespace Chap2 {
// struktura SPersonne
struct SPersonne {
// atrybuty prywatne
private string nom;
private int age;
// właściwości
public string Nom {
get { return nom; }
set { nom = value; }
}//nazwa
public int Age {
get { return age; }
set { age = value; }
}//wiek
// Producent
public SPersonne(string nom, int age) {
this.nom = nom;
this.age = age;
}//konstruktor
// ToString
public override string ToString() {
return "SPersonne(" + Nom + "," + Age + ")";
}//ToString
}//struktura
}//przestrzeń nazw
- wiersze 8–9: dwa pola prywatne
- wiersze 12–20: powiązane właściwości publiczne
- wiersze 23–26: definiujemy konstruktor. Należy zauważyć, że konstruktor bez parametrów SPersonne() jest zawsze obecny i nie trzeba go deklarować. Jego deklaracja zostanie odrzucona przez kompilator. W konstruktorze w wierszach 23–26 można ulec pokusie, by zainicjować pola prywatne nom, age za pomocą ich właściwości publicznych Nom, Age. Jest to odrzucane przez kompilator. Metody struktury nie mogą być używane podczas jej konstruowania.
- wiersze 29–31: redefinicja metody ToString.
Program testowy mógłby wyglądać następująco:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// osoba p1
SPersonne p1=new SPersonne();
p1.Nom="paul";
p1.Age= 10;
Console.WriteLine("p1={0}",p1);
// osoba p2
SPersonne p2 = p1;
Console.WriteLine("p2=" + p2);
// p2 zostaje zmodyfikowana
p2.Nom = "nicole";
p2.Age = 30;
// weryfikacja p1 i p2
Console.WriteLine("p1=" + p1);
Console.WriteLine("p2=" + p2);
// osoba p3
SPersonne p3 = new SPersonne("amandin", 18);
Console.WriteLine("p3=" + p3);
// osoba p4
SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
Console.WriteLine("p4=" + p4);
}
}
}
- wiersz 7: konieczne jest jawne użycie konstruktora bez parametrów, ponieważ w strukturze istnieje inny konstruktor. Gdyby struktura nie miała żadnego konstruktora, instrukcja
SPersonne p1;
wystarczyłaby do utworzenia pustej struktury.
- wiersze 8–9: struktura jest inicjowana za pomocą swoich właściwości publicznych
- wiersz 10: metoda p1.ToString zostanie wykorzystana w metodzie WriteLine.
- wiersz 21: utworzenie struktury za pomocą konstruktora SPersonne(string, int)
- wiersz 24: utworzenie struktury za pomocą konstruktora bez parametrów SPersonne() wraz z inicjalizacją pól prywatnych w nawiasach klamrowych za pomocą ich właściwości publicznych.
Otrzymujemy następujące wyniki wykonania:
Jedyną zauważalną różnicą między strukturą a klasą jest to, że w przypadku klasy obiekty p1 i p2 wskazywałyby na ten sam obiekt pod koniec programu.
4.6. Interfejsy
Interfejs to zbiór prototypów metod lub właściwości, które tworzą umowę. Klasa, która decyduje się na implementację interfejsu, zobowiązuje się do zapewnienia implementacji wszystkich metod zdefiniowanych w interfejsie. Kompilator weryfikuje tę implementację.
Oto na przykład definicja interfejsu System.Collections.IEnumerator:
public interface System.Collections.IEnumerator
{
// Właściwości
Object Current { get; }
// Metody
bool MoveNext();
void Reset();
}
Właściwości i metody interfejsu są zdefiniowane wyłącznie poprzez ich sygnatury. Nie są one zaimplementowane (nie mają kodu). To klasy implementujące interfejs nadają kod metodom i właściwościom interfejsu.
- wiersz 1: klasa C implementuje interfejs IEnumerator. Należy zauważyć, że znak „:” używany do implementacji interfejsu jest taki sam jak ten używany do dziedziczenia klasy.
- wiersze 3–5: implementacja metod i właściwości interfejsu IEnumerator.
Rozważmy następujący interfejs:
namespace Chap2 {
public interface IStats {
double Moyenne { get; }
double EcartType();
}
}
Interfejs IStats zawiera:
- właściwość tylko do odczytu Moyenne: służącą do obliczania średniej z szeregu wartości
- metodę EcartType: służącą do obliczania odchylenia standardowego
Należy zauważyć, że nigdzie nie jest sprecyzowane, o jaki ciąg wartości chodzi. Może to być średnia ocen w klasie, miesięczna średnia sprzedaży konkretnego produktu, średnia temperatura w danym miejscu itp. Na tym polega zasada działania interfejsów: zakłada się istnienie metod w obiekcie, ale nie konkretnych danych.
Pierwszą klasą implementującą interfejs IStats mogłaby być klasa służąca do przechowywania ocen uczniów z danej klasy z określonego przedmiotu. Uczeń byłby charakteryzowany przez następującą strukturę Elève:
public struct Elève {
public string Nom { get; set; }
public string Prénom { get; set; }
}//Uczeń
Uczeń byłby identyfikowany na podstawie swojego nazwiska i imienia. W wierszach 2–3 znajdują się automatyczne właściwości dla tych dwóch atrybutów.
Ocena byłaby opisana następującą strukturą: Note:
public struct Note {
public Elève Elève { get; set; }
public double Valeur { get; set; }
}//Ocena
Ocena byłaby identyfikowana przez ucznia, który ją otrzymał, oraz samą ocenę. W wierszach 2–3 znajdują się automatyczne właściwości dla tych dwóch atrybutów.
Oceny wszystkich uczniów z danego przedmiotu są zebrane w następującej klasie TableauDeNotes:
using System;
using System.Text;
namespace Chap2 {
public class TableauDeNotes : IStats {
// atrybuty
public string Matière { get; set; }
public Note[] Notes { get; set; }
public double Moyenne { get; private set; }
private double ecartType;
// konstruktor
public TableauDeNotes(string matière, Note[] notes) {
// zapisywanie za pomocą właściwości publicznych
Matière = matière;
Notes = notes;
// obliczanie średniej ocen
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;
// odchylenie standardowe
double carrés = 0;
for (int i = 0; i < Notes.Length; i++) {
carrés += Math.Pow((Notes[i].Valeur - Moyenne), 2);
}//for
if (Notes.Length != 0)
ecartType = Math.Sqrt(carrés / Notes.Length);
else ecartType = -1;
}//konstruktor
public double EcartType() {
return ecartType;
}
// ToString
public override string ToString() {
StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
int i;
// łączymy wszystkie oceny
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("],");
};
//ostatnia notatka
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(")");
// koniec
return valeur.ToString();
}//ToString
}//klasa
}
- wiersz 6: klasa TableauDeNotes implementuje interfejs IStats. Musi zatem implementować właściwość Moyenne oraz metodę EcartType. Są one zaimplementowane w wierszach 10 (Moyenne) oraz 35–37 (EcartType)
- wiersze 8–10: trzy właściwości automatyczne
- wiersz 8: przedmiot, którego oceny przechowuje obiekt
- wiersz 9: tablica ocen uczniów (Uczeń, Ocena)
- wiersz 10: średnia ocen – właściwość implementująca właściwość Moyenne interfejsu IStats.
- wiersz 11: pole przechowujące odchylenie standardowe ocen - powiązana metoda get (EcartType) z wierszy 35–37 implementuje metodę EcartType interfejsu IStats.
- wiersz 9: oceny są zapisywane w tablicy. Tablica ta jest przekazywana podczas tworzenia klasy TableauDeNotes do konstruktora w wierszach 14–33.
- wiersze 14–33: konstruktor. Zakłada się tutaj, że oceny przekazane do konstruktora nie ulegną już później zmianie. Dlatego też wykorzystuje się konstruktor do natychmiastowego obliczenia średniej i odchylenia standardowego tych ocen oraz zapisania ich w polach w wierszach 10–11. Średnia jest zapisywana w polu prywatnym powiązanym z właściwością automatyczną Moyenne w wierszu 10, a odchylenie standardowe – w polu prywatnym w wierszu 11.
- wiersz 10: metoda get właściwości automatycznej Moyenne zwróci przypisane pole prywatne.
- wiersze 35–37: metoda EcartType zwraca wartość pola prywatnego z wiersza 11.
W tym kodzie występuje kilka subtelności:
- wiersz 23: metoda set właściwości Moyenne służy do przypisania wartości. Metoda ta została zadeklarowana jako prywatna w wierszu 10, aby przypisanie wartości do właściwości Moyenne było możliwe wyłącznie wewnątrz klasy.
- wiersze 40–54: wykorzystują obiekt StringBuilder do skonstruowania ciągu znaków reprezentującego obiekt TableauDeNotes w celu poprawy wydajności. Należy zauważyć, że znacznie pogarsza to czytelność kodu. To druga strona medalu.
W poprzedniej klasie oceny były zapisywane w tablicy. Nie było możliwości dodania nowej oceny po utworzeniu obiektu TableauDeNotes. Proponujemy teraz drugą implementację interfejsu IStats, nazwaną ListeDeNotes, w której tym razem oceny byłyby zapisywane na liście, z możliwością dodawania ocen po początkowym utworzeniu obiektu ListeDeNotes.
Kod klasy ListeDeNotes wygląda następująco:
using System;
using System.Text;
using System.Collections.Generic;
namespace Chap2 {
public class ListeDeNotes : IStats {
// atrybuty
public string Matière { get; set; }
public List<Note> Notes { get; set; }
public double moyenne = -1;
public double ecartType = -1;
// konstruktor
public ListeDeNotes(string matière, List<Note> notes) {
// przechowywanie za pomocą właściwości publicznych
Matière = matière;
Notes = notes;
}//konstruktor
// dodanie notatki
public void Ajouter(Note note) {
// dodanie notatki
Notes.Add(note);
// zerowanie średniej i odchylenia standardowego
moyenne = -1;
ecartType = -1;
}
// ToString
public override string ToString() {
StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
int i;
// łączymy wszystkie notatki
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("],");
};
//ostatnia ocena
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(")");
// koniec
return valeur.ToString();
}//ToString
// średnia ocen
public double Moyenne {
get {
if (moyenne != -1) return moyenne;
// obliczenie średniej ocen
double somme = 0;
for (int i = 0; i < Notes.Count; i++) {
somme += Notes[i].Valeur;
}
// podajemy średnią
if (Notes.Count != 0) moyenne = somme / Notes.Count;
return moyenne;
}
}
public double EcartType() {
// odchylenie standardowe
if (ecartType != -1) return ecartType;
// średnia
double moyenne = Moyenne;
double carrés = 0;
for (int i = 0; i < Notes.Count; i++) {
carrés += Math.Pow((Notes[i].Valeur - moyenne), 2);
}//for
// zwracamy odchylenie standardowe
if (Notes.Count != 0)
ecartType = Math.Sqrt(carrés / Notes.Count);
return ecartType;
}
}//klasa
}
- wiersz 7: klasa ListeDeNotes implementuje interfejs IStats
- wiersz 10: notatki są teraz umieszczane na liście zamiast w tablicy
- wiersz 11: automatyczna właściwość Moyenne klasy TableauDeNotes została tutaj zastąpiona przez pole prywatne moyenne, wiersz 11, powiązane z publiczną właściwością tylko do odczytu Moyenne z wierszy 48–60
- wiersze 22–28: można teraz dodawać notatki do tych już zapisanych, czego wcześniej nie można było zrobić.
- wiersze 15–19: w związku z tym średnia i odchylenie standardowe nie są już obliczane w konstruktorze, lecz w samych metodach interfejsu: Moyenne (wiersze 48–60) i EcartType (wiersze 62–76). Ponowne obliczenie jest jednak uruchamiane tylko wtedy, gdy średnia i odchylenie standardowe różnią się od -1 (wiersze 50 i 64).
Klasa testowa mogłaby wyglądać następująco:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// niektórzy uczniowie i oceny z języka angielskiego
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 } };
//, które zapisujemy w obiekcie TableauDeNotes
TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
// wyświetlanie średniej i odchylenia standardowego
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
// umieszczamy uczniów i przedmiot w obiekcie ListeDeNotes
ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
// wyświetlanie średniej i odchylenia standardowego
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
// dodajemy ocenę
français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
// wyświetlanie średniej i odchylenia standardowego
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
}
}
}
- wiersz 8: utworzenie tablicy uczniów przy użyciu konstruktora bez parametrów i inicjalizacja za pomocą właściwości publicznych
- wiersz 9: utworzenie tablicy ocen przy użyciu tej samej techniki
- wiersz 11: obiekt TableauDeNotes, dla którego obliczamy średnią i odchylenie standardowe wiersz 13
- wiersz 15: obiekt ListeDeNotes, dla którego w wierszu 17 obliczana jest średnia i odchylenie standardowe. Klasa List<Note> posiada konstruktor przyjmujący obiekt implementujący interfejs IEnumerable<Note>. Tablica notes1 implementuje ten interfejs i może być wykorzystana do utworzenia obiektu List<Note>.
- wiersz 19: dodanie nowej notatki
- wiersz 21: ponowne obliczenie średniej i odchylenia standardowego
Wyniki wykonania są następujące:
W poprzednim przykładzie dwie klasy implementują interfejs IStats. Jednak przykład ten nie pokazuje zalet interfejsu IStats. Przepiszmy program testowy w następujący sposób:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// niektórzy uczniowie i oceny z języka angielskiego
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 } };
// które zapisujemy w obiekcie TableauDeNotes
TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
// wyświetlanie średniej i odchylenia standardowego
AfficheStats(anglais);
// umieszczamy uczniów i przedmiot w obiekcie ListeDeNotes
ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
// wyświetlanie średniej i odchylenia standardowego
AfficheStats(français);
// dodajemy ocenę
français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
// wyświetlanie średniej i odchylenia standardowego
AfficheStats(français);
}
// wyświetlanie średniej i odchylenia standardowego typu IStats
static void AfficheStats(IStats valeurs) {
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs);
}
}
}
- wiersze 25–27: metoda statyczna AfficheStats przyjmuje jako parametr typ IStats, a więc typ interfejsu. Oznacza to, że faktycznym parametrem może być dowolny obiekt implementujący interfejs IStats. Kiedy używamy danych o typie interfejsu, oznacza to, że będziemy korzystać wyłącznie z metod interfejsu zaimplementowanych przez te dane. Resztę pomijamy. Mamy tu do czynienia z właściwością zbliżoną do polimorfizmu, który omówiliśmy w przypadku klas. Jeśli zbiór klas Ci, które nie są ze sobą powiązane poprzez dziedziczenie (a zatem nie można wykorzystać polimorfizmu dziedziczenia), posiada zbiór metod o tej samej sygnaturze, interesujące może być zgrupowanie tych metod w interfejsie I, który implementowałyby wszystkie odnośne klasy. Instancje tych klas Ci mogą być wówczas wykorzystywane jako parametry rzeczywiste funkcji przyjmujących parametr formalny typu I, c.a.d – funkcji korzystających wyłącznie z metod obiektów Ci zdefiniowanych w interfejsie I, a nie z atrybutów i metod właściwych dla poszczególnych klas Ci.
- wiersz 13: metoda AfficheStats jest wywoływana z typem TableauDeNotes, który implementuje interfejs IStats
- wiersz 17: to samo z typem ListeDeNotes
Wyniki wykonania są identyczne jak w poprzednim przykładzie.
Zmienna może być typu interfejsu. Można zatem zapisać:
Deklaracja w wierszu 1 wskazuje, że stats1 jest instancją klasy implementującej interfejs IStats. Deklaracja ta oznacza, że kompilator zezwoli w klasie stats1 wyłącznie na dostęp do metod interfejsu: właściwości Moyenne oraz metody EcartType.
Na koniec należy zauważyć, że interfejsy mogą być implementowane wielokrotnie, np. c.a.d. Można to zapisać w następujący sposób:
gdzie Ij to interfejsy.
4.7. Klasy abstrakcyjne
Klasa abstrakcyjna to klasa, której nie można instancjonować. Należy utworzyć klasy pochodne, które będą mogły być instancjonowane.
Klasy abstrakcyjne można wykorzystać do wyodrębnienia kodu z linii klas. Rozważmy następujący przypadek:
using System;
namespace Chap2 {
abstract class Utilisateur {
// pola
private string login;
private string motDePasse;
private string role;
// producent
public Utilisateur(string login, string motDePasse) {
// rejestruje się informacje
this.login = login;
this.motDePasse = motDePasse;
// identyfikacja użytkownika
role=identifie();
// zidentyfikowano?
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);
}
// identyfikuje
abstract public string identifie();
}
}
- wiersze 11–21: konstruktor klasy Utilisateur. Klasa ta przechowuje informacje o użytkowniku aplikacji internetowej. Aplikacja ta obsługuje różne typy użytkowników uwierzytelnianych za pomocą loginu i hasła (wiersze 6–7). Te dwie informacje są weryfikowane w usłudze LDAP dla niektórych użytkowników, w usłudze SGBD dla innych itd.
- wiersze 13–14: informacje uwierzytelniające są zapisywane
- wiersz 16: są one weryfikowane przez metodę identyfikującą. Ponieważ metoda identyfikacji nie jest znana, w wierszu 29 jest ona zadeklarowana jako abstrakcyjna za pomocą słowa kluczowego abstract. Metoda identifie zwraca ciąg znaków określający rolę użytkownika (w skrócie to, do czego ma on prawo). Jeśli ten ciąg znaków jest wskaźnikiem null, w wierszu 19 zgłaszany jest wyjątek.
- linia 4: ponieważ posiada metodę abstrakcyjną, sama klasa jest zadeklarowana jako abstrakcyjna za pomocą słowa kluczowego `abstract`.
- wiersz 29: metoda abstrakcyjna identyfikuje nie ma definicji. To klasy pochodne nadadzą jej definicję.
- wiersze 24–26: metoda ToString, która identyfikuje instancję klasy.
Zakładamy tutaj, że programista chce mieć kontrolę nad tworzeniem instancji klasy Utilisateur i klas pochodnych, być może dlatego, że chce mieć pewność, iż zostanie zgłoszony wyjątek określonego typu, jeśli użytkownik nie zostanie rozpoznany (wiersz 19). Klasy pochodne będą mogły korzystać z tego konstruktora. W tym celu będą musiały udostępnić metodę identyfikuje.
Klasa ExceptionUtilisateurInconnu ma następującą postać:
using System;
namespace Chap2 {
class ExceptionUtilisateurInconnu : Exception {
public ExceptionUtilisateurInconnu(string message) : base(message){
}
}
}
- wiersz 3: pochodzi od klasy Exception
- wiersze 4–6: posiada tylko jeden konstruktor, który przyjmuje jako parametr komunikat o błędzie. Komunikat ten jest przekazywany do klasy nadrzędnej (wiersz 5), która posiada ten sam konstruktor.
Teraz wyprowadzamy klasę Utilisateur z klasy potomnej Administrateur:
namespace Chap2 {
class Administrateur : Utilisateur {
// producent
public Administrateur(string login, string motDePasse)
: base(login, motDePasse) {
}
// identyfikuje
public override string identifie() {
// identyfikacja LDAP
// ...
return "admin";
}
}
}
- wiersze 4–6: konstruktor po prostu przekazuje do swojej klasy nadrzędnej otrzymane parametry
- wiersze 9–12: metoda identyfikuje klasy Administrateur. Załóżmy, że administrator jest identyfikowany przez system LDAP. Ta metoda nadpisuje metodę identyfikującą swojej klasy nadrzędnej. Ponieważ nadpisuje metodę abstrakcyjną, nie ma potrzeby stosowania słowa kluczowego override.
Teraz wyprowadzamy klasę Utilisateur z klasy potomnej Observateur:
namespace Chap2 {
class Observateur : Utilisateur{
// producent
public Observateur(string login, string motDePasse)
: base(login, motDePasse) {
}
//identyfikuje
public override string identifie() {
// identyfikacja SGBD
// ...
return "observateur";
}
}
}
- wiersze 4–6: konstruktor po prostu przekazuje do klasy nadrzędnej otrzymane parametry
- wiersze 9–13: metoda identyfikująca klasy Observateur. Zakłada się, że obserwator jest identyfikowany poprzez sprawdzenie jego danych identyfikacyjnych w bazie danych.
Ostatecznie obiekty Administrateur i Observateur są instancjonowane przez ten sam konstruktor, należący do klasy nadrzędnej Utilisateur.. Konstruktor ten wykorzysta metodę identyfikującą, którą udostępniają te klasy.
Trzecia klasa Inconnu również wywodzi się z klasy Utilisateur:
namespace Chap2 {
class Inconnu : Utilisateur{
// producent
public Inconnu(string login, string motDePasse)
: base(login, motDePasse) {
}
//identyfikuje
public override string identifie() {
// nieznany użytkownik
// ...
return null;
}
}
}
- wiersz 13: metoda identifie zwraca wskaźnik null, aby wskazać, że użytkownik nie został rozpoznany.
Program testowy mógłby wyglądać następująco:
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);
}
}
}
}
Należy zauważyć, że w wierszach 6, 7 i 9 metoda [Utilisateur].ToString() będzie wykorzystywana przez metodę WriteLine.
Wyniki wykonania są następujące:
4.8. Klasy, interfejsy, metody generyczne
Załóżmy, że chcemy napisać metodę zamieniającą miejscami dwie liczby całkowite. Metoda ta mogłaby wyglądać następująco:
public static void Echanger1(ref int value1, ref int value2){
// zamieniamy wartości referencyjne value1 i value2
int temp = value2;
value2 = value1;
value1 = temp;
}
Gdybyśmy teraz chcieli zamienić miejscami dwa odwołania do obiektów Personne, napisalibyśmy:
public static void Echanger2(ref Personne value1, ref Personne value2){
// zamieniamy odwołania value1 i value2
Personne temp = value2;
value2 = value1;
value1 = temp;
}
To, co odróżnia te dwie metody, to typ T parametrów: int w Echanger1, Personne w Echanger2. Klasy i interfejsy generyczne zaspokajają potrzebę stosowania metod, które różnią się jedynie typem niektórych swoich parametrów.
W przypadku klasy generycznej metoda Echanger mogłaby zostać przepisana w następujący sposób:
namespace Chap2 {
class Generic1<T> {
public static void Echanger(ref T value1, ref T value2){
// zamieniono odwołania value1 i value2
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- wiersz 2: klasa Generic1 jest parametryzowana przez typ oznaczony jako T. Można nadać mu dowolną nazwę. Typ T jest następnie ponownie wykorzystywany w klasie w wierszach 3 i 5. Mówi się, że klasa Generic1 jest klasą generyczną.
- wiersz 3: definiuje dwa odwołania do typu T, które mają zostać zamienione
- wiersz 5: zmienna tymczasowa temp ma typ T.
Program testujący tę klasę mógłby wyglądać następująco:
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);
// ciąg znaków
string s1 = "s1", s2 = "s2";
Generic1<string>.Echanger(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Osoba
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);
}
}
}
- wiersz 8: w przypadku korzystania z klasy generycznej parametryzowanej typami T1, T2, ... typy te muszą zostać „instancjonowane”. W wierszu 8 wykorzystuje się metodę statyczną Echanger typu Generic1<int>, aby wskazać, że referencje przekazane do metody Echanger są typu int.
- Wiersz 12: użyto metody statycznej Echanger typu Generic1<string>, aby wskazać, że referencje przekazywane do metody Echanger są typu string.
- wiersz 16: używa się metody statycznej Echanger typu Generic1<Personne>, aby wskazać, że referencje przekazywane do metody Echanger są typu Personne.
Wyniki wykonania są następujące:
Metoda Echanger mogłaby również zostać zapisana w następujący sposób:
namespace Chap2 {
class Generic2 {
public static void Echanger<T>(ref T value1, ref T value2){
// zamieniamy odwołania value1 i value2
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- wiersz 2: klasa Generic2 nie jest już generyczna
- wiersz 3: metoda statyczna Echanger jest generyczna
Program testowy wygląda wówczas następująco:
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);
// ciąg znaków
string s1 = "s1", s2 = "s2";
Generic2.Echanger<string>(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Osoba
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);
}
}
}
- wiersze 8, 12 i 16: wywołuje się metodę Echanger, określając w <> typy parametrów. W rzeczywistości kompilator jest w stanie wywnioskować na podstawie typu faktycznych parametrów, który wariant metody Echanger należy zastosować. Dlatego następujący zapis jest poprawny:
Generic2.Echanger(ref i1, ref i2);
...
Generic2.Echanger(ref s1, ref s2);
...
Generic2.Echanger(ref p1, ref p2);
Wiersze 1, 3 i 5: wywoływany wariant metody Echanger nie jest już określony. Kompilator jest w stanie wywnioskować go na podstawie charakteru faktycznie użytych parametrów.
Można nałożyć ograniczenia na parametry generyczne:

Rozważmy następującą nową metodę generyczną Echanger:
namespace Chap2 {
class Generic3 {
public static void Echanger<T>(ref T value1, ref T value2) where T : class {
// zamieniamy odwołania value1 i value2
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- wiersz 3: wymagane jest, aby typ T był referencją (klasa, interfejs)
Rozważmy następujący program testowy:
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);
// ciąg znaków
string s1 = "s1", s2 = "s2";
Generic3.Echanger(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Osoba
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);
}
}
}
Kompilator zgłasza błąd w wierszu 8, ponieważ typ int nie jest klasą ani interfejsem, lecz strukturą:

Rozważmy następującą nową metodę generyczną Echanger:
namespace Chap2 {
class Generic4 {
public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
// pobieramy wartości obu elementów
int value1 = element1.Value();
int value2 = element2.Value();
// jeśli pierwszy element > drugi element, zamieniamy elementy miejscami
if (value1 > value2) {
T temp = element2;
element2 = element1;
element1 = temp;
}
}
}
}
- wiersz 3: typ T musi implementować interfejs Interface1. Interfejs ten posiada metodę Value, wykorzystaną w wierszach 5 i 6, która zwraca wartość obiektu typu T.
- wiersze 8–12: obie referencje element1 i element2 są wymieniane tylko wtedy, gdy wartość element1 jest większa od wartości element2.
Interfejs Interface1 ma następujący wygląd:
namespace Chap2 {
interface Interface1 {
int Value();
}
}
Jest on zaimplementowany przez następującą klasę Class1:
using System;
using System.Threading;
namespace Chap2 {
class Class1 : Interface1 {
// wartość obiektu
private int value;
// konstruktor
public Class1() {
// oczekiwanie 1 ms
Thread.Sleep(1);
// wartość losowa z przedziału od 0 do 99
value = new Random(DateTime.Now.Millisecond).Next(100);
}
// metoda dostępu do pola prywatnego „value”
public int Value() {
return value;
}
// stan instancji
public override string ToString() {
return value.ToString();
}
}
}
- wiersz 5: Class1 implementuje interfejs Interface1
- wiersz 7: wartość instancji klasy Class1
- wiersze 10–14: pole value jest inicjowane wartością losową z przedziału od 0 do 99
- wiersze 18–20: metoda Value interfejsu Interface1
- wiersze 23–25: metoda ToString klasy
Interfejs Interface1 jest również zaimplementowany przez klasę Class2:
using System;
namespace Chap2 {
class Class2 : Interface1 {
// wartości obiektu
private int value;
private String s;
// konstruktor
public Class2(String s) {
this.s = s;
value = s.Length;
}
// metoda dostępu do wartości pola prywatnego
public int Value() {
return value;
}
// stan instancji
public override string ToString() {
return s;
}
}
}
- wiersz 4: Class2 implementuje interfejs Interface1
- wiersz 6: wartość instancji klasy Class2
- wiersze 10–13: pole value jest inicjowane długością ciągu znaków przekazanego do konstruktora
- wiersze 16–18: metoda Value interfejsu Interface1
- wiersze 21–22: metoda ToString klasy
Program testowy mógłby wyglądać następująco:
using System;
namespace Chap2 {
class Program5 {
static void Main(string[] args) {
// wymiana instancji typu 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);
}
// wymiana instancji typu 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);
}
}
}
- wiersze 8–14: wymieniane są instancje typu Class1
- wiersze 16–22: wymieniane są instancje typu Class2
Wyniki wykonania są następujące:
Aby zilustrować pojęcie interfejsu generycznego, posortujemy tablicę osób najpierw według nazwisk, a następnie według wieku. Metodą umożliwiającą sortowanie tablicy jest statyczna metoda Sort klasy Array:

Przypominamy, że metodę statyczną wywołuje się, poprzedzając ją nazwą klasy, a nie nazwą instancji tej klasy. Metoda Sort ma różne sygnatury (jest przeciążona). Wykorzystamy następującą sygnaturę:
Sort to metoda generyczna, w której T oznacza dowolny typ. Metoda przyjmuje dwa parametry:
- T[] tablica: tablica elementów typu T, które mają zostać posortowane
- IComparer<T> komparator: odwołanie do obiektu implementującego interfejs IComparer<T>.
IComparer<T> jest interfejsem generycznym zdefiniowanym w następujący sposób:
Interfejs IComparer<T> posiada tylko jedną metodę. Metoda Compare:
- przyjmuje jako parametry dwa elementy t1 i t2 typu T
- zwraca 1, jeśli t1 > t2, 0, jeśli t1 == t2, -1, jeśli t1 < t2. To programista musi nadać znaczenie operatorom <, ==, >. Na przykład, jeśli p1 i p2 są dwoma obiektami typu Personne, można powiedzieć, że p1 > p2, jeśli nazwa p1 poprzedza nazwę p2 w porządku alfabetycznym. W ten sposób uzyskamy sortowanie rosnące według nazwisk osób. Jeśli chcemy sortować według wieku, powiemy, że p1 > p2, jeśli wiek p1 jest większy od wieku p2.
- Aby uzyskać sortowanie w porządku malejącym, wystarczy odwrócić wyniki +1 i -1
Wiemy już wystarczająco dużo, aby posortować tablicę zawierającą osoby. Program wygląda następująco:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program6 {
static void Main(string[] args) {
// tablica osób
Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
// wyświetlanie
Affiche("Tableau à trier", personnes1);
// sortowanie według nazwiska
Array.Sort(personnes1, new CompareNoms());
// wyświetlanie
Affiche("Tableau après le tri selon les nom et prénom", personnes1);
// sortowanie według wieku
Array.Sort(personnes1, new CompareAges());
// wyświetlanie
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);
}
}
}
// klasa porównawcza nazwisk i imion osób
class CompareNoms : IComparer<Personne> {
public int Compare(Personne p1, Personne p2) {
// porównuje się nazwiska
int i = p1.Nom.CompareTo(p2.Nom);
if (i != 0)
return i;
// zgodność nazwisk – porównuje się imiona
return p1.Prenom.CompareTo(p2.Prenom);
}
}
// klasa porównania wieku osób
class CompareAges : IComparer<Personne> {
public int Compare(Personne p1, Personne p2) {
// porównuje się wiek
if (p1.Age > p2.Age)
return 1;
else if (p1.Age == p2.Age)
return 0;
else
return -1;
}
}
}
- wiersz 8: tablica osób
- wiersz 12: posortowana tablica osób według nazwiska i imienia. Drugim parametrem metody generycznej Sort jest instancja klasy CompareNoms implementującej interfejs generyczny IComparer<Personne>.
- wiersze 30–39: klasa CompareNoms implementująca interfejs generyczny IComparer<Osoba>.
- wiersze 31–38: implementacja metody generycznej int CompareTo(T, T) z interfejsu IComparer<T>. Metoda ta wykorzystuje metodę String.CompareTo, opisaną w paragrafie 3.3.5.4, do porównywania dwóch ciągów znaków.
- wiersz 16: sortowanie tabeli osób według wieku. Drugim parametrem metody generycznej Sort jest instancja klasy CompareAges, która implementuje interfejs generyczny IComparer<Osoba> i jest zdefiniowana w wierszach 42–51.
Wyniki wykonania są następujące:
4.9. Przestrzenie nazw
Aby wyświetlić wiersz na ekranie, używamy instrukcji
Jeśli przyjrzymy się definicji klasy Console
Namespace: System
Assembly: Mscorlib (in Mscorlib.dll)
okazuje się, że należy ona do przestrzeni nazw System. Oznacza to, że klasa Console powinna być oznaczona jako System.Console, a w rzeczywistości należałoby napisać:
Można tego uniknąć, stosując klauzulę using:
Mówi się, że importuje się przestrzeń nazw System za pomocą klauzuli using. Gdy kompilator napotka nazwę klasy (w tym przypadku Console), będzie jej szukał w różnych przestrzeniach nazw zaimportowanych za pomocą klauzul using. W tym przypadku znajdzie klasę Console w przestrzeni nazw System. Zwróćmy teraz uwagę na drugą informację powiązaną z klasą Console:
Ten wiersz wskazuje, w którym „zestawie” znajduje się definicja klasy Console. Informacja ta może okazać się przydatna podczas kompilacji poza programem Visual Studio, gdy konieczne jest podanie odniesień do różnych plików dll zawierających klasy, z których należy skorzystać. Aby odwołać się do plików dll niezbędnych do kompilacji klasy, należy wpisać:
gdzie csc to kompilator C#. Tworząc klasę, można umieścić ją w przestrzeni nazw. Celem tych przestrzeni nazw jest uniknięcie konfliktów nazw między klasami, na przykład gdy są one sprzedawane. Rozważmy dwie firmy: E1 i E2, dystrybuujące klasy spakowane odpowiednio w dll, e1.dll oraz e2.dll. Załóżmy, że klient C kupuje te dwa zestawy klas, w których obie firmy zdefiniowały klasę o nazwie Personne. Klient C kompiluje program w następujący sposób:
Jeśli kod źródłowy prog.cs wykorzystuje klasę Personne, kompilator nie będzie wiedział, czy ma pobrać klasę Personne z klasy e1.dll, czy też z klasy e2.dll. Zgłosi błąd. Jeśli firma E1 zadba o utworzenie swoich klas w przestrzeni nazw o nazwie E1, a firma E2 w przestrzeni nazw o nazwie E2, obie klasy Personne będą miały nazwy E1.Personne i E2.Personne. Klient będzie musiał używać w swoich klasach albo E1.Personne, albo E2.Personne, ale nie Personne. Przestrzeń nazw pozwala wyeliminować tę niejednoznaczność.
Aby utworzyć klasę w przestrzeni nazw, należy wpisać:
4.10. Przykładowe zastosowanie – V2
Powrócimy do obliczania podatku, które omówiliśmy już w poprzednim rozdziale, w paragrafie 3.6, i teraz zajmiemy się tym zagadnieniem, wykorzystując klasy i interfejsy. Przypomnijmy sobie zadanie:
Zamierzamy napisać program umożliwiający obliczenie podatku podatnika. Rozważamy uproszczony przypadek podatnika, który ma do zadeklarowania wyłącznie swoje wynagrodzenie (dane z 2004 r. dotyczące dochodów z 2003 r.):
- liczy się liczbę udziałów pracownika według wzoru nbParts = nbEnfants/2 + 1, jeśli nie jest żonaty, nbEnfants/2 + 2, jeśli jest żonaty, gdzie nbEnfants to liczba jego dzieci.
- jeśli ma co najmniej troje dzieci, otrzymuje dodatkową połowę udziału
- oblicza się jego dochód podlegający opodatkowaniu R = 0,72 * S, gdzie S to jego roczne wynagrodzenie
- oblicza się jego współczynnik rodzinny QF = R / nbParts
- oblicza się jego podatek I. Rozważmy następującą tabelę:
4262 | 0 | 0 |
8382 | 0,0683 | 291,09 |
14753 | 0,1914 | 1322,92 |
23 888 | 0,2826 | 2668,39 |
38 868 | 0,3738 | 4846,98 |
47932 | 0,4262 | 6883,66 |
0 | 0,4809 | 9505,54 |
Każdy wiersz zawiera 3 pola. Aby obliczyć podatek I, należy znaleźć pierwszy wiersz, w którym QF <= pole1. Na przykład, jeśli QF = 5000, znajdziemy wiersz
Podatek I wynosi wówczas 0,0683*R – 291,09*nbParts. Jeśli QF jest takie, że warunek QF <= pole1 nigdy nie jest spełniony, wówczas stosuje się współczynniki z ostatniego wiersza. W tym przypadku:
co daje podatek I = 0,4809 * R – 9505,54 * nbParts.
Najpierw definiujemy strukturę, która może zawierać wiersz z powyższej tabeli:
namespace Chap2 {
// przedział podatkowy
struct TrancheImpot {
public decimal Limite { get; set; }
public decimal CoeffR { get; set; }
public decimal CoeffN { get; set; }
}
}
Następnie definiujemy interfejs IImpot umożliwiający obliczenie podatku:
namespace Chap2 {
interface IImpot {
int calculer(bool marié, int nbEnfants, int salaire);
}
}
- wiersz 3: metoda obliczania podatku na podstawie trzech danych: stanu cywilnego podatnika, liczby dzieci oraz wysokości wynagrodzenia
Następnie definiujemy klasę abstrakcyjną implementującą ten interfejs:
namespace Chap2 {
abstract class AbstractImpot : IImpot {
// przedziały podatkowe niezbędne do obliczenia podatku
// pochodzą ze źródła zewnętrznego
protected TrancheImpot[] tranchesImpot;
// obliczenie podatku
public int calculer(bool marié, int nbEnfants, int salaire) {
// obliczenie liczby udziałów
decimal nbParts;
if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
else nbParts = (decimal)nbEnfants / 2 + 1;
if (nbEnfants >= 3) nbParts += 0.5M;
// obliczenie dochodu podlegającego opodatkowaniu i współczynnika rodzinnego
decimal revenu = 0.72M * salaire;
decimal QF = revenu / nbParts;
// obliczenie podatku
tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
int i = 0;
while (QF > tranchesImpot[i].Limite) i++;
// zwrot wyniku
return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
}//obliczyć
}//klasa
}
- wiersz 2: klasa AbstractImpot implementuje interfejs IImpot.
- wiersz 7: roczne dane dotyczące obliczenia podatku w postaci pola chronionego. Klasa AbstractImpot nie wie, w jaki sposób pole to zostanie zainicjowane. Zadanie to pozostawia klasom pochodnym. Dlatego została zadeklarowana jako abstrakcyjna (wiersz 2), aby uniemożliwić jej instancjonowanie.
- wiersze 10–25: implementacja metody calculer z interfejsu IImpot. Klasy pochodne nie będą musiały przepisywać tej metody. Klasa AbstractImpot służy zatem jako klasa faktoryzacyjna dla klas pochodnych. Umieszcza się w niej elementy wspólne dla wszystkich klas pochodnych.
Klasę implementującą interfejs IImpot można utworzyć poprzez dziedziczenie po klasie AbstractImpot. Właśnie to teraz zrobimy:
using System;
namespace Chap2 {
class HardwiredImpot : AbstractImpot {
// tabele danych niezbędnych do obliczenia podatku
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() {
// tworzenie tabeli przedziałów podatkowych
tranchesImpot = new TrancheImpot[limites.Length];
// wypełnianie
for (int i = 0; i < tranchesImpot.Length; i++) {
tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
}
}
}// klasa
}// przestrzeń nazw
Klasa HardwiredImpot definiuje w wierszach 7–9 na stałe dane niezbędne do obliczenia podatku. Jej konstruktor (wiersze 11–18) wykorzystuje te dane do zainicjowania chronionego pola tranchesImpot klasy nadrzędnej AbstractImpot.
Program testowy mógłby wyglądać następująco:
using System;
namespace Chap2 {
class Program {
static void Main() {
// interaktywny program do obliczania podatku
// użytkownik wprowadza trzy dane za pomocą klawiatury: żonaty nbEnfants wynagrodzenie
// następnie program wyświetla podatek do zapłaty
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";
// utworzenie obiektu IImpot
IImpot impot = new HardwiredImpot();
// pętla nieskończona
while (true) {
// wymagane są parametry obliczenia podatku
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();
// co należy zrobić?
if (paramètres == null || paramètres == "") break;
// sprawdzanie liczby argumentów w wprowadzonej linii
string[] args = paramètres.Split(null);
int nbParamètres = args.Length;
if (nbParamètres != 3) {
Console.WriteLine(syntaxe);
continue;
}//if
// sprawdzanie poprawności parametrów
// żonaty
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
// dane poprawne?
if (!dataOk) {
Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
continue;
}
// wynagrodzenie
int salaire = 0;
dataOk = false;
try {
salaire = int.Parse(args[2]);
dataOk = salaire >= 0;
} catch {
}//try-catch
// dane poprawne?
if (!dataOk) {
Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
continue;
}
// parametry są poprawne – obliczamy podatek
Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
// kolejny podatnik
}//while
}
}
}
Powyższy program umożliwia użytkownikowi wielokrotne przeprowadzanie symulacji obliczeń podatku.
- wiersz 16: utworzenie obiektu impot realizującego interfejs IImpot. Obiekt ten powstaje poprzez instancjonowanie typu HardwiredImpot, który implementuje interfejs IImpot. Należy zauważyć, że zmiennej impot nie nadano typu HardwiredImpot, lecz typu IImpot. Zapis ten oznacza, że interesuje nas wyłącznie metoda calculer obiektu impot, a nie pozostałe elementy.
- wiersze 19–68: pętla symulacji obliczeń podatku
- wiersz 22: trzy parametry niezbędne dla metody calculer są wprowadzane w jednym wierszu za pomocą klawiatury.
- wiersz 26: metoda [chaine].Split(null) pozwala rozdzielić [chaine] na słowa. Są one zapisywane w tablicy args.
- wiersz 66: wywołanie metody calculer obiektu impot implementującego interfejs IImpot.
Oto przykładowe wykonanie programu:









