Rozdział 5. Programowanie obiektowe.
Adam Boduch
Wiele do tej pory mówiłem o klasach, metodach i właściwościach. Unikałem jednak stosowania skomplikowanych pojęć, które nie zostały wcześniej objaśnione. W tym rozdziale zamierzam objaśnić, na czym polega technika programowania zwana programowaniem obiektowym.
Idea programowania obiektowego staje się coraz powszechniejsza, gdyż aplikacje stają się coraz bardziej skomplikowane i złożone. Nie wystarcza już tylko podział aplikacji na moduły, taki kod należy podzielić na poszczególne klasy. Starsze języki programowania, takie jak np. C, nie umożliwiają programowania obiektowego — taka możliwość pojawiła się dopiero u następcy wspomnianego języka — C++. Omawiany tutaj język, czyli C#, jest w pełni obiektowy, dlatego dla programisty wręcz niezbędne jest poznanie, czym jest obiektowość i jak tworzyć własne klasy. Tym właśnie zajmiemy się w tym rozdziale.
1 Na czym polega programowanie obiektowe
2 Podstawowy kod formularza WinForms
2.1 Moduł Form1.Designer.cs
2.2 Generowanie kodu
2.3 Ukrywanie kodu
3 Programowanie zdarzeniowe
3.4 Generowanie zdarzeń
3.4.1 Przykładowy program
3.4.2 Generowanie pozostałych zdarzeń
3.5 Obsługa zdarzeń
4 Klasy
4.6 Składnia klasy
4.7 Do czego służą klasy
4.8 Instancja klasy
4.8.3 Słowo kluczowe this
4.9 Klasy zagnieżdżone
5 Pola
6 Metody
6.10 Zwracana wartość
6.11 Parametry metod
6.11.4 Wiele parametrów metod
6.11.5 Parametry domyślne
6.12 Przeciążanie metod
6.13 Przekazywanie parametrów
6.13.6 Przekazywanie przez wartość
6.13.7 Przekazywanie przez referencję
7 Dziedziczenie
7.14 Klasa domyślna
8 Hermetyzacja
9 Modyfikatory dostępu
9.15 Sekcja private
9.16 Sekcja public
9.17 Sekcja protected
9.18 Sekcja internal
10 Konstruktor
10.19 Pola tylko do odczytu
11 Destruktor
12 Właściwości
12.20 Modyfikatory dostępu
13 Elementy statyczne
13.21 Metody statyczne
13.22 Klasy statyczne
14 Polimorfizm
14.23 Ukrywanie elementów klas
14.24 Słowo kluczowe base
14.25 Metody wirtualne
14.26 Przedefiniowanie metod
14.27 Elementy abstrakcyjne
14.28 Elementy zaplombowane
15 .NET Framework Class Library
15.29 Przestrzenie nazw
15.30 Klasa System.Object
16 Opakowywanie typów
17 Interfejsy
17.31 Implementacja wielu interfejsów
18 Typy wyliczeniowe
18.32 Wartości elementów
19 Struktury
19.33 Konstruktory struktur
20 Operatory is i as
21 Przeładowanie operatorów
21.34 Słowo kluczowe operator
22 Dzielenie klas
23 Podsumowanie
Na czym polega programowanie obiektowe
Programy rozrastają się coraz bardziej i bardziej. Tak samo jak kiedyś nie wystarczała idea programowania proceduralnego, teraz nie wystarcza już programowanie strukturalne.
Koncepcja programowania obiektowego pojawiła się już w latach 60. za sprawą języka Simula 67, zaprojektowanego przez naukowców z Oslo w celu przeprowadzania symulacji zachowywania się statków. Jednakże idea programowania obiektowego swoją popularyzację zawdzięcza językowi SmallTalk. Połowa lat 80. to czas, kiedy programowanie obiektowe stawało się dominującą techniką — głównie za sprawą C++. Wtedy to też w wielu innych językach pojawiła się możliwość tworzenia obiektów.
Można powiedzieć, że klasa jest rodzajem zbioru, pewnym elementem programu, który wykonuje jakieś zadania. Klasa zawiera metody (funkcje) współdziałające ze sobą w celu wykonania jakiegoś zadania. Programowanie obiektowe przyczyniło się do tego, że podobnie jak moduły, klasy mogą być wykorzystywane w wielu innych projektach — ułatwia to jeszcze bardziej zarządzanie kodem i jego konserwację, a także przenoszenie go pomiędzy różnymi projektami.
Załóżmy, że napisano klasę do obsługi poczty (wysyłanie i odbieranie). Klasa może zawierać metody Connect
(połącz), SendMail
(wyślij e-mail), Disconnect
(rozłącz). Z kolei metoda Connect
może wywoływać inną, np. Error
(która też jest metodą znajdującą się w klasie), wyświetlającą komunikat o błędzie w razie niepowodzenia i zapisującą odpowiednią informację w dzienniku programu (czyli, inaczej mówiąc, w logach — plikach z rozszerzeniem *.log). Teraz taką klasę można wykorzystać w wielu innych aplikacjach — wystarczy skopiować fragment kodu i już gotowa jest obsługa błędów, łączenie itp. Taką klasę można również udostępnić innym użytkownikom lub swoim współpracownikom. Inny użytkownik nie musi wiedzieć, jak działa klasa — dla niego jest ważne jej działanie (np. wysyłanie e-maili). Użytkownik musi jedynie wiedzieć, że istnieje metoda Connect
, która połączy go z danym serwerem, oraz musi mieć świadomość obecności kilku innych funkcji. To wszystko — nie interesuje go obsługa błędów, nie musi nawet zdawać sobie sprawy z jej istnienia.
Można by oczywiście utworzyć nowy moduł, a w nim umieścić także procedury Connect
, SendMail
oraz Disconnect
, Error
i resztę potrzebnego kodu. Jednak w takim przypadku metody i zmienne (zmienne także mogą być elementami danej klasy) nie oddziałują na siebie w takim stopniu jak w przypadku programowania proceduralnego. Przykładowo, użytkownik korzystający z takiego kodu będzie miał dostęp do tych zmiennych, do których nie powinien mieć dostępu. Będzie mógł też wywołać swobodnie procedurę Error
— a nie powinien, bo może to spowodować niepożądane skutki. Dzięki klasom można sprawić, iż procedura Error nie będzie dostępna poza klasą. Jej elementy (zmienne) też nie będą mogły być odczytane przez przyszłego użytkownika.
OK, być może ta argumentacja Cię nie przekonuje. Załóżmy, że chcesz w swojej aplikacji pobierać nagłówki RSS z wiadomościami z danej witryny WWW. Zlecasz więc komuś (np. koledze z zespołu projektu informatycznego, w którym uczestniczysz), aby napisał taki moduł. W swojej aplikacji potrzebujesz również kodu, który przelicza różne istniejące jednostki miar i wag. Dołączasz obydwa moduły do swojej aplikacji po to, aby wykorzystać ich możliwości. Tak się składa, że w obydwu modułach znajdują się zmienne o tej samej nazwie. Jest problem, ponieważ kolidują one ze sobą, co może wpłynąć na złe funkcjonowanie programu. Co gorsza, obydwa moduły zawierają funkcję o tej samej nazwie! Kiedy używasz takiej funkcji, nie jesteś pewien, z którego modułu ona pochodzi. Masz problem.
Oczywiście przedstawiona tutaj sytuacja jest daleka od tego, co może przydarzyć się w rzeczywistości, z jednego prostego powodu: w C# nie ma możliwości programowania strukturalnego, ponieważ jest to język typowo obiektowy. Taka sytuacja jak przedstawiona powyżej nie ma prawa się wydarzyć.
Podstawowy kod formularza WinForms
Utwórz nowy projekt aplikacji typu Windows Forms (omawiałem to w rozdziale 1.). Oczywiście istnieje możliwość projektowania aplikacji wizualnych przy pomocy zwykłego edytora tekstu. Jednakże środowiska takie jak Visual C# Express Edition niezwykle ułatwiają projektowanie tego typu aplikacji. Są one określane mianem RAD, czyli Rapid Application Development (szybkie projektowanie aplikacji). Projektowanie interfejsów takich programów polega na umieszczaniu w odpowiednich miejscach formularza komponentów za pomocą techniki drag & drop (przeciągnij i upuść).
Biblioteka Windows Forms dostarcza nam całego zestawu komponentów, dzięki którym możemy zapewnić interakcję programu z użytkownikiem (przyciski, listy rozwijane, pola edycyjne). W trakcie projektowania interfejsu środowisko RAD w tle tworzy kod odpowiedzialny za utworzenie komponentów w momencie uruchamiania programu. Zobaczmy, jak to wygląda od kuchni.
Kliknij okno formularza prawym przyciskiem myszy. Z podręcznego menu wybierz pozycję View Code. W edytorze kodu, na osobnej zakładce wyświetlona zostanie zawartość pliku Form1.cs (przykładowo, taką nazwę ma plik w moim przypadku):
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace WindowsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
}
}
Ten plik zawiera kod źródłowy naszego formularza Windows Forms. Na samym początku do programu włączanych jest kilka najpopularniejszych przestrzeni nazw. W dalszej części kodu zadeklarowana jest przestrzeń nazw dla naszej aplikacji, a w niej klasa Form1
. Zawiera ona metodę o tej samej nazwie, która wywołuje funkcję InitializeComponent()
. Ta funkcja nie jest częścią środowiska .NET Framework, lecz została utworzona przez środowisko Visual C# Express Edition w sposób automatyczny. ¬
Spójrz na rysunek 5.1. Okno Solution Explorer zawiera hierarchiczną strukturę naszej aplikacji.
Rysunek 5.1. Okno Solution Explorer
I tak w gałęzi References mamy odwołania do poszczególnych podzespołów .NET Framework, które zawierają przestrzenie nazw wykorzystywane przez nas w projekcie. W gałęzi Properties znajdują się odwołania do plików zawierających ustawienia naszego projektu. Jeżeli teraz zapiszesz projekt (menu File/Save All), na dysku, we wskazanej lokalizacji zostaną zapisane wszystkie pliki wyszczególnione w oknie Solution Explorer.
Nas jednak najbardziej interesują pliki (moduły) Form1.cs, Form1.Designer.cs oraz Program.cs. Podwójne kliknięcie danej pozycji spowoduje otwarcie zaznaczonego pliku w edytorze kodu. Podstawowym modułem projektu jest plik Program.cs:
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace WindowsApplication1
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}
Nasza aplikacja rozpoczyna swoje działanie od tego pliku, a konkretnie od klasy Program i metody Main()
(jak zapewne pamiętasz, metoda Main()
stanowi główny element programu i to w niej muszą znajdować się polecenia, które zostaną wykonane w pierwszej kolejności). W ciele metody znajdują się odwołania do metod klasy Application
. Pierwsza z metod — EnableVisualStyles()
— uaktywnia wizualny tryb aplikacji. Kolejna metoda — SetCompatibleTextRenderingDefault()
— związana jest z graficzną biblioteką GDI. Nie powinieneś się teraz tym przejmować. Najważniejsza w tym kodzie jest metoda Run()
, która wyświetla główny formularz Form1
. Jeżeli skomentujemy tę linię, program zostanie uruchomiony, lecz żaden formularz nie zostanie pokazany. W skutek tego program natychmiast zostanie zamknięty.
Jak widzisz, w parametrze metody Run() znajduje się konstrukcja new Form1(). Operator new nakazuje utworzenie nowej instancji klasy Form1. Zostanie to omówione w dalszej części rozdziału.
Moduł Form1.Designer.cs
Jest jeszcze jeden ważny moduł, o którym należy wspomnieć, a mianowicie Form1.Designer.cs. Zawiera on kod źródłowy odpowiedzialny za zachowanie formularza oraz wszystkich umieszczonych na nim komponentów. Zawartość „czystego” projektu Windows Forms może wyglądać tak:
namespace WindowsApplication1
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Text = "Form1";
}
#endregion
}
}
Zwróć uwagę, iż to tutaj znajduje się metoda InitializeComponent()
. Odpowiada ona za utworzenie formularza oraz ustawienie dla niego właściwości. W gruncie rzeczy projektując aplikację w sposób wizualny, nigdy nie powinieneś ingerować w zawartość tego pliku, ponieważ jest ona generowana automatycznie przez środowisko Visual C# Express Edition.
Generowanie kodu
Wykonajmy mały test obrazujący, w jaki sposób projektowanie wizualne oddziałuje na kod źródłowy naszego projektu. Kliknij zakładkę Form1.cs [Design] w oknie edytora kodu. Dzięki temu z powrotem przełączysz się do trybu projektowania wizualnego. W oknie ToolBox
znajdź komponent Label
, a następnie umieść go na formularzu.
Jeżeli okno ToolBox jest niewidoczne, z menu View wybierz ToolBox.
Ponownie otwórz moduł Form1.Designer.cs. Zwróć uwagę na to, że zawartość metody InitializeComponent()
została zmieniona:
private void InitializeComponent()
{
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(125, 77);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(35, 13);
this.label1.TabIndex = 0;
this.label1.Text = "label1";
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(292, 273);
this.Controls.Add(this.label1);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
this.PerformLayout();
}
Metoda ta odpowiada za utworzenie komponentów oraz ustawienie właściwości takich jak pozycja oraz rozmiar poszczególnych kontrolek. Zwróć również uwagę, iż w klasie znajduje się nowa zmienna:
private System.Windows.Forms.Label label1;
Słowo kluczowe private
to tzw. modyfikator, ale pojęcie to zostanie objaśnione w dalszej części książki. Taki kod oznacza deklarację zmiennej o nazwie label1, typu System.Windows.Forms.Label
.
Jak powiedziałem wcześniej, zawartość pliku Form1.Designer.cs nie powinna Cię w ogóle interesować. Środowisko Visual C# Express Edition automatycznie generuje kod na podstawie czynności, jakich dokonujemy w trakcie projektowania. Np. przesunięcie komponentu Label zaowocuje uaktualnieniem kodu w pliku Form1.Designer.cs.
Ukrywanie kodu
Edytor Visual C# Express Edition posiada możliwość ukrywania fragmentu kodu. W takim przypadku z lewej strony edytora zostaną wyświetlone małe ikony, których kliknięcie spowoduje rozwinięcie ukrytego fragmentu. Jest to dość ciekawe rozwiązanie — umożliwia zwiększenie przejrzystości kodu i schowanie fragmentu, który w danym momencie nie interesuje programisty.
Wszystko to odbywa się za sprawą słowa (dyrektywy) #region
:
#region Windows Form Designer generated code
Nie ma to żadnego wpływu na działanie aplikacji, jest to jedynie informacja dla środowiska, iż ten fragment kodu można ukryć. Przykładowo, w dowolnym miejscu kodu źródłowego umieść następujący fragment :
#region Przykładowy region
// tutaj kod
#endregion
Zwróć teraz uwagę, iż po lewej stronie edytora kodu znajduje się ikona, dzięki której ten fragment można schować (rysunek 5.2).
Rysunek 5.2. Przykład zwijania kodu
Dyrektywa #region
rozpoczynająca dany region musi zostać zakończone słowem #endregion
. W rzeczywistości jest to dyrektywa preprocesora.
Programowanie zdarzeniowe
Powiedzieliśmy już sobie o programowaniu proceduralnym, strukturalnym. Tematyką tego rozdziału jest programowanie obiektowe. Należy jeszcze wspomnieć o metodzie zwanej programowaniem zdarzeniowym. Typowa aplikacja Windows Forms po uruchomieniu jest ładowana do pamięci. Następnie oczekuje na reakcję użytkownika nakazującą wykonywanie określonych czynności (np. kliknięcie myszą, przesunięcie kursora itp.).
Generowanie zdarzeń
Reakcję na określone czynności, czyli zdarzenia, najprościej generować przy użyciu środowiska Visual C# Express Edition. Okno Properties zawiera właściwości danego komponentu (położenie, rozmiar itp.) oraz zdarzenia (rysunek 5.3).
Rysunek 5.3. Okno Properties z wyświetloną listą zdarzeń
Jeżeli okno Properties jest ukryte, wystarczy użyć polecenia Properties Window z menu View.
Okno Properties posiada kilka przycisków, które określają sposoby wyświetlania. Trzeci przycisk od lewej służy do wyświetlania listy właściwości danego komponentu; naciśnięcie przycisku czwartego spowoduje wyświetlenie listy zdarzeń.
Lista zdarzeń jest domyślnie pogrupowana w kategorie. Jak widzisz, możemy zaprogramować reakcję na określone czynności, takie jak podwójne kliknięcie (DoubleClick
) czy użycie klawiszów klawiatury (KeyPress
).
#Umieść na formularzu komponent Button
.
#Przejdź do okna właściwości (Properties) i znajdź właściwość Text
.
#Zaznacz odszukaną właściwość.
#W prawej kolumnie wpisz dla właściwości nową wartość — Kliknij mnie
.
#Naciśnij Enter, co spowoduje zaakceptowanie nowej wartości.
W tym momencie ustawiliśmy właściwość dla komponentu. Oczywiście zostanie to odwzorowane w zawartości modułu Form1.Designer.cs. Kliknij teraz dwukrotnie umieszczony przycisk. Zostaniesz przeniesiony do edytora kodu, wygenerowane zostanie zdarzenie Click
odpowiadające za oprogramowanie kliknięcia komponentu.
Przykładowy program
Do tej pory pokazywałem, w jaki sposób zmieniać właściwości komponentu jedynie z poziomu okna Properties. Należałoby również wiedzieć, że jest to możliwe także z poziomu kodu źródłowego za pomocą operatora odwołania (.).
Nasz przykładowy program będzie zmieniał pozycję przycisku na formularzu. Nowa pozycja będzie losowana i ustawiana za każdym razem, gdy użytkownik kliknie przycisk. Wykorzystanie klasy losującej Random
prezentowałem już w rozdziale 3. Problemem jest natomiast ustawienie nowej pozycji naszego komponentu.
Położenie każdego komponentu w Windows Forms określa właściwość Location
, która wskazuje na strukturę System.Drawing.Point
. Struktura ta (będziemy o tym mówić w kolejnym rozdziale) ma dwa pola — X oraz Y. Listing 5.1 zawiera kod źródłowy modułu Form1.cs.
Listing 5.1. Kod źródłowy modułu Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace WindowsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
// tworzenie nowej instancji klasy
Random RandomObj = new Random();
// pobranie rozmiarów formularza
Point StartPoint = new Point(this.Size);
// wylosowanie nowej pozycji
int X = RandomObj.Next(1, StartPoint.X - 75);
int Y = RandomObj.Next(1, StartPoint.Y - 23);
this.button1.Location = new Point(X, Y);
}
}
}
Zacznijmy od początku. Jeżeli chcemy oprogramować dane zdarzenie (np. zdarzenie Click
), musimy je wygenerować, co spowoduje utworzenie tzw. procedury zdarzeniowej. W tym wypadku będzie to metoda button1_Click()
. Wygenerowanie zdarzenia Click
następuje po podwójnym kliknięciu danego obiektu (w naszym przypadku komponentu Button
). Spójrz ponownie na listę zdarzeń. Zwróć uwagę, że do zdarzenia Click
przypisana jest metoda button1_click
(rysunek 5.4).
Rysunek 5.4. Metoda przypisana do zdarzenia
Generowanie pozostałych zdarzeń
Wiesz już, jak wygenerować zdarzenie Click
. A co z pozostałymi zdarzeniami? Odszukaj i zaznacz pozycję Move
. W prawej kolumnie, z listy rozwijalnej możesz wybrać procedurę zdarzeniową, która ma odpowiadać za obsługę tego zdarzenia. Możesz też wygenerować nową. Zwyczajnie — nie wybieraj żadnej metody z listy, tylko dwukrotnie kliknij puste pole. Środowisko Visual C# Express Edition automatycznie wygeneruje nową procedurę zdarzeniową dla tego zdarzenia. W tym momencie możemy oprogramować zdarzenie Move
, które jest wywoływane, gdy komponent zostanie przeniesiony.
Kod takiej metody może wyglądać np. tak:
private void button1_Move(object sender, EventArgs e)
{
this.Text = String.Format("Aktualna pozycja: {0} - {1}", this.button1.Location.X, this.button1.Location.Y);
}
W momencie przesunięcia komponentu Button
na belce tytułowej formularza wyświetlona zostanie informacja o jego aktualnej pozycji. Należą Ci się pewne wyjaśnienia co do kodu, gdyż zastosowałem tutaj elementy nieomawiane wcześniej.
Po pierwsze, słowo kluczowe this
umożliwia odwołanie się do danej instancji klasy. Ponieważ jest to dość skomplikowane, pozwolę sobie objaśnić to szczegółowo w dalszej części rozdziału.
Formularz, podobnie jak każdy inny komponent, posiada właściwości i zdarzenia. Np. właściwość Text
określa tekst wyświetlany na belce tytułowej okna. Tak więc kod:
this.Text = "Tytuł okna";
spowoduje zmianę wartości właściwości Text
na Tytuł okna. W ten sam sposób można się odwołać do innych komponentów umieszczonych na formularzu oraz ich właściwości. Jak widzisz, w dalszej części kodu instrukcja this.button1.Location.X
odwołuje się do położenia komponentu w poziomie.
Inna rzecz, która wymaga objaśnienia, to metoda Format()
z klasy String
. Metoda służy do formatowania łańcucha. Innymi słowy, fraza {0} zostanie zamieniona przez wartość parametru metody Format()
(czyli this.button1.Location.X
); {1} zostanie zamieniona przez wartość kolejnego parametru itd. Zostanie to szczegółowo objaśnione w rozdziale 9.
Obsługa zdarzeń
Ta sama metoda (procedura zdarzeniowa) może być obsługiwana przez kilka zdarzeń. Przykładowo, znajdź na liście zdarzeń pozycję MouseHover
. Z listy rozwijanej wybierz button1_click()
. Od tego momentu zdarzenie MouseHover
będzie obsługiwane przez metodę button1_click()
. Możesz teraz uruchomić nasz program i sprawdzić jego działanie.
Zdarzenie MouseHover wykrywa moment, w którym kursor myszy znajdzie się nad danym komponentem. Innymi słowy, przy przesunięciu kursora nad przycisk zostanie wykonana metoda button1_click()
, co spowoduje zmianę pozycji komponentu.
Czasami może zajść potrzeba przypisania tej samej procedury zdarzeniowej do kilku zdarzeń różnych komponentów. Przykładowo, jeżeli umieścimy na formularzu kolejny przycisk, możemy oprogramować jego zdarzenie Click
, używając procedury zdarzeniowej button1_click()
.
Klasy
Klasy to podstawa projektowania aplikacji w języku C#. Powiedzieliśmy sobie już w rozdziale 3., iż nawet najprostszy program C# musi posiadać przynajmniej jedną klasę.
Na klasach opiera się całe środowisko .NET Framework, w tym biblioteka Windows Forms. Musisz przyzwyczaić się do myśli, iż wszystko w C# jest klasą! Każdy typ, komponent! Np. typ System.Int32
również jest klasą, a kiedy tworzymy nową klasę, automatycznie staje się ona typem! Wiem, że jest to dość zawiła terminologia, ale wszystko powinno się wyjaśnić w dalszej części rozdziału.
Klasy posiadają funkcje, które nazywane są metodami. To również powinieneś wiedzieć z lektury poprzednich rozdziałów. Do tej pory o klasach wspominałem dość sporadycznie, na tyle, na ile było to konieczne. Teraz mam zamiar omówić wszystkie elementy związane z tworzeniem nowych klas. Mam nadzieję, że dzięki temu prezentowane wcześniej elementy kodu, które nie były objaśniane, będą w pełni zrozumiałe po lekturze tego rozdziału.
Składnia klasy
W języku C# klasy deklaruje się (czyli tworzy) z użyciem słowa kluczowego class
:
class Foo
{
}
*Klasę deklarujemy z użyciem słowa kluczowego class
.
*Klasa musi mieć nazwę.
*Klasa może być pusta, lecz należy użyć klamer — { oraz }.
*Klasa może być umieszczona w przestrzeni nazw, ale nie musi.
Zwróć uwagę, że po nazwie klasy nie stawiamy znaku średnika.
Do czego służą klasy
Język C# jest w pełni obiektowy. W starszych językach, takich jak chociażby C++ czy Delphi, nie istnieje wymóg korzystania z klas. Przykładowo, prosty program w języku Delphi wygląda tak:
program Foo;
begin
Writeln('Witam serdecznie!');
Readln;
end.
Nie wiem, jak dla Ciebie, ale dla mnie taka konstrukcja jest prostsza. Język Delphi nie wymusza stosowania klas, aczkolwiek umożliwia ich użycie. Dla Czytelnika, który wcześniej programował, trochę niezrozumiała może być idea wykorzystania klas. Dlaczego C# wymusza na nas konieczność ich stosowania? Jakie właściwie korzyści płyną z użycia klas?
Przykładowo, aby złożyć komputer, nie muszę wiedzieć, jak dokładnie działa procesor i z jakich elementów jest zbudowany. Wystarczy, że wiem, że jest to centralna jednostka komputera i że niej nie uruchomię całości. Muszę także wiedzieć, gdzie włożyć procesor i jak go przymocować.
Kierowca samochodu nie musi wiedzieć, co auto ma pod maską, jakie są parametry jego silnika, jak działa skrzynia biegów i co powoduje, że całość się porusza. Wystarczy że wie, iż do uruchomienia samochodu potrzebne są kluczyki — musi również umieć posługiwać się kierownicą, dźwignią zmiany biegów i pedałami.
Jeżeli wraz ze swoimi wspólnikami projektujecie jakąś większą aplikację, każdy może zająć się przydzielonym zadaniem — przykładowo, ktoś zajmuje się utworzeniem klasy służącej do wyszukiwania plików na dysku, jeszcze ktoś tworzeniem innej klasy, a inna osoba jedynie wszystko koordynuje i łączy w całość. Nie musi ona wiedzieć, w jaki sposób działa klasa wyszukująca pliki, ale musi wiedzieć, jak ją połączyć z resztą programu, tak aby wszystko działało zgodnie z oczekiwaniami. Tego z kolei można się dowiedzieć z instrukcji (czyli z dokumentacji dostarczonej przez autora klasy).
Nie muszę wiedzieć, jak działa klasa Console
, jakie jest jej wnętrze. Obchodzi mnie tylko, jak z niej korzystać, jakie metody udostępnia i jak się nimi posługiwać. Najważniejsze jest więc wyświetlenie tekstu oraz pobranie tekstu wpisanego przez użytkownika.
Instancja klasy
Mogłeś zauważyć, iż w prezentowanych wcześniej kodach używałem słowa kluczowego new
. Ten operator (tak, jest to operator) używany jest do tworzenia tzw. instancji klasy. W tym momencie zostaje zarezerwowana pamięć potrzebna do wykonania metod znajdujących się w tej klasie. Istotną sprawą jest to, że może istnieć wiele instancji danej klasy. Jest to przewaga w stosunku do idei programowania strukturalnego. Każda instancja rezerwuje osobny blok pamięci. Ewentualne zmienne (pola) znajdujące się w obrębie klasy korzystają z osobnych przestrzeni adresowych i mogą mieć różne wartości.
W niektórych przypadkach nie trzeba tworzyć nowej instancji klasy. Zwróć bowiem uwagę, iż z klasy Console
korzystałem, nie tworząc wcześniej żadnej instancji, nie używałem w ogóle operatora new
. Ten aspekt zostanie wyjaśniony w dalszej części książki.
Spójrz na poniższą instrukcję:
System.Int32 i = new System.Int32();
Deklarujemy zmienną i typu System.Int32
, a następnie używamy operatora new
, aby utworzyć nową instancję klasy. Co prawda nie jest to konieczne, bo język C# udostępnia prostszy zapis:
int i;
Chodzi mi o sam sposób tworzenia nowej instancji klasy, który jest dość specyficzny. Należy się jednak nad tym zastanowić, gdyż w książce prezentowałem do tej pory skrócony zapis tworzenia nowej instancji.
Przed użyciem klasy czy wbudowanego typu danych (np. int
) należy zadeklarować wskazującą na nie zmienną. To już wiesz z lektury poprzednich rozdziałów. Następnie należy utworzyć nową instancję klasy i przypisać rezultat tej operacji do naszej zmiennej:
Foo MyFoo;
MyFoo = new Foo();
Od tej pory przy pomocy MyFoo
mamy dostęp do elementów klasy Foo
, czyli metod oraz pól czy właściwości.
Instancja klasy nazywana jest obiektem.
Słowo kluczowe this
Niekiedy przeglądając kody źródłowe programów napisanych w języku C#, możesz napotkać na użycie słowa kluczowego this
. Oznacza ono odwołanie do aktualnej instancji klasy. Innymi słowy, używając this
, otrzymujesz dostęp do elementów danej klasy, w której to słowo zostało użyte:
class Foo
{
int X, Y;
public void Bar()
{
this.X = 10;
this.Y = 20;
}
}
To słowo kluczowe możesz traktować jako ukryty wskaźnik do obiektu, nie jest ono wymagane podczas odwołania do elementów klasy:
public void Bar()
{
X = 10;
Y = 20;
}
Rozważmy jednak następującą sytuację:
class Foo
{
int X, Y;
public void Bar(int X, int Y)
{
X = X;
Y = Y;
}
}
Jak widzisz, w kodzie próbujemy przypisać polom klasy wartości przekazane w parametrze metody Bar()
. Kompilator wyświetli wówczas ostrzeżenie: Assignment made to same variable; did you mean to assign something else?. Komputer to tylko maszyna i nie domyśla się, że chcemy przypisać wartości polom, w końcu mają one takie same nazwy jak parametry. Wtedy z pomocą przychodzi słowo kluczowe this
, które pozwala jawnie określić, że chodzi nam o pola klasy:
this.X = X;
this.Y = Y;
Czasami może zajść potrzeba przekazania w parametrze metody wskaźnika do instancji klasy. Wtedy można użyć słowa kluczowego this: Foo(this);
.
Klasy zagnieżdżone
Środowisko .NET Framework dopuszcza możliwość zagnieżdżania typów, w tym oczywiście klas. Co to oznacza? Możliwe jest zadeklarowanie klasy wewnątrz innej klasy! Oto prosty przykład:
class Foo
{
public class Bar
{
}
}
Oczywiście przy tworzeniu instancji dla zagnieżdżanego obiektu należy podać jego lokalizację, używając operatora odwołania:
Foo.Bar FooBar = new Foo.Bar();
Pola
Pojęcie zmiennych poznałeś już dawno. Zmienne w języku C# mogą być umieszczone jedynie w klasach lub w ich metodach. Jeżeli zmienna jest umieszczona w klasie (nie wewnątrz metody), mówimy wówczas o polu klasy.
Pola po prostu są zmiennymi lub stałymi, deklarowanymi na użytek klasy lub udostępnionymi na zewnątrz od niej do użytku programisty. Dostęp do zawartości pól możliwy jest dzięki operatorowi odwołania (.):
class Foo
{
public String About = "Klasa v. 1.0";
}
class Program
{
static void Main(string[] args)
{
Foo MyFoo;
MyFoo = new Foo();
Console.Write(MyFoo.About);
Console.Read();
}
}
Oczywiście dostęp do pól danej klasy posiadają również metody, które w tej klasie się znajdują.
Metody
Funkcje języka C# umieszczone wewnątrz klas nazywane są metodami. Ponieważ język C# nie umożliwia deklarowania funkcji poza klasą, wszystkie funkcje jednocześnie są metodami.
W klasie może być wiele metod, do których dostęp mamy przy pomocy operatora odwołania, podobnie jak dostęp do pól. Jedyna różnica jest taka, że odwołując się do metody, musimy użyć symboli nawiasów okrągłych, nawet wówczas, gdy metoda nie posiada żadnych parametrów — np.:
Console.Read();
Najprostsza deklaracja metoda klasy może wyglądać np. tak:
void Bar()
{
}
Taki kod odpowiada za deklarację metody Bar()
nieposiadającej żadnych parametrów ani wartości zwrotnej. Podsumowując:
*Każda metoda musi posiadać nazwę.
*Metoda musi posiadać ciało ograniczone klamrami — { oraz } — nawet wówczas, gdy nie zawiera żadnego kodu.
*Metoda musi zwracać jakąś wartość.
*Metoda nie musi posiadać parametrów, lecz obowiązkowe są nawiasy () przy jej nazwie.
Metodę można podzielić na nagłówek (sygnaturę) oraz wspomniane już ciało, czyli znajdujący się w niej kod. Nagłówek metody to nazwa wraz parametrami oraz typem zwrotnym:
void Bar()
Deklarując metodę, zawsze trzeba określić jej typ zwrotny. Innymi słowy, każda metoda (funkcja) musi zwracać jakąś wartość. Przykładowo, możemy zadeklarować metodę Power()
, która podnosi daną liczbę do potęgi i zwraca przemnożoną wartość typu int. Jeżeli nie chcemy, aby metoda zwracała jakiekolwiek wartości, należy użyć typu void
, który oznacza wartość pustą.
W języku Pascal/Delphi istnieją procedury (które nie zwracają żadnej wartości) oraz funkcje (które muszą zwrócić jakąś wartość). W większości języków, np. C, C++, Java i C#, nie istnieją procedury, a jedynie funkcje, które zwracają jakąś wartość.
Typ void jest aliasem dla typu .NET Framework System.void.
Zwracana wartość
Aby lepiej zaprezentować możliwość zwracania wartości przez metody klasy, napiszmy prostą metodę i zaprezentujmy jej użycie.
W naszej przykładowej klasie Foo zadeklaruj metodę Power()
:
public int Power(int X)
{
return X * X;
}
Jest to prosta metoda, która zwraca wartość typu int
. Metoda posiada jeden parametr X (również typu int). W ciele metody następuje przemnożenie wartości parametru X i zwrócenie rezultatu.
Słowo kluczowe public to tzw. modyfikator dostępu. Pojęcie to zostanie objaśnione w dalszej części rozdziału.
Słowo kluczowe return
służy przede wszystkim do zwracania wartości. Gdy ono wystąpi, kod zawarty w dalszej części metody nie będzie już wykonywany:
int Bar()
{
return 2; // zwróć wartość 2
Console.WriteLine("Ten kod nie będzie wykonany");
}
Jeżeli wartość zwrotna metody jest typu void
, można pominąć wywołanie słowa return
lub pozostawić samo słowo kluczowe:
public void Bar()
{
return;
Console.WriteLine("Ten kod nie będzie wykonywany");
}
Parametry metod
Trochę się pospieszyłem i w poprzednim przykładzie skorzystałem z tzw. parametru. Pisząc własną metodę, w nawiasach okrągłych możemy określić, jakie dane wejściowe spodziewamy się otrzymać.
Przykładowo, parametrem metody Power()
jest X typu int
. Jest to parametr wejściowy, na jakim nasza metoda operuje.
Innymi słowy, chcąc użyć metody Power()
, w nawiasach musimy wpisać jakąś liczbę typu int
, gdyż tego wymaga od nas ta metoda, a tym samym — kompilator. Parametry metod mogą być dowolnego typu.
Dla zabawy możemy napisać prostą aplikację, w której zadeklarujemy metodę przeliczającą — powiedzmy — kilometry na mile angielskie. Jedna mila angielska to 1609,344 metry, czyli 1,6 km. Nasza metoda musi posiadać parametr, który oznaczać będzie liczbę kilometrów do obliczenia. Taki kod może wyglądać tak:
static double KmToMile(double Km)
{
return ((Km * 1000) / 1609.344);
}
Parametr naszej metody nosi nazwę Km i jest typu double
. W ciele metody dokonujemy działań matematycznych, które pozwolą uzyskać liczbę mil zwracanych przy pomocy słowa kluczowego return
.
Cały program prezentuje listing 5.2.
Listing 5.2. Kod źródłowy programu
using System;
namespace Temperature
{
class Program
{
static double KmToMile(double Km)
{
return ((Km * 1000) / 1609.344);
}
static void Main(string[] args)
{
Console.Write("Podaj liczbę kilometrów: ");
double Km = Convert.ToDouble(Console.ReadLine());
Console.WriteLine("Liczba mil: " + KmToMile(Km));
Console.ReadLine();
}
}
}
Wiele parametrów metod
Do tej pory w prezentowanych metodach używałem pojedynczych parametrów. Możliwe jest deklarowanie metod z wieloma parametrami, które posiadają różne typy. W takim przypadku parametry należy rozdzielić znakiem przecinka:
static int Multiple(int X, int Y)
{
return X * Y;
}
Parametry danej metody niekoniecznie muszą być tego samego typu jak w zaprezentowanym przykładzie (parametry są typu int
); równie dobrze można użyć innych typów dostępnych w C#:
static int Multiple(int X, int Y, float Z)
Oczywiście przekazując dane w parametrze metody, musisz zadbać o to, aby były one zgodne z typem zadeklarowanym w jej nagłówku.
Parametry domyślne
Znana zapewne wielu programistom możliwość nadawania domyślnych parametrów dla metod jest w języku C# niedostępna. Zamiast tego należy używać mechanizmu przeciążania metod. Jest to zapewne ważna informacja, jeżeli programowałeś wcześniej w innym języku.
Jeżeli dana metoda wymaga podania parametru, nie można tego pominąć przy jej wywoływaniu. W takim wypadku kompilator wyświetli błąd No overload for method 'Multiple' takes '0' argument.
Mówiąc o wywołaniu metody, mam na myśli jej użycie. Z takim pojęciem będziesz spotykał się w dalszej części książki.
Przeciążanie metod
Przeciążanie (często nazywane przeładowywaniem) metod jest techniką bardzo użyteczną i często spotykaną w środowisku .NET Framework.
W danej klasie nie mogą istnieć dwie metody o tej samej nazwie. Nie mogą istnieć, pod warunkiem że ich nagłówek jest taki sam. Nie ma przeszkód, aby zadeklarować dwie metody o tej samej nazwie, jeżeli tylko posiadają różną ilość parametrów (lub parametry są różnych typów). Oto przykład:
static int Multiple(int X, int Y)
{
return X * Y;
}
static double Multiple(double X, double Y)
{
return X * Y;
}
Program będzie w stanie stwierdzić, o którą metodę nam chodzi, na podstawie parametrów, jakie jej przekażemy:
Multiple(23.34, 35.45); // dobrze
Multiple(112, 10); // dobrze
W pierwszym przypadku wywoła metodę, której parametry są typu double; w drugim — tę z parametrami typu int
.
Przekazywanie parametrów
Proste, jak mogłoby się wydawać, przekazywanie parametrów do metod ma o wiele większe zastosowanie w języku C#. Są bowiem różne sposoby przekazywania parametrów do metod, o czym przekonasz się po lekturze dalszej części tego rozdziału.
Przekazywanie przez wartość
Do tej pory przekazywałeś do metod pewne wartości — np.:
Power(2, 3);
W metodzie Power()
otrzymujemy te wartości pod postacią zmiennych (w poprzednim przykładzie — X i Y). Nic nie stoi na przeszkodzie, aby przekazać do metody wartości w formie zmiennej:
int X, Y;
X = 2;
Y = 3;
Power(X, Y);
Jak widzisz, zadeklarowałem w programie zmienne X i Y, następnie nadałem im wartości, po czym przekazałem je do metody Power()
. Przekazywanie parametrów przez wartość równa się utworzeniu kopii zmiennych w danej metodzie.
Język C# nie zabrania nam jednak modyfikacji wartości parametrów w ciele metody. Aby lepiej to zrozumieć, spójrz na poniższy listing:
using System;
namespace Temperature
{
class Program
{
static int Power(int X, int Y)
{
X = 5;
Y = 5;
return X * Y;
}
static void Main(string[] args)
{
int X, Y;
X = 2;
Y = 3;
int Z = Power(X, Y);
Console.Write(Z);
Console.Read();
}
}
}
Mimo iż przekazaliśmy do metody parametry X i Y (odpowiednio cyfry 2 i 3), w ciele metody Power()
nastąpiło nadpisanie ich wartości. Jednakże oryginalna wartość zmiennej przekazanej do metody nie zostaje w żaden sposób naruszona:
int X, Y;
X = 2;
Y = 3;
int Z = Power(X, Y);
Console.Write(Z); // 25
Console.Write(X); // 2
Przekazywanie przez referencję
Przekazywanie parametrów przez referencję umożliwia metodzie modyfikację wartości, jaka została do niej przekazana oryginalnie. Oczywiście najłatwiej wytłumaczyć to na przykładzie. Spójrz na poniższy program:
using System;
namespace Temperature
{
class Program
{
static void Foo(out string S1, out string S2)
{
S1 = "Hello ";
S2 = "World";
}
static void Main(string[] args)
{
string MyS1, MyS2;
// przekazując parametry, nie zapomnij
// o użyciu out
Foo(out MyS1, out MyS2);
Console.WriteLine(MyS1 + MyS2);
Console.Read();
}
}
Wskutek jego uruchomienia na ekranie konsoli pojawi się napis: Hello World
. Deklarując parametry referencyjne w nagłówku metody, musimy je poprzedzić słowem kluczowym out
. Również przekazując parametry, musimy użyć tego słowa.
Kiedy używamy parametrów referencyjnych, nic nie stoi na przeszkodzie, aby w zależności od potrzeb przekazywać również parametry przez wartość. Spójrz na poniższy kod:
static void Foo(string S, out string S1, out string S2)
{
S1 = "Hello ";
S2 = "World" + S;
}
static void Main(string[] args)
{
string MyS1, MyS2;
Foo(" my darling!", out MyS1, out MyS2);
Console.WriteLine(MyS1 + MyS2);
Console.Read();
}
Pierwszy parametr metody Foo()
jest przekazywany przez wartość, kolejne dwa — przez referencję.
Zwracanie danych przez referencję często się przydaje, gdy metoda musi zwrócić wiele informacji na temat swojego działania. Każda metoda może zwrócić wartość — owszem. Ale co wówczas, gdy musisz zwrócić do programu głównego kilka informacji? Niezastąpiona jest wtedy metoda, której parametry zwracane są przez referencję.
W języku C# istnieje możliwość przekazywania parametrów, również przy pomocy słowa kluczowego ref
. Różnica pomiędzy ref
a out
jest taka, iż w przypadku ref zmienna musi zostać zainicjowana (tj. dane muszą zostać do niej wcześniej przypisane):
static void Foo(ref string S)
{
Console.WriteLine("Wartość oryginalna: " + S);
S = "Bar";
}
static void Main(string[] args)
{
string MyS = "Foo";
Foo(ref MyS);
Console.WriteLine("Wartość zastąpiona: " + MyS);
Console.Read();
}
Jeżeli w programie nie przypisalibyśmy wartości dla zmiennej MyS, podczas kompilacji wystąpiłby błąd: Use of unassigned local variable 'MyS'.
W programie C# każda zmienna przed użyciem musi zostać zainicjowana (czyli najprościej mówiąc — muszą zostać przypisane do niej jakieś dane). Poniższy fragment prezentuje kod, którego próba kompilacji zakończy się błędem (Use of unassigned local variable):
int I;
Console.WriteLine(I);
</dfn>
Programowanie z użyciem typów referencyjnych jest wydajnym sposobem tworzenia metod, zwłaszcza w przypadku dużych parametrów (np. zawierających spore porcje danych). Przekazywanie przez wartość powoduje utworzenie kopii zmiennej i przekazanie jej metodzie. Ta może na tej kopii dokonywać dowolnych operacji, a oryginał i tak nie ulegnie zmianie. W przypadku referencji do metody przekazywany jest jedynie adres komórki pamięci, w której znajduje się wartość (sama wartość nie jest duplikowana). Celowo w poprzednich przykładach nadawałem zmiennym różne nazwy, gdyż zarówno MyS, jak i S wskazywały na te same dane w pamięci.
Dziedziczenie
Cała biblioteka Windows Forms oparta jest na dziedziczeniu. Ba — cała biblioteka klas środowiska .NET Framework oparta jest na dziedziczeniu, które można określić jako fundament budowania klas.
Powróćmy do przykładu z silnikiem. Projektanci, chcąc ulepszyć dany silnik, mogą nie chcieć zaczynać od zera. Byłaby to zwyczajna strata czasu. Nie lepiej po prostu unowocześnić już istniejący silnik?
Przykład z silnikiem można zastosować do klas. Aby zbudować nową, bardziej funkcjonalną klasę, można przejąć możliwości starej. Taki proces nazywamy w programowaniu dziedziczeniem.
Przykładowo, w całym środowisku .NET Framework główną klasą jest Object
, znajdująca się w podstawowej przestrzeni nazw — System
. Kolejne klasy, np. Int32
, już tylko dziedziczą z tej podstawowej, przejmując od niej wszystkie możliwości. „Możliwościami” w tym wypadku są metody znajdujące się w klasie Object
. Przykładowo, klasa ta zawiera metodę ToString()
. Klasa Int32
oraz wszystkie pozostałe również zawierają tę metodę, ponieważ ją odziedziczyły!
W takim wypadku o klasie Object
mówimy jako o klasie bazowej, a Int23
nazywamy klasą potomną. Spójrz na poniższy fragment kodu, w którym zadeklarowano dwie klasy:
class A
{
public void Foo()
{
Console.WriteLine("Jestem metodą z klasy A!");
}
}
class B : A
{
}
W klasie A
znajduje się metoda Foo()
. Klasa B
dziedziczy po A
, czyli przejmuje od niej wszystkie metody i pola! Dziedziczenie polega na umieszczeniu klasy bazowej po znaku dwukropka:
class Nazwa klasy : Nazwa klasy bazowej { }
Możemy teraz bez problemu utworzyć instancję klasy B
i wywołać metodę Foo()
:
B MyClass = new B();
MyClass.Foo();
Klasa domyślna
Kiedy deklarujemy nową klasę, nie ma wymogu podawania jej klasy bazowej! Jeżeli nie określimy klasy bazowej, to nasz typ automatycznie będzie dziedziczył po klasie System.Object
, przejmując od niej wszystkie metody.
Hermetyzacja
Pojęcie hermetyzacji jest związane z ukrywaniem pewnych danych. Klasy udostępniają na zewnątrz pewien interfejs opisujący ich działanie i tylko z tego interfejsu może korzystać użytkownik. Klasy mogą bowiem zawierać dziesiątki, a nawet setki metod (funkcji), które wykonują różne czynności. My jako projektanci klasy powinniśmy zapewnić dostęp jedynie do niektórych metod, tak aby potencjalny użytkownik nie mógł wykorzystywać wszystkich, gdyż może to spowodować nieprzewidywalne działanie programu, zawieszanie itp.
Wewnątrz silnika samochodu też dochodzi do pewnych procesów, ale kierowca nie musi o nich wiedzieć. Informacji tych nie potrzebuje także inny element silnika, który się z nim łączy — komunikowanie się pomiędzy elementami przebiega ustalonym strumieniem i to wystarczy.
C# pozwala na ukrywanie kodu w klasie, w tym celu stosuje się pewne klauzule nazwane w C# modyfikatorami dostępu (ang. access modifiers).
Modyfikatory dostępu
Modyfikatory dostępu to słowa kluczowe określające dostęp do elementów klasy.
Elementami klasy są jej metody oraz pola, a także właściwości (o tym za chwilę).
Język C# udostępnia cztery słowa kluczowe określające sposób dostępu do danego członka klasy: private
(prywatne), protected
(chronione), public
(publiczne) oraz internal
(wewnętrzne). W zależności od sekcji, w której metody zostaną umieszczone, będą one inaczej interpretowane przez kompilator. Modyfikator dostępu musi poprzedzać deklarację danego elementu klasy:
class Bar
{
private string S;
public
int X, Y;
protected double D;
}
W klasie Bar
pole S będzie prywatne, natomiast pola X i Y — publiczne. W klasie zadeklarowałem jedno pole chronione — D.
Gdy wystąpi słowo kluczowe określające modyfikator dostępu, wszystkie dalsze deklaracje będą traktowane zgodnie z jego znaczeniem. W powyższym kodzie pola X i Y będą więc typu publicznego. Dobrą praktyką jest poprzedzanie każdej deklaracji elementu klasy odpowiednim modyfikatorem:
class Bar
{
private int X;
private int Y;
public string S;
protected double D;
}
Sekcja private
Wszystkie pola czy metody umieszczone w klasie automatycznie są traktowane jako prywatne. Oznacza to, że dostęp do nich jest niemożliwy spoza klasy, w której są umieszczone! Oto przykład:
class MyClass
{
int X, Y;
}
class Program
{
static void Main(string[] args)
{
MyClass.X = 10; // błąd!
MyClass Class = new MyClass();
Class.Y = 10; // błąd!
}
}
Obydwa odwołania do pól X oraz Y są błędne. Kompilator w trakcie kompilacji wyświetli błąd: 'FooConsole.MyClass.X' is inaccessible due to its protection level. Nie ma za to problemu z dostępem do tych pól z poziomu metod znajdujących się w tej samej klasie:
class MyClass
{
int X, Y;
void Foo()
{
X = 10;
Y = 20;
}
}
Poniższe dwie instrukcje są ze sobą równoważne:
private int X;
int X;
Jeżeli nie określimy modyfikatora dostępu, kompilator automatycznie będzie traktował takie elementy jako prywatne.</dfn>
Sekcja public
Modyfikator public
oznacza, iż elementy klasy opatrzone tą klauzulą będą traktowane jako publiczne. Do takich elementów można odwoływać się zarówno spoza klasy, jak i z jej wnętrza. Poniższy kod jest więc jak najbardziej prawidłowy:
class MyClass
{
public int X, Y;
public void Foo()
{
X = 10;
Y = 20;
}
}
class Program
{
static void Main(string[] args)
{
MyClass Class = new MyClass();
Class.Foo();
Class.X = 20;
Class.Y = 50;
}
}
Modyfikatory dostępu mają takie same działanie w przypadku pól oraz metod. Chciałbym podkreślić, iż metody również mogą być prywatne lub publiczne, tak samo jak pola klasy.
Sekcja protected
Elementy klasy zadeklarowane jako protected
są traktowane jako chronione. Ma to związek z procesem dziedziczenia klas. Chronione elementy są niedostępne poza klasą, w której są zadeklarowane. Są one „widoczne” jedynie dla klas potomnych. Spójrz na poniższy przykład:
class A
{
protected int X;
public void Foo()
{
Console.WriteLine("Jestem metodą z klasy A!");
}
}
class B : A
{
private void Bar()
{
X = 10;
}
}
class Program
{
static void Main(string[] args)
{
B MyB = new B();
MyB.Foo();
A MyA = new A();
MyB.X = 10; // Błąd!
MyA.X = 10; // Błąd!
}
}
Spójrz na metodę Main()
klasy Program
. Podczas próby kompilacji takiego kodu odwołanie się do elementu X, zarówno z obiektu MyA
, jak i MyB
, zakończy się niepowodzeniem. Natomiast w klasie B
możemy wykorzystać pole X, mimo iż zostało ono zadeklarowane w klasie A
.
Sekcja internal
Jest to zdecydowanie najrzadziej wykorzystywany modyfikator dostępu. Określa on, że z danego elementu będzie można skorzystać jedynie w obrębie podzespołu, w którym jest użyty. Szczegółowe omówienie podzespołów .NET znajdziesz w rozdziale 11.
Konstruktor
Sama klasa nie zajmuje miejsca w pamięci komputera. W momencie utworzenia nowej instancji program rezerwuje w pamięci miejsce potrzebne do wykonania danej klasy.
Konstruktor to specjalna metoda, która jest wywoływana automatycznie w momencie utworzenia instancji klasy. W rzeczywistości instrukcja new Foo()
oprócz utworzenia instancji próbuje wywołać konstruktora tej klasy. Konstruktor jest jednak elementem opcjonalnym i jego pominięcie nie jest błędem.
Konstruktor jest metodą o takiej samej nazwie co klasa, w której jest zadeklarowany. Spójrz na poniższy fragment kodu:
using System;
namespace MyConsole
{
class Foo
{
public Foo()
{
Console.WriteLine("Jestem konstruktorem klasy Foo");
}
}
class Program
{
static void Main(string[] args)
{
Foo MyFoo;
MyFoo = new Foo();
Console.Read();
}
}
}
Zwróć uwagę, że w klasie Foo
zadeklarowałem metodę Foo()
, która od tego momentu jest konstruktorem klasy. Instrukcja:
MyFoo = new Foo();
tworzy nową instancję klasy i wywołuje jej konstruktor. Możesz to sprawdzić, uruchamiając powyższy program.
Konstruktor musi być opatrzony klauzulą public
. Nie może on również zwracać żadnej wartości. Deklarując konstruktor, nie określamy jego typu zwrotnego, nawet jeśli używamy wartości pustej (void).
Istnieje możliwość przeciążania konstruktorów, tak jak można przeciążać zwykłe metody klasy. Proszę pamiętać o tym, że jeżeli chcemy w klasie umieścić wiele konstruktorów, każdy z nich musi posiadać różne parametry.
Konstruktor musi posiadać taką samą nazwę jak klasa. Nie ma możliwości utworzenia konstruktor klasy, który posiadałby inną nazwę.
Pola tylko do odczytu
Istnieje możliwość deklarowania pól jedynie do odczytu. Wartości takich pól nie mogą być modyfikowane. Istnieje jeden wyjątek pozwalający na modyfikację zawartości takiego pola w konstruktorze. Spójrz na poniższy kod:
class MyClass
{
private readonly int X = 10;
public MyClass()
{
X = 20;
}
public void Foo()
{
X = 30; // Błąd!
}
}
Pola tylko do odczytu deklaruje się z użyciem słowa kluczowego readonly
. Wartość takiego pola można nadać bezpośrednio w kodzie (w trakcie deklaracji), tak jak w przypadku stałych, lub w ciele konstruktora, tak jak zostało to zaprezentowane na przykładzie. Próba zmiany wartości pola w innym miejscu w kodzie kończy się błędem: A readonly field cannot be assigned to (except in a constructor or a variable initializer).
Pola tylko do odczytu mogą być dobrym rozwiązaniem pośrednim pomiędzy polami stałymi (const
) a zwykłymi.
Destruktor
Destruktor jest również specjalną metodą, wywoływaną po zakończeniu korzystania z danej klasy. Przy zamykaniu aplikacji specjalny mechanizm środowiska .NET Framework (nazwany garbage collection) zwalnia pamięć zarezerwowaną przez poszczególne instancje klas naszego programu. W momencie usuwania danej klasy z pamięci wywoływany jest jej destruktor. Jeżeli programista chce jakoś zareagować na moment usuwania klasy z pamięci, powinien umieścić w niej destruktor.
Destruktor w klasie może być tylko jeden i musi nosić taką samą nazwę co klasa, w której się znajduje. Destruktor należy zadeklarować, poprzedzając jego nazwę symbolem ~.
~Foo()
{
Console.WriteLine("Do widzenia...");
}
Właściwości
Język C# dopuszcza tworzenie publicznych pól. Dobrą praktyką jest deklarowanie pól jako elementów prywatnych. Klasa powinna wykorzystywać pola jako zmienne, jedynie na własny użytek. Do komunikowania się ze „światem zewnętrznym” powinno się używać właściwości.
Właściwości, podobnie jak pola (te pojęcia są często ze sobą mylone), służą do gromadzenia danych (czyli do odczytywania oraz przypisywania informacji). Oprócz tego w przypadku właściwości istnieje możliwość zaprogramowania dodatkowych czynności podczas, na przykład, przypisywania im wartości (danych).
Właściwości mogą być prywatne, lecz najczęściej są to elementy publiczne.
Cała biblioteka klas środowiska .NET Framework opiera się na właściwościach oraz metodach. Pola klas tej biblioteki są polami prywatnymi, wykorzystywanymi na potrzeby danej klasy.
Właściwość musi posiadać przede wszystkim nazwę oraz typ. Przykładowa deklaracja właściwości:
class Foo
{
private int month = 12;
public int Month
{
get
{
return month;
}
}
}
Obowiązkowymi elementami właściwości są również klamry — { }. Po słowie kluczowym get
należy umieścić instrukcje, które będą wykonywane w momencie, gdy użytkownik zażąda odczytu wartości owej właściwości. Innymi słowy: próba odczytania wartości Month spowoduje zwrócenie wartości pola month.
Z właściwości korzystamy tak jak ze zwykłego pola:
Foo MyFoo = new Foo();
Console.WriteLine("Miesiąc: " + MyFoo.Month);
Właściwości języka C# mogą być jedynie do odczytu lub jedynie do zapisu (albo do odczytu i zapisu jednocześnie). Innymi słowy, możemy zabronić przypisywania danych do właściwości. W powyższym przykładzie właściwość Month jest tylko do odczytu, próba przypisania danych zakończy się komunikatem błędu: Property or indexer 'FooConsole.Foo.Month' cannot be assigned to -- it is read only.
Słowo kluczowe get
określa akcję, jaka będzie wykonywana w momencie odczytu danych. Natomiast inne słowo kluczowe — set
— umożliwia zaprogramowanie czynności, jakie będą wykonywane w momencie zapisu danych.
Przykładowo: piszemy klasę, która posiada właściwość Month (miesiąc). Chcemy, aby użytkownik mógł do niej przypisać liczbę z zakresu od 1 do 12 (w końcu mamy 12 miesięcy w roku). Dzięki właściwościom możemy dokonać sprawdzenia poprawności danych:
class Foo
{
private int month = 12;
public int Month
{
get
{
return month;
}
set
{
if (value > 1 && value <= 12)
{
month = value;
}
}
}
}
value
jest słowem kluczowym języka C#. Przechowuje wartość przypisaną do właściwości. Możesz sprawdzić działanie takiego programu. Próba przypisania liczby większej od 12 (lub mniejszej od 1) zakończy się niepowodzeniem:
Foo MyFoo = new Foo();
MyFoo.Month = 23; // wartość nie zostanie przypisana!
Kolejny przykład:
class Foo
{
private int hour;
private int second;
public int Hour
{
get
{
return hour;
}
set
{
if (value >= 0 && value <= 24)
{
hour = value;
second = value * 3600;
}
}
}
public string Info()
{
return "Godzina " + hour + ", " + second + " sekunda tej doby";
}
}
Przy ustawianiu wartości dla właściwości Hour obliczana jest ilość sekund. Te wartości są następnie przechowywane w polach hour oraz second (uwaga na wielkość znaków!). Publiczna metoda Info()
zwraca łańcuch zawierający informacje odnośnie do liczby sekund oraz godziny.
Podsumowując:
*Właściwości zapewniają dogodny sposób na przypisywanie i odczytywanie danych, ukrywając przy tym szczegóły ich weryfikacji.
*Słowo kluczowe get
jest używane do zwracania wartości, a set
— do ustawiania nowych.
*Właściwość nie może pozostać pusta, tj. musi posiadać blok set
lub get
.
*Właściwości nieposiadające bloku set
są tylko do odczytu.
Modyfikatory dostępu
Właściwości również mogą posiadać modyfikatory dostępu. To jest jasne. Ciekawostką jest to, że modyfikatory dostępu mogą być przypisywane blokom set
oraz get
:
public int Hour
{
get
{
return hour;
}
protected set
{
if (value >= 0 && value <= 24)
{
hour = value;
second = value * 3600;
}
}
}
Tak zadeklarowanej właściwości nie można nadać wartości, gdyż blok set
opatrzony jest klauzulą protected
. Można to dopiero zrobić w klasach potomnych:
class Bar : Foo
{
public Bar()
{
// przypisanie nowej wartości
Hour = 23;
}
}
W przyszłości możesz się spotkać z określeniem „akcesory” w odniesieniu do bloków set
oraz get
właściwości klas.
Elementy statyczne
Chciałbym w tym momencie wspomnieć o istotnym elemencie programowania obiektowego; bardzo istotnym, jeżeli chodzi o środowisko .NET Framework, w którym występuje bardzo często. Mowa tutaj o elementach statycznych.
Biblioteka klas .NET Framework to setki (jeśli nie tysiące) klas. Zwróć uwagę, że możemy używać niektórych klas, nie tworząc nowej instancji. Dobrym przykładem jest klasa Console
, której prawdopodobnie używałeś najczęściej, czytając tę książkę. Nie przypominasz sobie, abyś kiedykolwiek tworzył instancję tej klasy, prawda?
Biblioteka klas zawiera mnóstwo typów, które są statyczne, opatrzone klauzulą static
. Klasy statyczne (klasa też jest typem!) mogą działać bez konieczności tworzenia nowego obiektu. Ba, niekiedy nie jest wówczas możliwe utworzenie nowej instancji takiej klasy przy użyciu słowa kluczowego new
. Klasy statyczne są ładowane do pamięci przez CLR w momencie, gdy program lub przestrzeń nazw, która je zawiera, jest ładowana.
Metody statyczne
Słowo kluczowe static
jest właściwie modyfikatorem dostępu. Statyczne mogą być nie tylko klasy, ale również metody czy właściwości. Spójrz na poniższy przykład:
class Foo
{
public Foo()
{
Console.WriteLine("Tworzenie nowego obiektu!");
}
static public void Bar()
{
Console.WriteLine("Hello from static!");
}
}
W klasie Foo
utworzyłem konstruktor oraz metodę Bar()
, która jest statyczną metodą publiczną. Taką metodę mogę wywołać, nie tworząc nowej instancji klasy:
Foo.Bar();
Nie oznacza to, że nie mogę najzwyczajniej utworzyć nowego obiektu:
Foo MyFoo = new Foo();
Metody statyczne różnią się od normalnych pod wieloma względami. Przede wszystkim metoda statyczna nie może być wywoływana z poziomu obiektu klasy:
Foo MyFoo = new Foo();
MyFoo.Bar(); // <-- błąd
Taka konstrukcja spowoduje wyświetlenie błędu kompilacji: Static member 'FooConsole.Foo.Bar()' cannot be accessed with an instance reference; qualify it with a type name instead.
To samo jednak można powiedzieć o zwykłych metodach wewnątrz klasy. One z kolei nie mogą być wywoływane bez uprzedniego utworzenia instancji klasy.
Kolejne poważne ograniczenie, o którym powinieneś wiedzieć, jest takie, że w metodach statycznych nie można używać słowa kluczowego this
. Wszystko dlatego, że this odnosi się do danej instancji klasy. Oznacza to więc, że nie możesz się odwoływać do pól/właściwości klasy, które nie są statyczne! Jest to ważne, więc radzę zapamiętać to zdanie — być może pozwoli to uniknąć problemów z kompilacją programu. Oto przykład:
static int Field;
int Field2;
static public void Bar()
{
Field = 10;
Field2 = 10; // <-- błąd
}
Z metody statycznej Bar()
nie możemy odwołać się do pola Field2, ponieważ nie jest ono statyczne.
Natomiast odwrotna sytuacja jest jak najbardziej dopuszczalna. Tzn. zwykłe metody klas mogą odwoływać się do metod statycznych.
Klauzula static
musi znajdować się przed słowem określającym typ zwrotny metody. Z kolei położenie modyfikatora dostępu nie ma znaczenia. Możemy więc napisać static public
lub public static
.
Metody statyczne mogą być prywatne lub publiczne, aczkolwiek deklarowanie prywatnej metody statycznej mija się z celem.
Klasy statyczne
Klasa może posiadać statyczne metody, pola oraz właściwości. Słowem kluczowym static
możemy opatrzyć nawet całą klasę. Wówczas wymuszamy, aby wszystkie elementy tej klasy były statyczne. Spójrz na poniższy kod:
static class Foo
{
public void Bar() // <-- błąd
{
}
}
Mimo iż klasa została opatrzona klauzulą static
, metoda Bar()
wciąż pozostaje zwykłą metodą. Próba kompilacji takiego kodu zakończy się błędem: 'Bar': cannot declare instance members in a static class.
Statyczne klasy mogą posiadać konstruktory, lecz one także muszą być statyczne i prywatne (tj. pozbawione modyfikatora public
):
static class Foo
{
static Foo()
{
Console.WriteLine("Wykryłem użycie klasy Foo!");
}
public static void Bar()
{
Console.WriteLine("Statyczna metoda Bar()!");
}
}
Statyczny konstruktor nie może posiadać modyfikatorów dostępu oraz parametrów. Nie można go jawnie wywołać — jest on wywoływany wówczas, gdy nastąpi pierwsze odwołanie do klasy:
Foo.Bar();
Podsumowując: typy statyczne to bardzo wydajny mechanizm umożliwiający wykorzystanie klas bez konieczności tworzenia ich instancji. Podczas projektowania własnych klas musisz się zastanowić, czy będziesz potrzebował tworzyć kilka obiektów danej klasy oraz jak jest ona skomplikowana.
Klasy statyczne mogą być przydatne jako „pojemnik” służący do grupowania metod wykonujących podobne zadania. Dobrym przykładem może być klasa Math
, która zawiera metody służące do obliczeń matematycznych.
Polimorfizm
Pojęcie polimorfizmu w języku C# jest związane z dziedziczeniem. Jest to dość skomplikowane pojęcie (szczególnie dla początkujących programistów) pozwalające na tworzenie zaawansowanych klas bezpośrednio ze sobą połączonych. Ja postaram się wytłumaczyć to jak najprościej, prezentując fragmenty kodu.
Polimorfizm jest największym osiągnięciem techniki programowania obiektowego. Słowo to pochodzi od greckiego polýmorphos oznaczającego wielopostaciowy, co odzwierciedla znaczenie tej techniki. W programowaniu oznacza to możliwość operowania na obiektach należących do różnych klas. Oczywiście ta definicja teraz wydaje Ci się bardzo mglista, więc zacznijmy od początku…
Ukrywanie elementów klas
Powiedzieliśmy już, że klasy potomne dziedziczą z klas bazowych ich elementy publiczne oraz chronione. Rozważmy teraz sytuację, w której w klasie potomnej chcemy zadeklarować metodę istniejącą w klasie bazowej. Myślę, że poniższy przykład zobrazuje moje zamierzenia:
class Animal
{
public void Run()
{
Console.WriteLine("Metoda Run() z klasy Animal");
}
}
class Mammal : Animal
{
public void Run()
{
Console.WriteLine("Metoda Run() z klasy Mammal");
}
}
Klasa Mammal
dziedziczy po Animal
. Obie posiadają metodę Run()
. Jeśli utworzymy nową instancję klasy Mammal
i wywołamy metodę Run()
, wykonany zostanie kod z klasy Mammal
:
Mammal MyAnimal = new Mammal();
MyAnimal.Run();
Czyli wszystko działa tak, jak powinno. W takim programie kompilator C# wyświetli ostrzeżenie: 'FooConsole.Mammal.Run()' hides inherited member 'FooConsole.Animal.Run()'. Use the new keyword if hiding was intended. Komunikat informuje nas, że w klasie Mammal
zadeklarowaliśmy metodę Run()
, która przykrywa element o tej samie nazwie, uprzednio zadeklarowany w klasie Animal
.
Chociaż z punktu widzenia kompilatora nie jest to błąd (taki kod zostanie skompilowany), dobrą praktyką jest jawne określenie, że wiemy, co robimy, i chcemy przykryć element uprzednio zadeklarowany w klasie bazowej. Służy do tego słowo kluczowe new
, które poznałeś już wcześniej. W tym kontekście to słowo służy jako modyfikator dostępu:
new public void Run()
{
Console.WriteLine("Metoda Run() z klasy Mammal");
}
Za sprawą modyfikatora new komunikat informujący o przykrywaniu metody nie będzie się już więcej pokazywał. Informujemy tym samym kompilator, iż wiemy, że w klasie bazowej istnieje metoda Run()
, ale chcemy zadeklarować metodę o takiej samej nazwie w klasie potomnej.
Takie działanie nie dotyczy jedynie metod klas, ale wszystkich elementów, włączając właściwości i pola.
Elementy statyczne również mogą być przykrywane. Spójrz na poniższy przykład:
class Animal
{
public static int Age = 12;
public static void Run()
{
Console.WriteLine("Metoda Run() z klasy Animal");
}
}
class Mammal : Animal
{
new public static int Age = 100;
new public static void Run()
{
Console.WriteLine("Metoda Run() z klasy Mammal");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Animal.Age);
Console.WriteLine(Mammal.Age);
Console.ReadLine();
}
}
W klasie Animal
zadeklarowałem statyczne pole Age, któremu nadałem wartość 12. W klasie potomnej to pole jest przykrywane i nadawana jest mu nowa wartość. W skutek działania takiego kodu na ekranie konsoli wyświetlone zostanie:
12
100
Dla sprawdzenia zasad działania dziedziczenia możesz skomentować linię odpowiedzialną za przykrywanie elementu Age
:
// new public static int Age = 100;
Teraz uruchom program ponownie. Rezultat działania takiego programu:
12
12
Wiesz, dlaczego tak się stało?
Słowo kluczowe base
Nie wspomniałem o tym przy okazji omawiania dziedziczenia, lecz jest to istotny element związany z tym procesem. Mam tu na myśli możliwość uzyskania dostępu do elementów klasy bazowej. W C# realizujemy to przy pomocy słowa kluczowego base
:
new public void Run()
{
base.Run();
Console.WriteLine("Metoda Run() z klasy Mammal");
}
W tej metodzie najpierw wykonujemy kod metody Run()
z klasy bazowej, a dopiero później dalsze instrukcje z ciała metody. Nie ma tutaj większej filozofii. Dostęp do elementów klasy bazowej uzyskujemy przy pomocy operatora odwołania (.).
Słowo kluczowe base
ma bardziej zaawansowane zastosowanie w połączeniu z konstruktorami klas:
class Animal
{
public string Name;
public Animal()
{
this.Name = "Pet";
}
public Animal(string Name)
{
this.Name = Name;
}
}
class Mammal : Animal
{
public Mammal()
: base()
{
// dodatkowy kod
}
public Mammal(string Name)
: base("Tina")
{
// dodatkowy kod
}
public void Run()
{
Console.WriteLine("Imię zwierzątka to: " + this.Name);
}
}
Zacznijmy od początku. W klasie Animal
utworzyłem dwa konstruktory. Jeden z nich posiada parametr Name
. Taka konstrukcja jest Ci znana z lektury poprzednich fragmentów rozdziału. W klasie potomnej również zadeklarowałem dwa konstruktory. Pierwszy nie posiada parametrów i wywołuje konstruktor klasy bazowej (który również nie posiada parametrów). Drugi konstruktor klasy Mammal
wywołuje konstruktor klasy bazowej z parametrem Tina. Jak myślisz, jaki będzie rezultat działania poniższego kodu?
Mammal MyAnimal = new Mammal("Jack");
MyAnimal.Run();
Na konsoli zostanie wyświetlony tekst: Imię zwierzątka to: Tina
. Dlaczego nie Jack? Ponieważ w konstruktorze klasy Mammal
podajemy parametr. Teraz zwróć uwagę na kod tego konstruktora. Wywołuje on konstruktor bazowy z parametrem Tina. Poprawienie tego kodu wymaga małej poprawki:
public Mammal(string Name)
: base(Name)
{
// dodatkowy kod
}
Teraz do konstruktora bazowego przekazywany jest parametr przekazany do konstruktora klasy Mammal
.
Słowo kluczowe base
nie może być używane w metodach statycznych.
Metody wirtualne
Wyobraź sobie, że w programie tworzysz klasy odpowiadające gatunkom zwierząt. Mamy więc klasę bazową — Animal
. Mamy również klasy pochodne Mammal
(ssaki) oraz Fish
(ryby). W klasie macierzystej mamy metodę Breath()
(oddychaj), która oczywiście jest również dostępna w klasach potomnych, tyle że może być inaczej interpretowana w klasie Mammal
oraz Fish
. Ssaki oddychają przecież płucami, a ryby — skrzelami. Możemy więc w klasie Fish
utworzyć nową metodę Breath()
, która przykrywa oryginalną, zadeklarowaną w klasie bazowej. Możemy również zadeklarować tzw. metodę wirtualną, która w klasach potomnych może być przedefiniowana.
Te dwa pojęcia są ściśle związane z polimorfizmem.
Metoda wirtualna to taka, która jest przygotowana do zastąpienia w klasie potomnej.
Metodę wirtualną tworzymy, dodając do jej deklaracji słowo kluczowe virtual
:
class Animal
{
public int Age;
public virtual void Breath()
{
Console.WriteLine("Zwierzak oddycha...");
}
}
W ten sposób dajemy kompilatorowi do zrozumienia, iż metoda Breath()
może być poddana w klasach potomnych procesowi przedefiniowania.
Metoda statyczna nie może być opatrzona klauzulą virtual
.
Przedefiniowanie metod
Przedefiniowanie (ang. override
) to proces polegający na tworzeniu nowej wersji metody w klasie potomnej. Polega on na utworzeniu metody, która opatrzona będzie słowem kluczowym override
:
class Mammal : Animal
{
public override void Breath()
{
Console.WriteLine("Ssak oddycha płucami...");
}
}
class Fish : Animal
{
public override void Breath()
{
Console.WriteLine("Ryba oddycha skrzelami...");
}
}
W tym przykładzie w klasach potomnych zmieniliśmy znaczenie metody Breath()
. No dobrze, ale czym to się różni od uprzednio zaprezentowanego przykrywania elementów klas? Przyjrzyj się metodzie Main()
, której kod znajduje się na listingu 5.3.
Listing 5.3. Przykład wykorzystania przedefiniowanych metod
static void Main(string[] args)
{
Animal MyAnimal;
Console.WriteLine("1 - twórz obiekt klasy Animal");
Console.WriteLine("2 - twórz obiekt klasy Mammal");
Console.WriteLine("3 - twórz obiekt klasy Fish");
ConsoleKeyInfo Key = Console.ReadKey();
switch (Key.KeyChar)
{
case '1':
MyAnimal = new Animal();
break;
case '2':
MyAnimal = new Mammal();
break;
case '3':
MyAnimal = new Fish();
break;
default:
MyAnimal = new Animal();
break;
}
Console.Clear();
// wywołanie metody
MyAnimal.Breath();
Console.ReadLine();
}
Na samym początku zadeklarowałem zmienną MyAnimal, wskazującą na klasę bazową — Animal
. Następnie na podstawie opcji wybranej przez użytkownika utworzony zostanie odpowiedni obiekt. Na samym końcu wywołana zostanie metoda Breath()
. Dzięki metodom wirtualnym kompilator wie, z której klasy kod ma zostać w danej chwili wywołany.
Możesz sprawdzić działanie takiego kodu, usuwając ze źródła wszelkie słowa kluczowe virtual
oraz override
. Po kompilacji programu możesz zauważyć, że niezależnie od wybranej opcji zostanie wykonana metoda Breath()
z klasy Animal
. W zwyczajnych metodach decyzja, która metoda rzeczywiście ma zostać wywołana, zapada już w trakcie kompilacji programu. Decyduje o tym powiązanie:
Animal MyAnimal;
W tym momencie określamy, iż wszelkie wywołania metody Breath()
będą odnosić się właśnie do tej klasy. Jest to tzw. wczesne powiązanie (ang. early binding).
Jeżeli zastosujemy metody wirtualne, kompilator wstrzyma się z decyzją, do jakiej klasy przypisać daną metodę. Ta decyzja zostanie podjęta dopiero w trakcie działania programu. Jak widzisz, w naszym programie podejmuje ją użytkownik przy pomocy klawiszy 1, 2 lub 3. Takie rozwiązanie nazywamy późnym powiązaniem (ang. late binding).
Oczywiście w metodach przedefiniowanych możemy wywoływać metody bazowe przy pomocy słowa kluczowego base
.
Elementy abstrakcyjne
W poprzednim przykładzie zadeklarowałem klasę bazową Animal
, która zawierała metodę Breath()
. Załóżmy, że w programie nie ma potrzeby używania klasy Animal
— jedynie z klas potomnych. Po co wówczas implementować metody w klasie Animal
? Można opatrzyć deklarację metody słowem kluczowym abstract
. Klasa abstrakcyjna nie ma implementacji (tj. definicji metod), ma jedynie nagłówki (deklaracje):
abstract class Animal
{
public int Age;
public abstract void Breath();
}
Taka konstrukcja wymusza, aby klasy potomne posiadały metodę Breath()
. Klasa Animal
nie może być w takim wypadku używana, służy jedynie jako fundament do budowania kolejnych klas.
*Klasa zawierająca elementy abstrakcyjne również musi być opatrzona klauzulą abstract
.
*Metody abstrakcyjne nie posiadają kodu.
*Nie ma możliwości utworzenia instancji klasy abstrakcyjnej.
*Metody abstrakcyjne nie mogą być opatrzone klauzulą virtual
lub static
.
Elementy zaplombowane
Elementy zaplombowane nie mogą służyć jako klasy bazowe. To jest podstawowe i jedyne zastosowanie tego typu klas. Plombowanie klasy polega na opatrzeniu jej deklaracji słowem kluczowym sealed
:
sealed class Foo {}
Domyślnie wszystkie klasy pisane przez nas powinny mieć możliwość rozszerzenia jej funkcjonalności. Używaj słowa kluczowego sealed
tylko wówczas, gdy masz poważny powód. Np. klasa jest statyczna albo dziedziczy elementy zawierające informacje ważne dla bezpieczeństwa. Wtedy pamiętaj, aby w zaplombowanej klasie nie umieszczać metod wirtualnych ani nie korzystać z modyfikatora protected
, gdyż wtedy nie ma to sensu.
.NET Framework Class Library
Biblioteka klas środowiska .NET Framework (ang. .NET Framework Class Library — FCL) to rozbudowana biblioteka składająca się z setek klas, interfejsów, struktur (o tym w dalszej części książki), które umożliwiają szybkie projektowanie aplikacji. O tej bibliotece wspominałem już niejednokrotnie w tej książce.
Skompilowany kod tej biblioteki podzielony jest na podzespoły (ang. assembly), które z kolei posiadają przestrzenie nazw, a te składają się z klas i innych typów danych. Biblioteka klas udostępnia wiele mechanizmów umożliwiających proste wykorzystanie nawet skomplikowanych zadań, jak np. obsługiwanie plików XML, połączeń internetowych itp.
Pomysł udostępniania programistom gotowych bibliotek klas nie jest nowy. Przed tym, jak na rynku pojawiła się platforma .NET, programiści mogli korzystać z interfejsu WinAPI systemu Windows. WinAPI nie jest jednak biblioteką obiektową — raczej trudnym do obsługi i przestarzałym interfejsem. Bardzo popularna była udostępniona przez firmę Microsoft biblioteka MFC umożliwiająca tworzenie wizualnych aplikacji w środowisku Visual C++. Firma Borland — lider na rynku aplikacji programistycznych — w swoich środowiskach C++ Builder oraz Delphi umieszczała bibliotekę VCL (ang. Visual Class Library), również umożliwiającą proste tworzenie aplikacji wizualnych. Żadna z nich nie była jednak tak rozbudowana i użyteczna jak FCL.
Przestrzenie nazw
Wiesz już, że przestrzenie nazw są podstawowym sposobem organizacji i grupowania klas w .NET Framework. Biblioteka FCL udostępnia kilka podstawowych przestrzeni nazw grupujących klasy:
*System
— podstawowa przestrzeń nazw całej biblioteki. Zawiera podstawowe klasy oraz typy danych (np. Int32
, Int16
, String
itp.).
*System.Windows.Forms
— przestrzeń nazw zawierająca klasy oraz interfejsy służące do projektowania interfejsu graficznego. Zawiera klasy reprezentujące podstawowe kontrolki interakcji z użytkownikiem (przyciski, listy rozwijane, panele itp.) oraz chyba najważniejszą klasę obsługi formularza, czyli System.Windows.Forms.Form
.
*System.Data
— przestrzeń nazw zawierająca klasy obsługi baz danych takie jak MS SQL Server czy Oracle. Możliwa jest także obsługa technologii OLE DB lub ODBC.
*System.XML
— przestrzeń zawiera klasy umożliwiające obsługę plików XML (parsowanie, tworzenie, usuwanie, edycja). Zagadnienia związane z obsługą XML-a w C# omówię w rozdziale 13.
*System.IO
— klasy zawarte w tej przestrzeni nazw służą do obsługi operacji wejścia-wyjścia. Dzięki nim można dodawać do swojej aplikacji obsługę plików, strumieni, katalogów itp. Tym zagadnieniem zajmiemy się w rozdziale 12.
*System.Web
— to jeden z podstawowych komponentów środowiska .NET Framework, czyli ASP.NET. W tej przestrzeni nazw znajdują się klasy służące do obsługi ASP.NET oraz zawierające komponenty typu Web Forms.
*System.Reflection
— przestrzeń nazw zapewniająca obsługę mechanizmu reflection. Nie będę tutaj zagłębiał się w szczegóły, szerzej o technologii reflection opowiem w rozdziale 11.
*System.Net
— w tej przestrzeni nazw znajdują się klasy odpowiedzialne za obsługę różnych protokołów internetowych, takich jak HTTP, DNS czy IP.
*System.Security
— przestrzeń nazw zawierająca mechanizmy zabezpieczeń, klasy implementujące różne algorytmy szyfrujące.
Klasa System.Object
Bazową klasą dla każdego z typów jest System.Object
. Nawet jeżeli nie określimy klasy bazowej naszej klasy, to kompilator automatycznie przyjmie, że jest nią System.Object
. Owa klasa dostarcza podstawowych mechanizmów korzystania z klas — podstawowe metody zostały opisane w tabeli 5.1.
Tabela 5.1. Krótki opis metod używanych w klasie System.Object
`Metoda` | Opis |
---|---|
`Equals` | Porównuje, czy dwie instancje typu object są takie same (mają taką samą zawartość). |
`ReferenceEquals` | Porównuje, czy dwie instancje obiektu są tego samego typu. |
`GetHashCode` | Zwraca unikalny numer instancji obiektu. |
`GetType` | Zwraca informacje na temat obiektu: metody, właściwości itp. |
`ToString` | Znakowa reprezentacja obiektu — zwraca jego typ. |
Listing 5.4 prezentuje przykład wykorzystania metod z klasy System.Object
.
Listing 5.4. Przykład wykorzystania metod klasy System.Object
using System;
namespace FooConsole
{
class Foo
{
public Foo()
{
// pusto
}
}
class Bar
{
public Bar()
{
Foo MyFoo = new Foo();
Console.WriteLine("GetHashCode() " + this.GetHashCode());
Console.WriteLine("GetType() " + this.GetType());
Console.WriteLine("ToString() " + this.ToString());
Console.WriteLine("Equals() " + this.Equals(MyFoo));
Console.WriteLine("ReferenceEquals() " + object.ReferenceEquals(this, MyFoo));
}
}
class Program
{
static void Main(string[] args)
{
Bar MyBar = new Bar();
Console.Read();
}
}
}
Skompiluj i uruchom tak napisany program i sprawdź jego działanie.
Opakowywanie typów
Opakowywanie typów jest specyficzną cechą języków działających w środowisku .NET Framework. Opakowywanie daje możliwość traktowania typów prostych jak obiektów.
Tutaj należą Ci się wyjaśnienia. W C# możemy wyróżnić dwa rodzaje typów — proste oraz referencyjne. Wbudowane typy są prostymi typami przechowywanymi na stosie. Można do nich zaliczyć liczby całkowite, liczby zmiennoprzecinkowe podwójnej precyzji oraz wartości logiczne.
Stos jest tym obszarem pamięci komputera, który jest alokowany (przydzielany) w momencie uruchamiania jakiegoś programu. System operacyjny w tym momencie musi określić, ile pamięci będzie potrzebował do prawidłowego działania programu.
Typy referencyjne są umieszczane na tzw. stercie (ang. heap). Zmienna typu referencyjnego przechowuje jedynie adres wskazujący na rzeczywisty egzemplarz danego typu. Kiedy tworzymy nową instancję danego typu (przy pomocy operatora new
), następuje alokacja (czyli rezerwacja) pamięci potrzebnej na przechowywanie danego obiektu.
Sterta jest obszarem pamięci operacyjnej dostępnej w trakcie działania programu. Kiedy tworzymy nowy obiekt, pamięć dla niego jest alokowana właśnie na stercie.
Konwersja ze zmiennej typu prostego na postać obiektu i konwersja obiektu na zmienną typu prostego w terminologii przyjętej przez programistów aplikacji .NET jest określana odpowiednio jako opakowywanie (ang. boxing) oraz odpakowywanie (ang. unboxing). Poniższy przykład prezentuje domniemane opakowywanie:
int Foo = 100;
object O = Foo;
Console.WriteLine(O);
Jak widzisz, polega to jedynie na przypisaniu wartości zmiennej Foo do zmiennej O typu object
(typ object
jest odpowiednikiem typu System.Object
biblioteki FCL). Nie ma potrzeby określania, że opakowujemy wartość typu int
, ponieważ kompilator jest w stanie się tego „domyślić”. Możliwe jest jednak sprecyzowane opakowywanie polegające na podaniu typu danych:
int Foo = 100;
object O = (object)Foo;
Odwrotna sytuacja, czyli odpakowywanie, jest równie proste:
int Bar = (int)O;
Interfejsy
Interfejsy swoim działaniem przypominają klasy abstrakcyjne. Mogą zawierać jedynie deklaracje metod oraz właściwości. Interfejsy deklaruje się z użyciem słowa kluczowego interface
:
interface IFoo
{
int Power(int X, int Y);
}
Jak widzisz, w interfejsie o nazwie IFoo
znajduje się metoda Power()
. W takim wypadku mówimy, że interfejs definiuje dane metody. Zwróć uwagę, że w interfejsie znajduje się nagłówek metody pozbawiony ciała (czyli kodu).
Interfejsy są kolejnym elementem używanym w metodologii zwanej programowaniem obiektowym, często wykorzystywanym w bibliotece FCL. Klasy języka C# mogą dziedziczyć jedynie elementy pojedynczej klasy. Jak sama nazwa wskazuje, interfejs definiuje wyłącznie mechanizm pośredniczący w komunikacji klienta z obiektem. Za obsługę interfejsu i odpowiednią implementację każdej z jego funkcji odpowiada klasa.
Dana klasa może jednakże dziedziczyć wiele interfejsów. Ponieważ interfejs nie może posiadać kodu metod, mówimy wówczas, iż klasa implementuje elementy interfejsu. Pewnie zastanawiasz się jakie zalety niesie za sobą wykorzystanie interfejsów. Służą one przede wszystkim do wymuszania deklaracji danych elementów w klasie. Spójrz na poniższy kod:
interface IFoo
{
int Power(int X, int Y);
}
class Foo : IFoo
{
}
Mamy tutaj interfejs IFoo
oraz klasę Foo
, która go implementuje. Próba kompilacji takiego programu zakończy się komunikatem: 'FooConsole.Foo' does not implement interface member 'FooConsole.IFoo.Power(int, int)'. Kompilator próbuje nam powiedzieć, że w klasie Foo
nie znajduje implementacji elementu Power
.
Implementowana metoda musi posiadać taki sam nagłówek jak ta zdefiniowana w interfejsie. Nie może to być metoda statyczna:
class Foo : IFoo
{
public int Power(int X, int Y)
{
return X * Y;
}
}
Interfejsy nie mogą definiować pól.
Dobrą praktyką jest specyficzne nazewnictwo interfejsów polegające na dodaniu przed nazwą litery I.
Interfejsy są często wykorzystywane w bibliotece klas FCL. Przykładowo, klasa System.String
(o której szczegółowo będzie mowa w rozdziale 9.) implementuje wiele interfejsów, w tym IConvertible
. Ten z kolei definiuje metody używane przy konwersji danych.
Implementacja wielu interfejsów
Jak wspomniałem, dana klasa może implementować wiele interfejsów. W takim przypadku ich nazwy należy rozdzielić znakiem przecinka:
interface IFoo
{
int Power(int X, int Y);
}
interface IBar
{
double Power(double X, double Y);
}
class Foo : IFoo, IBar
{
public int Power(int X, int Y)
{
return X * Y;
}
public double Power(double X, double Y)
{
return X * Y;
}
}
Istnieje możliwość, aby obydwa interfejsy definiowały metodę o tej samej nazwie. Wówczas inna jest implementacja takich metod:
interface IFoo
{
void Foo();
}
interface IBar
{
void Foo();
}
class Foo : IFoo, IBar
{
void IFoo.Foo()
{
Console.WriteLine("Metoda implementująca IFoo.Foo");
}
void IBar.Foo()
{
Console.WriteLine("Metoda implementująca IBar.Foo");
}
}
Zwróć uwagę na brak modyfikatorów dostępu takich implementowanych metod. Takie rozwiązania umożliwia dostęp do metody IFoo.Foo()
tylko poprzez interfejs IFoo
:
Foo obj = new Foo();
IFoo MyFoo = (IFoo)obj;
MyFoo.Foo();
Podejrzewam jednak, że takiego rozwiązania (podobnie jak i całego mechanizmu interfejsów) nie będziesz często stosował w swoich projektach.
Podsumowując:
*Interfejsy definiują metody oraz właściwości.
*Interfejsy nie mogą zawierać pól.
*Interfejsy mogą dziedziczyć po sobie podobnie jak klasy.
*Wszystkie elementy interfejsu są domyślnie traktowane jako publiczne.
*Interfejs nie może zawierać elementów statycznych.
Typy wyliczeniowe
Umożliwiają one tworzenie nowych typów danych zawierających zbiór elementów tego samego typu. Jeżeli programowałeś wcześniej w języku Delphi, być może znane są Ci zbiory (ang. set). Typy wyliczeniowe (nazywane często po prostu wyliczeniami) są podobnym mechanizmem jak zbiory z języka Delphi.
Typy wyliczeniowe deklarujemy z użyciem słowa kluczowego enum
:
enum Days { Pn, Wt, Śr, Czw, Pt, So, Nd };
W nawiasach klamrowych musimy zapisać listę elementów, które będą znajdować się w naszym zbiorze. Listę elementów należy oddzielić znakiem przecinka.
Nie ma znaczenia, w jaki sposób zapisana jest deklaracja typu wyliczeniowego. Wielu programistów, aby zwiększyć czytelność, zapisuje elementy w ten sposób:
enum Days
{
Pn,
Wt,
Śr,
Czw,
Pt,
So,
Nd
};
Użycie typów wyliczeniowych nie wymaga od programisty utworzenia nowej instancji takiego typu czy deklarowania zmiennej wskazującej na nowy typ. Odwołanie do konkretnego elementu możliwe jest przy użyciu operatora odwołania:
Console.WriteLine(Days.Pn);
Taki kod zwyczajnie wyświetli na konsoli napis Pn.
Wartości elementów
Typy wyliczeniowe są prostym mechanizmem zapewniającym grupowanie danych. Ilość elementów wyliczenia jest stała, nie podlega zmianom. Jedyna rzecz, którą możemy ustalić w trakcie projektowania aplikacji, to ich wartość. Otóż domyślnie pierwszy element wyliczenia ma wartość równą 0, drugi 1 itd.
Możemy to prosto sprawdzić, dokonując rzutowania:
Console.WriteLine((int)Days.Pn);
Możliwe jest jawne nadanie numeru dla elementu:
enum Days
{
Pn = 10,
Wt,
Śr,
Czw = 50,
Pt,
So,
Nd
};
Teraz element Pn ma wartość 10, Wt 11 itd. Element Czw posiada wartość równą 50, Pt 51.
Pamiętaj, że na elementach wyliczenia nie możesz dokonywać żadnych działań arytmetycznych (dodawanie, odejmowanie). Najpierw konieczne jest rzutowanie na typ całkowity.
Jeżeli zakres dostępny dla typu int nie wystarcza, aby nadać wartość dla elementu wyliczenia, można jawnie określić typ:
enum Foo : long { Min = -1200034676867853, Max = 564555666 };
Pamiętaj o tym, aby dokonać właściwego rzutowania, jeżeli chcesz odczytać wartość elementów wyliczenia:
Console.WriteLine("Min: " + (long)Foo.Min);
Console.WriteLine("Max: " + (long)Foo.Max);
Struktury
Również struktury odgrywają ważną rolę w środowisku .NET Framework. Zawsze lubiłem myśleć o strukturach jako uproszczonych klasach, gdyż są one pozbawione niektórych zaawansowanych cech, takich jak dziedziczenie czy polimorfizm.
Struktury są zorganizowanym zbiorem danych, niekoniecznie tego samego typu. Podobnie jak klasy mogą posiadać pola, metody a nawet konstruktor.
Struktury deklarujemy z użyciem słowa kluczowego struct
:
public struct Contact
{
string Name;
long Phone;
byte Age;
}
W tym momencie zadeklarowałem strukturę składającą się z pól: Name (nazwa), Phone (telefon) oraz Age (wiek), do których mogę przypisywać dane kontaktowe.
No dobrze, ale na razie nie ma różnicy pomiędzy utworzeniem klasy o nazwie Contact
, a strukturą zawierającą te same pola. Masz rację. Struktury są bardzo podobne do klas, ich tworzenie oraz wykorzystanie jest praktycznie identyczne.
Wyobraź sobie, że piszesz aplikację służącą do przechowywania informacji kontaktowych. Może to być książka adresowa. Załóżmy również, że posiadasz w książce 100 kontaktów, co jest bardzo prawdopodobne. Do przechowywania kontaktów wykorzystujesz klasy. Aby odczytać dane kontaktowe i przypisać je do klasy, musisz utworzyć 100 instancji klasy! Wpłynie to na wydajność programu, nie mówiąc już o zużyciu pamięci.
Użycie struktur nie wymaga tworzenia obiektu, a jedynie zmiennej wskazującej na strukturę:
Contact c1, c2;
c1.Name = "Jan Kowalski";
c1.Age = 23;
c1.Phone = "601-000-111";
c2.Name = "Piotr Nowak";
c2.Age = 43;
c2.Phone = "501-111-222";
Jak widzisz, w kodzie zadeklarowałem dwie zmienne wskazujące na strukturę, po czym wypełniłem je danymi. Nie było koniecznie użycie przy tym operatora new
.
W rzeczywistości programista, który musi przechować w pamięci dane kontaktowe 100 osób, nie będzie deklarował setki zmiennych wskazujących na strukturę Contact
. Języki programowania oferują inne narzędzia, takie jak np. tablice, o których będzie mowa w rozdziale 7.
Listing 5.5. prezentuje prosty program umożliwiający tworzenie oraz wykorzystanie struktur.
Listing 5.5. Program wykorzystujący struktury
using System;
namespace FooApp
{
public struct Contact
{
public string Name;
public string Phone;
public byte Age;
}
class Program
{
static void ShowInfo(Contact c)
{
Console.WriteLine("Witaj " + c.Name + ", wiem że masz " +
c.Age + " lat, a Twój numer telefonu to: " + c.Phone);
}
static void Main(string[] args)
{
Contact c1, c2;
c1.Name = "Jan Kowalski";
c1.Age = 23;
c1.Phone = "601-000-111";
c2.Name = "Piotr Nowak";
c2.Age = 43;
c2.Phone = "501-111-222";
ShowInfo(c2);
Console.Read();
}
}
}
Najważniejsze w tym banalnym programie jest to, że strukturę możemy przekazać jako parametr metody, tak jak to uczyniłem, deklarując metodę ShowInfo()
. Wyobraź sobie sytuację, w której metoda miałaby tyle parametrów, ile elementów ma struktura Contact
:
ShowInfo(string Name, string Phone, byte Age);
Struktura Contact
posiada jedynie 3 parametry, ale nie trudno sobie wyobrazić sytuację, w której ta liczba jest większa. Taki kod nie wyglądałby zbyt efektownie, dlatego wiele parametrów można zastąpić jednym, który wskazuje na strukturę.
Konstruktory struktur
Sposób deklarowania konstruktorów w strukturach jest dość ciekawy, dlatego warto o nim wspomnieć. Pierwsza uwaga: w strukturach nie można deklarować konstruktorów, które nie posiadają żadnych parametrów!
Zadeklarowany konstruktor musi służyć do przypisywania danych polom struktury:
public struct Contact
{
public string Name;
public string Phone;
public byte Age;
public Contact(string FName, string FPhone, byte FAge)
{
Name = FName;
Phone = FPhone;
Age = FAge;
}
}
Z ciekawości możesz usunąć kod z ciała konstruktora. Próba kompilacji nie powiedzie się, kompilator zasygnalizuje błąd: Field 'FooApp.Contact.Age' must be fully assigned before control leaves the constructor.
Konstruktor struktury jest wywoływany przy pomocy operatora new
, podobnie jak w przypadku klas. Listing 5.6 zawiera zmodyfikowany kod z listingu 5.5 i prezentuje sposób przypisania danych do struktury za pomocą konstruktora.
Listing 5.6. Przykład wykorzystania struktur
using System;
namespace FooApp
{
public struct Contact
{
public string Name;
public string Phone;
public byte Age;
public Contact(string FName, string FPhone, byte FAge)
{
Name = FName;
Phone = FPhone;
Age = FAge;
}
}
class Program
{
static void ShowInfo(Contact c)
{
Console.WriteLine("Witaj " + c.Name + ", wiem że masz " +
c.Age + " lat, a Twój numer telefonu to: " + c.Phone);
}
static void Main(string[] args)
{
Contact c1, c2, c3;
c1.Age = 23;
c1.Phone = "601-000-111";
c2.Name = "Piotr Nowak";
c2.Age = 43;
c2.Phone = "501-111-222";
c3 = new Contact("Janusz Piotrkowski", "654-435-345", 12);
ShowInfo(c3);
Console.Read();
}
}
}
Podsumujmy informacje o strukturach:
*Struktury, podobnie jak klasy, mogą zawierać pola, metody i właściwości.
*Struktury mogą implementować interfejsy.
*Struktury nie mogą dziedziczyć z innych klas, aczkolwiek dziedziczą metody z klasy bazowej System.Object
.
*Struktury nie mogą posiadać metod wirtualnych.
*Struktury nie mogą posiadać elementów statycznych.
*Utworzenie nowej instancji struktury nie wymaga użycia operatora new.
Operatory is i as
Istnieją dwa operatory, is
i as
, które są stosowane w połączeniu z klasami. Programiści rzadko z nich korzystają, jednak warto poświęcić im nieco uwagi.
Zacznijmy od prostego przykładu. Wyobraź sobie, że mamy kilka komponentów tego samego typu — np. TextBox
. Chcemy oprogramować zdarzenie KeyPress
każdego z nich. Oczywiście, aby ułatwić sobie pracę, generujemy zdarzenie jednego komponentu, a pozostałym przypisujemy tę samą procedurę zdarzeniową.
Zwróć uwagę, że wygenerowana procedura zdarzeniowa posiada dwa parametry:
private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
{
}
Ważnym parametrem jest sender
, który wskazuje na typ object
. Parametr ten umożliwia programiście kontrolę oraz daje informację, z jakiego komponentu pochodzi zdarzenie. Teraz przy pomocy operatora is możemy sprawdzić typ komponentu, z którego pochodzi zdarzenie:
if (sender is TextBox) { }
W przypadku gdy zdarzenie pochodzi z komponentu typu TextBox
(wskazuje na to parametr sender), instrukcja if zostanie spełniona. Operator is
działa podobnie jak porównanie za pomocą ==. Niekiedy jednak nie można użyć operatora ==:
if (sender == TextBox) { }
Powyższy kod spowoduje wyświetlenie komunikatu o błędzie: 'System.Windows.Forms.TextBox' is a 'type' but is used like a 'variable'.
Operator as
natomiast służy do tzw. konwersji. Nie chodzi tutaj o konwersję typów, którą omawiałem poprzednio.
Powróćmy do przykładu. Umieściłem na formularzu trzy komponenty typu TextBox
i oprogramowałem zdarzenie KeyPress
, które przypisałem każdej kontrolce. Chciałbym zmienić jakąś właściwość jednego komponentu typu TextBox
, a to jest możliwe dzięki operatorowi as
:
private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
{
if (sender is TextBox)
{
(sender as TextBox).Text = " ";
}
richTextBox1.Text += "Naciśnięto klawisz " + e.KeyChar + ", a zdarzenie pochodzi z kontrolki " + (sender as TextBox).Name + "\n";
}
Po uruchomieniu programu i naciśnięciu klawisza w momencie, gdy komponent TextBox
jest aktywny, zostanie wywołane zdarzenie KeyPress, co spowoduje czyszczenie właściwości Text tegoż komponentu.
Jak widać, dzięki takiemu zabiegowi możliwa jest zmiana właściwości takiego komponentu, nawet jeśli nie znamy jego nazwy, a jedynie typ (rysunek 5.5).
Rysunek 5.5. Przykład wykorzystania operatorów is oraz
Przeładowanie operatorów
Mechanizm przeładowania operatorów (lub inaczej — przeciążania operatorów) był znany już wcześniej, m.in. programistom C++, teraz został wprowadzony również na platformie .NET.
Ogólnie można powiedzieć, że dzięki przeładowaniu operatorów można dokonywać różnych działań na obiektach klas (mnożenie, dodawanie, dzielenie itp.) tak samo jak na zmiennych, tym samym upraszczając nieco zapis kodu. Wygląda to tak: stosując operator (np. +) w obiektach klas, w rzeczywistości wywołujemy odpowiednią metodę klasy. Od projektanta zależy, jaki kod będzie miała owa funkcja. Może to wyglądać np. tak:
Foo MyFoo = new Foo();
Foo MyBar = new Foo();
MyFoo = 10;
if (MyFoo != MyBar)
{
}
Na początku ten mechanizm może wydawać się trochę dziwny, lecz osoby mające wcześniej styczność np. z C++ nie powinny mieć problemu z jego zrozumieniem.
Słowo kluczowe operator
Przeładowanie operatora polega na utworzeniu w klasie metody, określenie jej słowem kluczowym operator
oraz symbolem operatora, który ma zostać przeładowany. Dla zobrazowania tego problemu przeładujmy operator ++ w przykładowej klasie Foo
:
class Foo
{
private int X;
public Foo(int X)
{
this.X = X;
}
public int GetValue
{
get
{
return (this.X);
}
}
public static Foo operator ++(Foo FooObj)
{
++FooObj.X;
return FooObj;
}
}
Klasa jest banalna i właściwie nie spełnia żadnego określonego działania. Liczba podana w parametrze klasy zostaje przypisana do pola X. Nie to jest jednak ważne. Najważniejsza dla nas jest statyczna metoda na samym końcu tego kodu. Nie posiada nazwy, a jej składnia jest dość specyficzna:
<modyfikatory dostępu> <typ zwrotny="zwrotny"> operator <symbol operatora="operatora">(<parametry>)
{
}
Oto przykład wykorzystania klasy:
Foo MyFoo = new Foo(5);
// inkrementacja
MyFoo++;
Console.WriteLine(MyFoo.GetValue); // rezultat: 6
W kodzie inkrementujemy wartość zmiennej MyFoo. W rzeczywistości wywoływana zostaje wówczas odpowiednia metoda, która obsługuje przeładowanie tego operatora. Kod metody może być dowolny — np.:
public static Foo operator ++(Foo FooObj)
{
FooObj.X += 2;
return FooObj;
}
W parametrze tej metody przekazywana jest instancja klasy Foo
. Do wartości pola X tej instancji dodajemy cyfrę 2. Następnie zwracamy instancję. Ponieważ tematyka przeładowania operatorów może nie być zbyt klarowna na samym początku, rozbudujmy nasz program i przeładujmy kolejny operator +. Oto rozwiązanie:
public static Foo operator +(Foo Obj1, Foo Obj2)
{
return new Foo(Obj1.X + Obj2.X);
}
Jak widzisz, tutaj wymagane są dwa parametry, gdyż dodajemy do sobie wartości dwóch obiektów:
MyFoo = Foo1 + Foo2;
Zakładając, że wszystkie obiekty są typu Foo
, musimy obsłużyć proces przypisywania danych do obiektu:
public static implicit operator Foo(int Obj1)
{
return new Foo(Obj1);
}
Jak widzisz, konstrukcja obsługi procesu przypisania różni się nieco od przedstawionych poprzednio. Oto kod wykorzystujący przeładowane operatory:
Foo MyFoo, Foo1, Foo2 = new Foo(5);
Foo1 = 10;
MyFoo = Foo1 + Foo2;
Console.WriteLine(MyFoo.GetValue); // rezultat: 15
W języku C# nie można przeładować operatorów &&, || i new.
Użycie słowa kluczowego implict
pozwala uniknąć niepotrzebnego rzutowania typów.
Przykład:
public static implicit operator int(Foo Obj1)
{
return Obj1.X;
}
// ...
Foo MyFoo = new Foo(10);
int i = 20;
i = MyFoo;
W tym przykładzie do zmiennej MyFoo przypisaliśmy wartość zmiennej i. Przeciwieństwem słowa kluczowego implict
jest explict
:
public static explicit operator int(Foo Obj1)
Po tej kosmetycznej poprawce kod przedstawiony powyżej nie ma prawa się skompilować. Wyświetlony zostanie błąd: Cannot implicitly convert type 'FooApp.Foo' to 'int'. An explicit conversion exists (are you missing a cast?). Trzeba wówczas użyć rzutowania:
i = (int)MyFoo;
Dzielenie klas
Gdy na początku rozdziału prezentowałem kod źródłowy formularza, który generowany jest automatycznie przez środowisko Visual C# Express Edition, mogłeś zauważyć, że stosowane jest tam słowo kluczowe partial
. W przypadku dużych projektów możesz podzielić kod klasy na kilka różnych modułów. Wówczas konieczne jest użycie słowa kluczowego partial
:
partial class Foo
{
public Foo()
{
}
}
partial class Foo
{
public void Bar()
{
}
}
Taki kod w trakcie kompilacji jest łączony w jedną klasę. Takie rozwiązanie jest stosowne w projektach typu Windows Forms. Np. kod, który odpowiada za tworzenie formularza oraz komponentów, umieszczony jest w osobnym module *.designer.cs, co znacznie ułatwia pracę nad projektem.
Podsumowanie
To był bardzo ważny okres nauki języka C#. Zagadnienia związane z podstawami programowania obiektowego są bardzo ważne, a to dlatego, że język C# jest w pełni obiektowy. Jednocześnie zdaję sobie sprawę, że tematyka programowania obiektowego jest trudna do zrozumienia dla początkujących programistów i musi minąć trochę czasu, aby została w pełni zrozumiana.
Świetny materiał, wielkie dzięki ;)
Dzięki wielkie dla autora - cholernie przydatny materiał.
8-O
Adam nie wrzucaj takich dobrych tutoriali bo odbierasz klientów innym ;)