Deklaracje

Każda deklarowana zmienna lub funkcja jest składową jakiegoś typu (klasy, struktury). Bezpośrednio w przestrzeni nazw możemy deklarować tylko typy. Kolejność deklaracji nie ma znaczenia.

Każdy identyfikator jest słowem złożonym ze znaków Unicode, zaczynającym się od litery lub znaku podkreślenia. Brana jest pod uwagę wielkość liter. Zasięg identyfikatora rozciąga się do końca bieżącego bloku programu.


Rodzaje typów:

W C# wszystkie typy (predefiniowane oraz stworzone przez programistę) należą do jednej z trzech kategorii :

Bezpośrednie : (value type) są strukturami przechowywanymi (na stosie) jako wartości. Do tej kategorii należą takie typy jak np. int, bool, float, char ... oraz struktury. Jeśli typ bezpośredni jest składową większego obiektu (klasy, tablicy), to jest przechowywany bezpośrednio jako część tego obiektu.

Referencyjne : (reference type) ich wartości są przechowywane na stercie (w pamięci rezerwowanej dynamicznie), na stosie znajdują się jedynie referencje (wskazania) do obiektów. Do tej kategorii należą wszystkie klasy (np. object, string), ale także tablice czy interfejsy. Zmienna typu referencyjnego może mieć wartość null oznaczającą, że nie wskazuje żadnego obiektu. Obiekt typu referencyjnego pozostaje na stercie tak długo, aż system nie stwierdzi, że nie ma już żadnych odwołań do tego obiektu (W C# mamy do czynienia z automatycznym zwalnianiem pamięci zajmowanej przez nieużywane obiekty).

Wskaźnikowe : używane do jawnego manipulowania pamięcią. Zwykle się ich nie używa (ponadto wskaźniki mogą być używane tylko w blokach nienadzorowanych).

Przypisanie (operator = ) typu referencyjnego polega na skopiowaniu wskazania na obiekt (nie powstaje nowy egzemplarz obiektu, a tylko mamy kolejne wskazanie na ten sam obiekt). Przypisanie (i ogólnie odwołanie się) typu bezpośredniego polega na skopiowaniu wartości całego obiektu.

Każdy typ bezpośredni posiada (niejawny) przypisany do niego typ referencyjny. W momencie rzutowania typu bezpośredniego na typ referencyjny automatycznie jest tworzony obiekt tego typu referencyjnego. Dzięki temu typy bezpośrednie możemy traktować jako pochodne typu object.

void : typ pusty. Możemy deklarować metody (ew. delegacje) tego typu - w takim wypadku metoda nie musi zwracać żadnej wartości.

null : słowo kluczowe oznaczające pustą referencję (wskazanie puste). Zmienna typu referencyjnego mająca wartość null nie wskazuje na żaden obiekt.


Rodzaje dostępu:

W C# zdefiniowano pięć rodzajów dostępu do typów i ich składowych:

public : składowa lub typ zadeklarowany jako publiczny są dostępne z dowolnego miejsca. Ten rodzaj dostępu jest domyślny dla interfejsów.

private : składowa zadeklarowana jako prywatna jest dostępna tylko z wnętrza typu, w którym została zadeklarowana. Ten rodzaj dostępu jest domyślny dla składowych klas i struktur.

protected : składowa zadeklarowana jako chroniona jest dostępna z wnętrza klasy, w której została zadeklarowana lub z wnętrza klasy pochodnej.

internal : typ lub składowa typu są dostępne tylko z wnętrza złożenia, w którym nastąpiła ich deklaracja (podczas kompilacji pliki .cs z kodem źródłowym programu są kompilowane w moduły - zgodnie z podziałem na przestrzenie nazw - a następnie grupowane w złożenia, ang. assembly)

protected internal : składowa zadeklarowana z takim rodzajem dostępu jest widoczna z wnętrza klasy, w której została zadeklarowana (lub klasy pochodnej od niej) oraz z wnętrza złożenia, w którym się znajduje.

Wymienione rodzaje dostępu można stosować w stosunku do typów (klas, struktur) oraz ich składowych (metod i danych). Każdy typ/składowa w C# posiada któryś z wymienionych rodzajów dostępu (co najwyżej przypisany domyślnie).


Typy predefiniowane w C#:

int : ( inaczej System.Int32). Liczba całkowita ze znakiem, zajmująca 4 bajty.

uint : ( System.UInt32). Liczba całkowita bez znaku. Zajmuje 4 bajty.

short : ( System.Int16 ). Liczba całkowita krótka ze znakiem. Zajmuje 2 bajty.

ushort : ( System.UInt16). Liczba całkowita krótka bez znaku. Zajmuje 2 bajty.

long : ( System.Int64 ). Liczba całkowita długa ze znakiem. Zajmuje 8 bajtów.

ulong : ( System.UInt64 ). Liczba całkowita długa bez znaku. Zajmuje 8 bajtów.

byte : ( System.Byte ). Pojedynczy bajt, bez uwzględnienia znaku. Istnieje także typ sbyte (System.Sbyte ) oznaczający pojedynczy bajt (ew. małą liczbę całkowitą) ze znakiem.

char : ( System.Char ) reprezentuje pojedynczy znak Unicode. Zajmuje 2 bajty. Literał tego typu zapisujemy w apostrofach, np. 'A' (znak A) lub '\u0041' (zapis w Unicode). Ponadto możemy używać pewnych sekwencji specjalnych, np. '\n'' (znak nowej lini), '\t' (tabulacja), ' \" ' (cudzysłów), '\'' (apostrof), '\0' (null).

bool : ( System.Boolean ) typ logiczny mogący przyjmować wartośći true lub false . Nie można dokonywać konwersji z bool na typ całkowity lub odwrotnie.

float : ( System.Single ) liczba zmiennopozycyjna pojedynczej precyzji. Zajmuje 4 bajty. W zakresie wartości typów zmiennopozycyjnych (float i double) mieszczą się wartości specjalne ?0, ?? oraz NaN (not a number). Literały typu float zapisujemy z przyrostkiem 'f' lub 'F', np. 9.4f Domyślnie literał zmiennopozycyjny jest typu double (co można dodatkowo podkreślić używając przyrostków 'd' lub 'D').

double : ( System.Double ) liczba zmiennopozycyjna podwójnej precyzji. Zajmuje 8 bajtów.

decimal : ( System.Decimal ) liczba w systemie dziesiętnym zajmująca 12 bajtów (przechowuje 28 cyfr oraz pozycję punktu dziesiętnego w tych liczbach). Ma mniejszy zakres w porównaniu z liczbami zmiennopozycyjnymi jednak zapewnia bardzo dużą precyzję przechowywania liczb o podstawie 10. Liczba zapisywana jako dziesiętna wymaga przyrostka 'm' lub 'M', np. decimal d = 10.1m ;

object : ( System.Object ) typ bazowy dla wszystkich innych typów (z wyjątkiem wskaźnikowych). Powoduje narzut 8 bajtów pamięci dla egzemplarzy typów wywodzących się z niego przechowywanych na stercie (w wypadku przechowywania na stosie nie ma narzutu).

string : ( System.String ) reprezentuje łańcuch (zmiennej długości) znaków Unicode. Możemy używać tych samych sekwencji specjalnych co w przypadku typu char, np. '\n', '\0', ... Pomimo tego, że string jest klasą (typem referencyjnym) ma pewne szczególne przywileje - można go tworzyć bez użycia operatora new, np. string s = " tekst \n " ; Poza zwykłymi literałami łańcuchowymi istnieją także tzw. literały dosłowne - zawarte wewnątrz @"...". Zawartość wewnątrz takiego literału jest brana 'dosłownie'. Tożsame są łańcuchy @"\\serwer\plik.txt" oraz "\\\\serwer\\plik.txt" .

Zapisując literał oznaczający liczbę możemy określić typ literału itp. , np.
0x5 : liczba 5 w systemie szesnastkowym. Literały szesnastkowe mają przedrostek 0x
5UL : oznacza wartość 5 typu ulong ( U - liczba bez znaku, L - liczba długa ).


Zmienne:

Zmienna (dostępna poprzez identyfikator) reprezentuje miejsce w pamięci. Każda zmienna posiada swój typ, który określa zbiór możliwych wartości i operacji. C# jest językiem o ścisłej typizacji, zbiór możliwych operacji dostępnych dla danego typu/zmiennej jest określany już podczas kompilacji, a zmienna jest dostępna tylko poprzez powiązany z nią typ. Zmienne deklarujemy według wzoru: modyfikatory typ identyfikator = wartPocz; (Modyfikatory oraz wartość początkowa nie są wymagane)
Np. int a = 10 ;


Dostępne modyfikatory:

static : pozwala zadeklarować składową statyczną (istniejącą na rzecz całego typu, a nie pojedynczego obiektu tego typu). Składowe statyczne nie wymagają istnienia obiektów. Dana składowa statyczna istnieje jeszcze przed pojawieniem się pierwszego obiektu danej klasy - ponadto istnieje tylko jedna jej wartość, wspólna dla wszystkich obiektów klasy,
np. static string napisWspólnyDlaCałejKlasy = "..."

const : pozwala zadeklarować stałą (daną składową której wartość jest obliczana w czasie kompilacji i nie może być zmieniana w czasie działania programu). Typ stałej musi być jednym z typów predefiniowanych (np. int, float, long, string), np. public const double PI = 3.14 ;

readonly : pozwala zadeklarować składową, której wartość nie będzie mogła być modyfikowana po początkowym nadaniu jej wartości. W odróżnieniu od danych stałych (const) wartości danych tego typu są obliczane podczas działania programu, a nie podczas kompilacji. Np. readonly MyClass a = new MyClass( ); readonly int b = 10;

volatile : deklaruje pole typu nietrwałego. Optymalizacje dokonywane na takim polu przez kompilator i CLR (reorganizacja kolejności instrukcji itp.) są ograniczone – zawsze odczyt lub przypisanie wartości takiej zmiennej powoduje fizyczne wywołanie odczytu lub zapisu do pmięci. Np. volatile int a = 1;


W przypadku typów referencyjnych nie wystarczy prosta deklaracja zmiennej. Deklaracja np. MyClass zm ; jest poprawna, jednak nie tworzy nowego obiektu typu, a jedynie samo wskazanie (zm nie wskazuje na żaden obiekt). Aby utworzyć egzemplarz typu referencyjnego konieczne jest użycie operatora new.

new : alokuje pamięć na stercie oraz wywołuje konstruktor podanego typu, w celu utworzenia obiektu. Pozwala utworzyć dynamicznie obiekt (obiekty typu referencyjnego, takie jak klasy, tworzymy tylko dynamicznie). Aby utworzyć egzemplarz danej klasy używamy konstrukcji, np. MyClass zm = new MyClass( );

Przed użyciem zmiennej konieczne jest przypisanie jej wartości (za wyjątkiem bloków unsafed) – może się to odbyć jawnie lub niejawnie (z użyciem wartości domyślnej. Jednak w tym wypadku kompilator wygeneruje ostrzeżenie). Wartości domyślne to:
null - dla referencji (gdy tworzymy zmienną typu referencyjnego bez przypisania do niej obiektu).
0 - dla wszystkich typów liczbowych (np. int, float) i wyliczeniowych.
false - dla typu logicznego bool.


Konwersje:

W C# istnieją konwersje jawne (przy użyciu rzutowania) i niejawne (przeprowadzane automatycznie w razie potrzeby). Rzutujemy stosując konstrukcję: ( typ ) wartość . Przykładowo:
int liczba = 100;
long długaLiczba = liczba ; // Konwersja niejawna
short krótkaLiczba = (short) liczba ; // Jawna konwersja

Każdy typ posiada własny zestaw reguł definiujących zasady konwersji. Dla typów wbudowanych niejawne konwersje są dostępne wtedy, gdy nie powodują utraty informacji (np. z int do long, z float do double, z long do float, ...) – w innym przypadku musimy stosować rzutowanie.

W klasie możemy zdefiniować możliwość jej konwersji do innego typu. W tym celu należy zdefiniować metodę typu public static implicit operator Typ ( ... ) aby móc niejawnie przekształcać klasę do typu Typ lub funkcję public static explicit operator Typ ( ... ) aby móc to zrobić jawnie. Ponadto taka funkcja musi pobierać jeden parametr takiego typu jak definiowana klasa, np.


public class A {
 double  wartość ;
 public static implicit  operator  double ( A a ) { return a.wartość ; }
 public static explicit operator  long( A a ) { return (long)wartość; }
}


Tablice:

Tablice są obiektami (wywodzącymi się z System.Array) umożliwiającymi przechowywanie wielu elementów pewnego typu w ciągłym obszarze pamięci. Operator [ ] służy do tworzenia i indeksowania tablic (indeksy rozpoczynają się od zera). Po utworzeniu tablicy jej długość nie może być już zmieniona. W przypadku przekroczenia zakresu tablicy zgłaszany jest wyjątek IndexOutOfRangeException.

Tworzenie tablic typów bezpośrednich jest bardzo efektywne, np.
int[] tablInt = new int[ 100 ] ;
int[ ] tabl = { 1, 2 , 3 } ;
MojaStruktura[ ] tablStr = new MojaStruktura[100];

Tworzenie tablic typów referencyjnych wymaga późniejszej inicjalizacji wartości tablicy, np.
mKlasa[] tablKlas = new mKlasa[100]; /* Na razie tablica jest wypełniona referencjami o wartości null. */
for ( int i=0; i < tablKlas.Length; i++ ) tablKlas[i] = new mKlasa();
/* inicjalizacja tablicy. */

Możliwe jest tworzenie tablic wielowymiarowych:
int [,,] tabl3D = new int [5][10][10]; // Tworzy regularną tablicę.
int [ ] [ ] tabl2D = new int [10] [ ] ; // Tworzy tablicę nieregularną.

for( int a=0; a < 10; a++ ) // tablice nieregularne są
tabl2D[a] = new int[ a ] ; // tablicami tablic

Tablice posiadają składowe informujące o ich długości:
Length : informuje o długości tablic jednowymiarowych, np. for( int i=0; i < tabl.Length ; i++) ... ;
GetLength : metoda pozwalająca uzyskać informację o długości tablicy w danym wymiarze (dla tablic wielowymiarowych), liczonym od 0. Np. for( int i=0; i < tabl3d.GetLength(2); i++) ... ;


Typ wyliczeniowy :

enum : słowo kluczowe pozwalające zdefiniować typ wyliczeniowy (nowy typ danych, składający się z nazwanych stałych numerycznych).

Przykładowo:

public  enum  DniTyg {  Pn, Wt, Sr, Czw, Pt, Sb, Nd } ;	// Definicja typu
. . .
DniTyg  dzien = DniTyg.Pn ;     // Deklaracja zmiennej. Nazwy składowych
                                // wyliczenia muszą być poprzedzone nazwą
                                // typu wyliczeniowego.

Domyślnie kolejne elementy typu wyliczeniowego (stałe numeryczne) mają przyznawane wartości liczbowe w kolejności 0, 1, 2, 3 itd. Można jednak zdefiniować własny porządek, np.
public enum Kierunki { Północ = 1, Południe=2, Wschód=4, Zachód=8 }
Kierunki k = Kierunki.Północ | Kierunki.Wschód ;

Może zajść niejawna konwersja pomiędzy typem wyliczeniowym a numerycznym (lub odwrotnie), ponadto typ wyliczeniowy może być jawnie konwertowany do innego typu wyliczeniowego.


Metody:

Wszystkie funkcje w C# są składowymi pewnych typów - pełnią rolę metody pewnej klasy lub struktury. Ogólna deklaracja metody:
[modyfikatory] typ nazwaMetody ( [listaParametrów] ) { ... }

Dostępne modyfikatory metod:
public : (oraz private, protected, internal, protected internal). Modyfikuje dostęp do metody.
virtual : pozwala tworzyć metodę wirtualną (przydatne przy dziedziczeniu).
abstract : pozwala utworzyć metodę abstrakcyjną (przydatne przy dziedziczeniu)
extern : wskazuje, że metoda jest zaimplementowana z użyciem kodu nienadzorowanego.
static : metoda statyczna działa na rzecz całego typu, a nie pojedynczego obiektu – wywołujemy ją według wzoru : nazwaTypu.nazwaFunkcjiStatycznej() . Aby wywołać metodę statyczną nie potrzebujemy żadnego obiektu (klasy lub struktury).

Na liście parametrów poza typem możemy podać modyfikatory:
ref : pozwala przekazać dany parametr przez referencję (domyślnie parametry są przekazywane przez wartość. Modyfikator ref musi się pojawić na liście parametrów metody oraz podczas jej wywoływania).
out : parametr tego typu służy przekazaniu wartości z metody na zewnątrz (domyślnie parametr przekazuje wartość z zewnątrz do metody). Przekazując do metody zmienną w trybie out przekazujemy ją przez referencję, oczekując jednocześnie że wewnątrz metody zostanie jej nadana odpowiednia wartość. Modyfikator out musi się pojawić na liście parametrów metody oraz podczas jej wywołania.
params : dzięki temu modyfikatorowi metoda może przyjmować dowolną liczbę parametrów. Modyfikator params może być zastosowany tylko do ostatniego parametru metody.

Np.
public class A {
  public void  f_1 (  ref  int  parametr ) { ... }
  public void  f_2 ( params int[ ]  tabl ) { … }
}
...
A  a = new A( ) ;  // Utworzenie obiektu klasy A
a.f_1( ref  x );   // Przekazanie przez referencję zmiennej   int x ;
a.f_2( 1, 2, 3 );  // Tablica tabl (wewnątrz metody f_2) będzie
                   // zawierała 3 elementy 

Możliwe jest przeciążanie metod (może istnieć wiele metod o tej samej nazwie, ale pobierających różne parametry. Kompilator dokona właściwego wyboru metody).


Atrybuty

Atrybuty pozwalają uzupełnić elementy kodu (deklaracje typów i składowych) o pewne dodatkowe informacje. Te dodatkowe informacje będą przechowywane w metadanych opisujących dany element kodu.

Atrybuty zapisujemy: [ NazwaAtrybutu ]

Przykładowo pisząc usługę WebService i chcąc udostępnić publicznie pewną metodę wystarczy przed jej definicją zapisać atrybut [WebMethod], np.

   [WebMethod]
   public int dodaj(int a, int b)
   {
       return a + b;
   }

Konkretne atrybuty mogą być umieszczane tylko przed konkretnymi elementami języka. Przykładowo atrybut [Serializable] może być umieszczony przed definicją klasy lub struktury, ale nie przed deklaracją funkcji.

Atrybut może pobierać parametry.Np.

   [Obsolete("Typ przestarzały", true)]
   public class A { ... }

Atrybuty są klasami wywodzącymi się z klasy System.Attribute (jednak ich użycie następuje już według specjalnej konwencji, z użyciem nawiasów kwadratowych). Istnieją atrybuty predefiniowane (np. Serializable, Obsolete, NonSerialized, ... ). Ponadto użytkownik może definiować własne atrybuty, wyprowadzając je z klasy System.Attribute.


Właściwości:

Właściwości są bardzo podobne do pól ... oraz metod. Ogólna deklaracja właściwości:

[modyfikatory]  typ  nazwaWłaściwości {
	[modyfikatory]  get  { ... }
	[modyfikatory]  set  { ... }
	...
}

Modyfikatorami dla właściwości mogą być modyfikatory określające rodzaj dostępu (public, private, ...) oraz virtual, static i abstract.Np.

public class A {
  float  wartość ;		// To jest pole prywatne
  public  int  wart( )  {
    get {   return (int)wartość ;  }
    set {   wartość = (float)value ; }
  }
}
...
A a = new A( );
a.wart = 10 ;            // użycie części  set  deklaracji właściwości
Console.Write( a.wart ); // użycie części  get 

Część get właściwości musi zwracać wartość o takim samym typie jak typ właściwości, natomiast część set właściwości posiada parametr value, zgodny z typem właściwości.

W rzeczywistości, w podanym wyżej przykładzie właściwość wart() zostanie przez kompilator przekształcona do dwóch metod - get_wart( ) oraz set_wart( ). Ponadto metody te będą typu inline (będą rozwijane w miejscu wywołania - co zapewnia większą szybkość ich 'wywołania').

Definiując właściwość możemy określić samą część get - w takim wypadku będzie to właściwość tylko do odczytu

Istnieje także specjalny rodzaj właściwości, który pozwala wprowadzić indeksowanie do tworzonej przez nas klasy. Od zwykłych właściwości różni się tym, że zamiast nazwaWłaściwości(...) wstawiamy this[...]. Np.

public  class  Tablica2D {
  ...
  int[ ]   tablica = new  int[ wlkX * wlkY ] ;

  public int  this [ int x, int y ] {
    get { return tablica[ x + y*wlkX ] ; }
    set { tablica[x+y*wlkX] = (int) value ; }
  }
}
...
Tablica2D  tabl = new  Tablica2D( 10,10 );
tabl [0,0] = 0 ; // klasę można indeksować 


prev | Strona Główna | next

Copyright © 2006 Daniel Dusiński
Wszelkie prawa zastrzeżone