Klasa to nic innego jak zestaw danych oraz metod umożliwiających ich przetwarzanie. Kod klasy możemy podzielić tak, aby pewne fragmenty były udostępnione publicznie, a inne zostały ukryte. Mechanizm ten nazywamy hermetyzacją. Przeanalizujmy prosty przykład klasy zapewniającej identyfikatory.
public class Identifier
{
private int currentID = 0;
public int GetNextID()
{
return currentID += 1;
}
}
Pierwsze słowo kluczowe public
jest modyfikatorem, określającym wspomniany poziom dostępności klasy. Wskazanie modyfikatora jest opcjonalne. W sytuacji, gdy nie zostanie podany, nadawany jest najbardziej rygorystyczny. W tym przypadku będzie to internal
. Klasa wewnętrzna internal
dostępna jest wyłącznie wewnątrz projektu. Należy zaznaczyć, że istnieje możliwość zagnieżdżenia klasy w klasie, przez co dostępny jest modyfikator private
. Użycie klasy prywatnej możliwe jest wyłącznie z poziomu klasy, w której znajduje się jej definicja. Osobiście uważam, że powinno się zawsze określać poziom dostępności. Pozwala to uniknąć sytuacji, w których osoba analizująca kod zastanawia się, czy jest to zabieg celowy, czy efekt pomyłki.
Słowo kluczowe class
informuje, że mamy doczynienia z definicją klasy. Następnie określana zostaje nazwa klasy, w naszym przykładzie jest to Identifier. Modyfikator klasy Identifier został określony jako public
. Nie oznacza to, że wszystkie elementy klasy są publiczne. Dostępność pola currentID została określona jako private
, przez co zmiana wartości pola możliwa jest wyłącznie wewnątrz klasy Identifier. Określenie poziomu dostępności pól oraz pozostałych składowych klasy jest opcjonalne. W przypadku nie podania, składowe określane są jako private
.
Zdefiniowana klasa Identifier posiada publiczną metodę GetNextID, której zadaniem jest zwrócenie następnego identyfikatora. Metoda to fragment kodu o określonej nazwie, który opcjonalnie może przyjmować zestaw parametrów oraz w razie potrzeby zwracać wynik. Metody będące składowymi klasy również posiadają modyfikator dostępności. Podobnie jak w języku C, metoda nie musi zwracać wartości, sygnalizuje to słowo kluczowe void
umieszczone przed nazwą. W celu zwrócenia wartości, nazwę metody musi poprzedzać zwracany typ, a wewnątrz musi zostać użyte słowo kluczowe return
, zapewniające wyjście z metody wraz ze zwróceniem wartości. Przejdźmy do kodu głównej klasy Main, który obrazuje użycie wcześniej przygotowanej klasy.
class Program
{
static void Main(string[] args)
{
Identifier i0 = new Identifier();
Console.WriteLine("i0 = {0}", i0.GetNextID());
Console.WriteLine("i0 = {0}", i0.GetNextID());
Console.WriteLine("i0 = {0}", i0.GetNextID());
Identifier j1 = new Identifier();
Console.WriteLine("j1 = {0}", j1.GetNextID());
Console.ReadLine();
}
}
Słowo kluczowe new
tworzy egzemplarz klasy. W przykładzie utworzone zostały dwa egzemplarze klasy. Kilkukrotne wywołanie metody GetNextID na jednym z egzemplarzy klasy nie wpływa na wartość zwracanego identyfikatora przez inny egzemplarz. A co jeśli nie chcemy wiązać wartości z konkretnym egzemplarzem klasy? W takiej sytuacji należy użyć słowa kluczowego static
. Dzięki temu składowa klasy nie będzie związana z żadną konkretną instancją klasy. Klasa Identifier została rozszerzona o prywatne statyczne pole totalGetID, którego zadaniem jest zliczenie łącznej ilości pobranych identyfikatorów. Wartość pola możemy odczytać poprzez publiczną statyczną właściwość TotalGetID. Właściwość nie jest związana z żadną instancją klasy, a dostęp do niej odbywa się poprzez definicję klasy. Należy zaznaczyć, że dostęp do statycznego pola totalGetID wewnątrz klasy nie różni się niczym od pola niestatycznego. Z drugiej strony próba użycia niestatycznej składowej klasy wewnątrz np. statycznej właściwości lub metody zakończy się błędem na poziomie kompilacji kodu.
public class Identifier
{
private static int totalGetID;
private int currentID;
public int GetNextID()
{
TotalGetID += 1;
return currentID += 1;
}
public static int TotalGetID
{
get { return totalGetID; }
}
}
Dla potwierdzenia, że statyczna właściwość TotalGetID nie jest powiązana z żadną konkretną instancją, zachęcam do uruchomienia przykładu i zweryfikowania otrzymanej wartości. Właściwości są bardziej podobne do metod niż do pól, chociaż odwołanie do właściwości wygląda identycznie jak odwołanie do pola. Właściwości zazwyczaj udostępniają zestaw metod, jedną do odczytu, a drugą umożliwiającą ustawienie wartości. Każda z nich może zostać ocechowana innym modyfikatorem. Dzięki temu możemy pozwolić na odczyt wartości z zewnątrz, ale jej ustawienie będzie możliwe wyłącznie z poziomu klasy. Dodatkowo wewnątrz akcesora set
możemy korzystać ze słowa kluczowego value
, które reprezentuje wartość przekazaną do właściwości. Prezentuje to kod poniżej.
public static int TotalGetID
{
get { return totalGetID; }
private set { totalGetID = value; }
}
Nasuwa się pytanie, dlaczego nie udostępnić pola na zewnątrz, skoro efekt będzie podobny? Nie należy tego robić z kilku powodów. Przede wszystkim udostępniona zostanie na zewnątrz wartość, która może zostać zmieniona bez wiedzy obiektu, co może doprowadzić do późniejszych problemów. Nietrudno wyobrazić sobie sytuację, w której chcemy umożliwić odczytanie wartości, ale nie chcemy dopuścić do jej ustawienia z zewnątrz. Oprócz tego utracilibyśmy możliwość użycia interfejsów. Dokładny opis zastosowania interfejsów znajdziesz [tutaj](/DevLog/Post/Interfejsy). Kolejnym powodem jest fakt, że część mechanizmów wiązania danych z interfejsem użytkownika wymaga użycia właściwości. Najprościej mówiąc, jest to zły pomysł.
Innym ciekawym aspektem właściwości jest możliwość korzystania ze składni automatycznej. Wyobraźmy sobie sytuację, w której chcemy określić publiczny dostęp do wartości, a jednocześnie chcemy zablokować możliwość modyfikacji z zewnątrz. W dodatku nie mamy potrzeby definiowania prywatnego pola. W takiej sytuacji nie musimy go tworzyć. Niejawnie zrobi to za nas kompilator, a nasz kod będzie treściwy i czytelny. Spróbuj dostosować wcześniejszy kod, tak aby pozbyć się statycznego pola totalGetID i przestawić się na właściwość TotalGetID.
public static int TotalGetID
{
get; private set;
}
Kolejnym wspomnianym już elementem klasy są pola. Oprócz określenia modyfikatora oraz dostępności spoza instancji obiektu, możemy oznaczyć niezmienność wartości w trakcie działania programu poprzez dodanie const
. Słowo kluczowe const
zostało opisane przy okazji wpisu "Zmienne języka C#" [tutaj](/DevLog/Post/ZmienneJezykaCSharp#const). W razie potrzeby doczytaj. Dodatkowo pole możemy oznaczyć jako readonly
. Różnice pomiędzy const
a readonly
są istotne. Wartość pola const
musi zostać określona na poziomie kompilacji. Wartość readonly
może zostać określona w trakcie tworzenia egzemplarza klasy, a później już nie. Dzięki temu jest bardziej elastyczna. Jednocześnie należy pamiętać o sytuacjach, w których możliwe jest użycie stałych np. sekcja case
w instrukcji switch
, a w których niemożliwe jest skorzystanie z pola tylko do odczytu.
Zastosujmy w praktyce poznaną wiedzę dotyczącą składowych klasy i przekształćmy przykład. Do klasy Identifier dodajmy pole tylko do odczytu o nazwie instanceSeqNo, w którym przechowywany będzie numer instancji. Numer instacji powinien być dostępny na zewnątrz poprzez akcesora get
właściwości InstanceSeqNo. Dodatkowo potrzebujemy statycznego pola totalInstance, którego zadaniem będzie zliczanie wszystkich instancji. Zgodnie z tym, co przeczytałeś, inicjalizacja wartości pola tylko do odczytu musi zostać wykonana podczas tworzenia egzemplarza klasy, czyli wewnątrz konstruktora.
class Identifier
{
private int currentID;
private static int totalInstance;
private readonly int instanceSeqNo;
public Identifier()
{
instanceSeqNo = totalInstance++;
}
public int GetNextID()
{
TotalGetID += 1;
return currentID += 1;
}
public int InstanceSeqNo
{
get { return instanceSeqNo; }
}
public static int TotalGetID
{
get; private set;
}
}
Konstruktor klasy jest swego rodzaju specjalną metodą, której zadaniem jest przygotowanie obiektu do działania. Definicję konstruktora rozpoczyna się od określenia modyfikatora (public, internal lub private), po którym następuje nazwa klasy. Następnie, identycznie jak w przypadku metody, w nawiasach określamy listę parametrów. W naszym przykładzie klasa Identifier posiada publiczny konstruktor niewymagający parametrów. Konstruktor tego typu nazywany jest domyślnym. W sytuacji, gdy nie zdefiniujemy konstruktora domyślnego, zrobi to za nas niejawnie kompilator. Najczęściej zadania konstruktora ograniczają się do skopiowania wartości argumentów do pól. Nic nie stoi na przeszkodzie, aby wewnątrz konstruktora używać składowych klasy, np. metod. Klasa może posiadać kilka konstruktorów, warunek to zachowanie unikalności parametrów, tak aby operator new
wiedział, którego użyć. Załóżmy, że potrzebujemy dodać kolejny konstruktor, którego zadaniem będzie ustawienie wartości początkowej dla pola currentID. W przykładzie poniżej zwróćmy uwagę na wyrażenie this() po zapisie dwukropka, które wymusza wywołanie konstruktora domyślnego. W ten sposób możemy sterować przejściem przez kolejne konstruktory. Należy zauważyć, że używając konstruktora z parametrem initialValueID, w pierwszej kolejności wyrażenie this() uruchomi konstruktor domyślny. Dopiero później wykonany zostanie kod konstruktora z parametrem.
public Identifier(int initialValueID) : this()
{
currentID = initialValueID++;
}
Oprócz wymienionych składowych wewnątrz klasy możemy zadeklarować indeksatory, które są właściwościami pobierającymi argument, czy wskazać na interfejsy określające strukturę klasy. Kolejnym elementem są operatory, które umożliwiają określenie zachowania związanego z użyciem operatora. Do opisania pozostają również zdarzenia, pozwalające na generowanie powiadomień przekazywanych do obiektów nasłuchujących. Wszystkie te wymienione mechanizmy oraz rozszerzenie już wstępnie opisanych znajdą się w notatce "Obiektowości ciąg dalszy".
Należy wspomnieć o konwencji nazewnictwa klas. Pierwszy znak nazwy klasy powinien być zapisany wielką literą. Jeżeli nazwa klasy składa się z kilku słów, każde kolejne słowo powinno rozpoczynać się od wielkiej litery. Dodatkowo nie powinniśmy stosować podkreślenia. Opisana konwencja określana jest jako PascalCasing, a jej zasady dotyczą: klas, metod, właściwości oraz publicznych pól. Więcej informacji dotyczących konwencji nazewnictwa znajdziesz tutaj.
Poniżej zamieszczam kod klasy, którą udało się przygotować na podstawie przekazanych informacji.
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Identifier i0 = new Identifier(10);
Console.WriteLine("i0 = {0}", i0.GetNextID());
Console.WriteLine("i0 = {0}", i0.GetNextID());
Console.WriteLine("i0 = {0}", i0.GetNextID());
Console.WriteLine("i0 InstanceSeqNo = {0}", i0.InstanceSeqNo);
Identifier j1 = new Identifier();
Console.WriteLine("j1 = {0}", j1.GetNextID());
Console.WriteLine("j1 InstanceSeqNo = {0}", j1.InstanceSeqNo);
Console.WriteLine("TotalGetID = {0}", Identifier.TotalGetID);
Console.ReadLine();
}
}
class Identifier
{
private int currentID;
private static int totalInstance;
private readonly int instanceSeqNo;
public Identifier()
{
instanceSeqNo = totalInstance++;
}
public Identifier(int initialValueID) : this()
{
currentID = initialValueID++;
}
public int GetNextID()
{
TotalGetID += 1;
return currentID += 1;
}
public int InstanceSeqNo
{
get { return instanceSeqNo; }
}
public static int TotalGetID
{
get; private set;
}
}
}
Na koniec najważniejsze. Tworząc instancje klasy, tworzymy obiekty typu referencyjnego. Oznacza to, że zmienna nie przechowuje wartości, lecz referencję. W przypadku typów wartościowych, np. int
, przypisanie wartości zmiennej x do y spowoduje skopiowanie wartości. Modyfikacja wartości zmiennej x nie wpływa na wartość zmiennej y. Wykonanie tej samej operacji dla typu referencyjnego będzie skutkowało skopiowaniem referencji do obiektu. Spójrzmy na przykład poniżej.
class Program
{
static void Main(string[] args)
{
int x = 7;
int y = x;
x = x + 2;
Console.WriteLine("x = {0}, y = {1}", x, y);
Identifier i0 = new Identifier(10);
Console.WriteLine("i0 = {0}", i0.GetNextID());
Console.WriteLine("i0 = {0}", i0.GetNextID());
Identifier j1 = i0;
Console.WriteLine("j1 = {0}", j1.GetNextID());
Console.WriteLine("i0 = {0}", i0.GetNextID());
Console.ReadLine();
}
}
Troska Robert