(za książką: Microsoft C# Language Specification)
C# jest prostym, nowoczesnym, obiektowym, bezpiecznym ze względu na typy
językiem programowania, wywodzącym się z języków C i C++.
C# stanowi część platformy Microsoft .NET Framework, zawierającej wspólny
aparat wykonawczy dla różnych języków oraz bogatą bibliotekę klas. W ramach
platformy Microsoft .NET zdefiniowano swoisty język uniwersalny, tzw. CLS
(ang. Common Language Subset - wspólna część języków). Zapewnia on
płynną współpracę między zgodnymi ze standardem CLS językami i bibliotekami
klas.
Język C# sam w sobie nie zawiera biblioteki klas.
1.1. Kanoniczny program "hello, world"
using System;
class Hello
{
static void Main() {
Console.WriteLine("hello, world");
}
}
1.1.a Punkt wejścia do programu
Metoda punku wejścia do programu nosi zawsze nazwę Main i może
mieć jedną z następujących sygnatur:
static void Main() {...}
static void Main(string[] args) {...}
static int Main() {...}
static int Main(string[] args) {...}
W ramach jednego programu żadna klasa ani struktura nie może zawierać
więcej niż jednej metody Main , której definicja spełnia
warunki nakładane na punkt wejścia do programu.
Ponieważ język C# obsługuje przeciążąnie metod, dozwolone są inne,
przeciążone wersje metody Main , o ile mają więcej niż
jeden parametr lub jeśli ich jedyny parametr nie jest typu
string[] .
1.2. Typy
W języku C# obsługiwane są dwa rodzaje typów danych: typy bezpośrednie
(ang. value types) i typy referencyjne (ang. reference types).
Do typów bezpośrednich należą:
- typy proste (np.
char , int i float )
- typy wyliczeniowe (
enum )
- typy struktur (
struct )
Do typów referencjnych należą:
- typy klas (
class )
- typy interfejsów (
interface )
- typy delegacji (
delegate )
- typy tablicowe
1.2.1 Typy predefiniowane
Predefiniowanymi typami referencyjnymi są typy object i string .
Typ object jest ostatecznym typem bazowym wszystkich pozostałych typów.
Typ string służy do reprezentowania wartości tekstowych w formacie Unicode.
Wartości typu string są niezmienne.
Predefiniowane typy i przykładowe zapisy wartości literałów:
Typ |
Opis |
Przykład |
object |
ostateczny typ bazowy wszystkich pozostałych typów |
object o = null; |
string |
typ ciągu tekstowego; ciąg tekstowy jest łańcuchem znaków w standardzie Unicode |
string s = "hello"; |
sbyte |
8-bitowy typ całkowitoliczbowy ze znakiem |
sbyte val = 12; |
short |
16-bitowy typ całkowitoliczbowy ze znakiem |
short val = 12; |
int |
32-bitowy typ całkowitoliczbowy ze znakiem |
int val = 12; |
long |
64-bitowy typ całkowitoliczbowy ze znakiem |
long val1 = 12; long val2 = 34L; |
byte |
8-bitowy typ całkowitoliczbowy bez znaku |
byte val1 = 12; byte val2 = 34U; |
ushort |
16-bitowy typ całkowitoliczbowy bez znaku |
ushort val1 = 12; ushort val2 = 34U; |
uint |
32-bitowy typ całkowitoliczbowy bez znaku |
uint val1 = 12; uint val2 = 34U; |
ulong |
64-bitowy typ całkowitoliczbowy bez znaku |
ulong val1 = 12; ulong val2 = 34U;
ulong val3 = 56L; ulong val4 = 78UL; |
float |
typ zmiennoprzecinkowy pojedynczej precyzji |
float val = 1.23F; |
double |
typ zmiennoprzecinkowy podwójnej precyzji |
double val1 = 1.23;
double val2 = 4.56D; |
bool |
typ logiczny; wartością typu bool jest prawda lub fałsz |
bool val1 = true;
bool val2 = false; |
char |
typ znakowy; wartością typu char jest znak w standardzie Unicode |
char val = 'h'; |
decimal |
precyzyjny typ dziesiętny z 28 znaczącymi cyframi |
decimal val = 1.23M; |
Predefiniowane typy są skrótami typów zapewnianych przez system. Np. słowo
kluczowe int odnosi się do struktury System.Int32 .
Ze względu na styl programowania zamiast pełnej nazwy typu systemowego
lepiej jest używać słów kluczowych.
Predefiniowane typy bezpośrednie takie jak int w pewnych wypadkach
traktowane są specjalnie, lecz przeważnie zachowują się tak samo jak inne
struktury. Przeciążenie operatorów umożliwia programistom definiowanie nowych
typów struktur, zachowujących się podobnie jak predefiniowane typy bezpośrednie.
Przeciążenie operatorów zastosowano także w samych typach predefiniowanych.
Na przykład operatory porównania == i != mają różne
znaczenie w zależności od danego typu predefiniowanego:
- Dwa wyrażenia typu
int uważa się za równe, jeśli reprezentują
tą samą wartość całkowitoliczbową.
- Dwa wyrażenia typu
object uważa się za równe, jeśli odnoszą
się do tego samego obiektu lub mają wartość null .
- Dwa wyrażenia typu
string uważa się za równe, jeśli ciągi mają
identyczną długość i zawierają identyczne znaki w odpowiednich pozycjach
w ciągu lub jeśli oba naraz mają wartość null .
1.2.2 Przekształcenia typów
W języku C# wyróżnia się dwa typy przekształceń: przekształcenia niejawne
i przekształcenia jawne. Przekształcenia niejawne stosuje się wówczas,
gdy można je bezpiecznie przeprowadzać bez szczególnej ostrożności. Dotyczy
to sytuacji, gdy typ "mniejszy" jest przekształcany w typ "większy", bez
utraty informacji.
using System;
class Test
{
static void Main() {
int intValue = 123;
long longValue = intValue; // przekształcenie niejawne typu int w long
Console.WriteLine("{0}, {1}", intValue, longValue);
}
}
Natomiast przekształcenia jawne przeprowadza się z użyciem rzutowania:
using System;
class Test
{
static void Main() {
long longValue = Int64.MaxValue;
int intValue = (int) longValue; // rzutowanie typu long w int
Console.WriteLine("{0} = {1}", longValue, intValue);
}
}
Tekst wyjściowy z powodu obcięcia (liczba z lewej - poprawna, wynik -1
niepoprawny) jest następujący:
9223372036854775807 = -1
1.2.3 Typy tablicowe
Tablice mogą być jednowymiarowe i wielowymiarowe. Obsługiwane są tablice
"prostokątne", jak i "nieregularne" (tablice tablic).
class Test
{
static void Main() {
int[] a1; // jednowymiarowa tablica wartości typu int
int[,] a2; // dwuwymiarowa tablica wartości typu int
int[,,] a3; // trójwymiarowa tablica wartości typu int
int[][] j2; // tablica nieregularna: tablica tablic wartości typu int
int[][][] j3; // tablica tablic tablic wartości typu int
}
}
Tablice są typami referencyjnymi, a więc deklaracja zmiennej tablicowej
powoduje jedynie zarezerwowanie miejsca na referencję do tablicy.
class Test
{
static void Main() {
int[] a1 = new int[] {1, 2, 3};
int[,] a2 = new int[,] {{1, 2, 3}, {4, 5, 6}};
int[,,] a3 = new int[10, 20, 30];
int[][] j2 = new int[3][];
j2[0] = new int[] {1, 2, 3};
j2[1] = new int[] {1, 2, 3, 4, 5, 6};
j2[2] = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
}
}
W przypadku deklaracji zmiennych lokalnych i pól dopuszczalna jest forma
skrótowa:
int[] a1 = new int[] {1, 2, 3};
// równoważne z:
int[] a2 = {1, 2, 3};
1.2.4 Ujednolicenie systemu typów
Język C# oferuje "ujednolicony system typów". Wszystkie typy - w tym także
bezpośrednie są pochodne względem typu object . Metody obiektów
można wywoływać w odniesieniu do dowolnej wartości, nawet wartości typów
"prymitywnych", jak int .
using System;
class Test
{
static void Main() {
Console.WriteLine(3.ToString());
}
}
Z powyższym stwierdzeniem wiążą się także operacje opakowywania
i rozpakowywania (ang. boxing i unboxing).
using System;
class Test
{
static void Main() {
int i = 123;
object o = i; // opakowywanie
int j = (int) o; // rozpakowywanie
i++;
Console.WriteLine("i={0} j={1}", i, j); // wypisze: i=124, j=123
}
}
Kiedy zmienną typu bezpośredniego trzeba przekształcić w typ referencyjny,
alokowany jest obiekt opakowanie, przechowujący jej skopiowaną
wartość. Rozpakowanie jest operacją odwrotną i polega na
rzutowaniu na oryginalny typ bezpośredni - przy czym wartość jest
kopiowana z opakowania do odpowiedniej komórki pamięci.
Tego rodzaju ujednolicenie systemu typów zapewnia typy bezpośrednie
o cechach obiektowych, ale nie wprowadza niepotrzebnych obciążeń.
W programach, w których wartości typu int muszą zachowywać
się jak obiekty, można skorzystać z opakowywania. Dzięki możliwości
traktowania typów bezpośrednich jak obiekty zasypana została przepaść
między typami bezpośrednimi a referencyjnymi, jaka istnieje
w większości języków programowania.
1.3. Zmienne i parametry
Parametry bezpośrednie
using System;
class Test
{
static void F(int p) { // parametr bezpośredni
Console.WriteLine("p = {0}", p;
p++;
}
static void Main() {
int a = 1;
Console.WriteLine("pre: a = {0}", a);
F(a);
Console.WriteLine("post: a = {0}", a);
}
}
// wynik:
// pre: a = 1
// p = 1
// post: a = 1
Parametry referencyjne
using System;
class Test
{
static void Swap(ref int a, ref int b) { // parametry referencyjne
int t = a;
a = b;
b = t;
}
static void Main() {
int x = 1;
int y = 2;
Console.WriteLine("pre: x = {0}, y = {1}", x, y);
Swap(ref x, ref y);
Console.WriteLine("post: x = {0}, y = {1}", x, y);
}
}
// wynik:
// pre: x = 1, y = 2
// post: x = 2, y = 1
Słowo kluczowe ref musi być stosowane zarówno w deklaracji
parametru formalnego jak i w miejscu jego użycia. Użycie modyfikatora
ref zwraca uwagę na parametr, dzięki czemu programista czytający
kod zauważy, że argument może się zmienić wskutek wywołania.
Parametry wyjściowe
using System;
class Test
{
static void CalcSquare(int a, out int b) { // parametr wyjściowy
b = a * a;
}
static void Main() {
int x = 3;
int y = 0;
Console.WriteLine("pre: x = {0}, y = {1}", x, y);
CalcSquare(x, out y);
Console.WriteLine("post: x = {0}, y = {1}", x, y);
}
}
// wynik:
// pre: x = 3, y = 0
// post: x = 3, y = 9
Tablica parametrów o zmiennej długości
Tablicę parametrów definiuje się z użyciem modyfikatora params .
Dla danej metody może istnieć tylko jedna tablica parametrów i będzie ona
zawsze ostatnim podanym parametrem. Tablica praramatrów jest zawsze tablicą
typu jednowymiarowego.
using System;
class Test
{
static void F(params int[] args) {
Console.WriteLine("liczba argumentów: {0}", args.Length);
for (int i = 0; i < args.Length; i++)
Console.WriteLine("\targs[{0}] = {1}", i, args[i]);
}
static void Main() {
F();
F(1);
F(1, 2);
F(1, 2, 3);
F(new intp[ {1, 2, 3, 4});
}
}
// wynik:
// liczba argumentów: 0
// args[0] = 1
// liczba argumentów: 1
// args[0] = 1
// liczba argumentów: 2
// args[0] = 1
// args[1] = 2
// liczba argumentów: 3
// args[0] = 1
// args[1] = 2
// args[2] = 3
// liczba argumentów: 4
// args[0] = 1
// args[1] = 2
// args[2] = 3
// args[3] = 4
1.4. Automatyczne zarządzanie pamięcią
1.5. Wyrażenia
Wyrażenia buduje się z operandów i operatorów. Operatory wyrażenia
wskazują, jakie operacje należy wykonać na operandach.
W języku C# istnieją trzy typy operatorów:
- operatory jednoargumentowe - mają jeden operand i są zapisywane w notacji
przedrostkowej (np.
-x ) lub przyrostkowej (np. x++ ),
- operatory dwuargumentowe - mają dwa operandy i są zapisywane w notacji
wrostkowej (np.
x + y ),
- istnieje tylko jeden operator trójargumentowy
?: - ma trzy
operandy i zapisywany jest w notacji wrostkowej (c ? x + y ).
Kolejność obliczania operatorów w wyrażeniu jest wyznaczana przez priorytet
i łączność operatorów.
Pewne operatory mogą być przeciążane. Przeciążanie opratorów pozwala na
podawanie zdefiniowanych przez użytkownika implementacji operatorów w wypadku
operacji, gdzie jeden lub oba operandy mają zdefiniowany przez użytkownika typ
klasy lub struktury.
1.5.1 Priorytet i łączność operatorów
Gdy wyrażenie zawiera zbyt wiele operatorów, o kolejności obliczania poszczególnych
operatorów rozstrzyga priorytet operatorów. Na przykład wyrażenie
x + y * z oblicza się tak jak x + (y * z) , ponieważ operator *
ma wyższy priorytet niż operator + . Priorytet operatora ustala definicja
gramatyki języka.
Poniższa tabela zawiera listę operatorów, w kolejności zgodnej
z ich priorytetem - od najwyższego do najniższego:
Kategoria |
Operatory |
podstawowe |
(x) x.y f(x) a[x] x++ x--
new typeof sizeof checked unchecked |
jednoargumentowe |
+ - ! ~ ++x --x (T)x |
multiplikatywne |
* / % |
addytywne |
+ - |
przesunięcia |
<< >> |
relacji |
< > <= >=
is as |
równości |
== != |
koniunkcji (AND) |
& |
różnicy symetrycznej (XOR) |
^ |
alternatywy (OR) |
| |
warunkowy koniunkcji (AND) |
&& |
warunkowy alternatywy (OR) |
|| |
warunkowy |
?: |
przypisania |
= *= /= %= += -= <<
>>= &= ^= |= |
1.5.2 Przeciążanie operatorów
1.6. Instrukcje
W języku C# większość instrukcji pochodzi bezpośrednio z języków C i C++, choć
istnieją tu pewne nowe możliowści i istotne różnice. W poniższej tabeli podano
rodzaje używanych instrukcji wraz z przykładami.
Instrukcja |
Przykład |
Listy instrukcji i instrukcje blokowe |
static void Main() {
F();
G();
{
H();
I();
}
}
|
Instrukcje z etykietami i instukcje goto |
static void Main(string[] args) {
if (args.Length == 0)
goto done;
Console.WriteLine(args.Length);
done:
Cosole.WriteLine("Done");
}
|
Lokalne deklaracje stałych |
static void Main() {
const float pi = 3.14;
const intr = 123;
Console.WriteLine(pi * r * r);
}
|
Lokalne deklaracje zmiennych |
static void Main() {
int a;
int b = 2, c = 3;
a = 1;
Console.WriteLine(a + b + c);
}
|
Instrukcje wyrażeniowe |
static int F(int a, int b) {
return a + b;
}
static void Main() {
F(1, 2); // instrukcja wyrażeniowa
}
|
Instrukcje if |
static void Main(string[] args) {
if (args.Length == 0)
Console.WriteLine("No args");
else
Console.WriteLine("Some args");
}
|
Instrukcje switch |
static void Main(string[] args) {
switch (args.Length) {
case 0:
Console.WriteLine("No args");
break;
case 1:
Console.WriteLine("One arg");
break;
case 2:
TwoArgs();
goto case 3;
case 3:
TwoOrThreeArgs();
goto default;
default:
Console.WriteLine("{0} args", args.Length);
break;
}
}
|
Instrukcje while |
static void Main(string[] args) {
int i = 0;
while (i < args.Length) {
Console.WriteLine(args[i]);
i++;
}
}
|
Instrukcje do |
static void Main() {
string s;
do { s = Console.ReadLine(); }
while (s != "Exit");
}
|
Instrukcje for |
static void Main(string[] args) {
for (int i = 0; i < args.Length; i++)
Console.WriteLine(args[i]);
}
|
Instrukcje foreach |
static void Main(string[] args) {
foreach (string s in args)
Console.WriteLine(s);
}
|
Instrukcje break |
static void Main(string[] args) {
int i = 0;
while (true) {
if (i >= args.Length)
break;
Console.WriteLine(args[i++]);
}
}
|
Instrukcje continue |
static void Main(string[] args) {
int i = 0;
while (true) {
Console.WriteLine(args[i++]);
if (i < args.Length)
continue;
break;
}
}
|
Instrukcje return |
static int F(int a, int b) {
return a + b;
}
static void Main() {
Console.WriteLine(F(1, 2));
return;
}
|
Instrukcje throw i try |
static int F(int a, int b) {
if (b == 0)
throw new Exception("Divide by zero");
return a / b;
}
static void Main() {
try {
Console.WriteLine(F(5, 0));
}
catch (Exception e) {
Console.WriteLine("Error: " + e.Message);
}
}
|
Instrukcje checked i unchecked |
static void Main() {
int x = Int32.MaxValue;
Console.WriteLine(unchecked(x + 1)); // Przepełnienie
Console.WriteLine(checked(x + 1)); // Wyjątek
Console.WriteLine(x + 1); // Przepełnienie
}
|
Instrukcje lock |
static void Main() {
A a = ...
lock(a) {
a.P = a.P + 1;
}
/* równoważne z poniższym, "a" wartościowane tylko raz
System.Threading.Monitor.Enter(a);
try {
a.P = a.P + 1;
}
finally {
System.Threading.Monitor.Exit(a);
}
*/
}
|
Instrukcje using |
using System;
class Resource: IDisposable
{
public void F() {
Console.WriteLine("Resource.F");
}
public void Dispose() {...}
...
}
static void Main() {
using (Resource r = new Resource()) {
r.F();
}
/* równoważne z poniższym
Resource r = new Respurce();
try {
r.F();
}
finally {
if (r != null)
((IDisposable)r).Dispose();
}
*/
}
|
1.7. Klasy
1.7.1 Stałe
1.7.2 Pola
1.7.3 Metody
1.7.4 Właściwości
1.7.5 Zdarzenia
1.7.6 Operatory
1.7.7 Indeksatory
1.7.8 Konstruktory egzemplarza
1.7.9 Destruktory
1.7.9.a Finalizer
1.7.10 Konstruktory statyczne
1.7.11 Dziedziczenie
1.8 Struktury
1.9. Interfejsy
1.10. Delegacje
delegate void SimpleDelegate();
class Test
{
static void F() {
System.Console.WriteLine("Test.F");
}
static void Main() {
SimpleDelegate d = new SimpleDelegate(F);
d();
}
}
Delegacje okazują się użyteczne zwłaszcza wtedy, gdy korzysta się
z ich anonimowości:
using System;
class Test
{
delegate void PrintValue(int val);
static void Plain(int val) {
Console.Write(val);
}
static void Square(int val) {
Console.WriteLine(" -> {0}", val * val);
}
static void PrintNumbers(int last, PrintValue[] funcs) {
for (int i = 1; i <= last; i++) {
for (int j = 0; j < funcs.Length; j++) {
PrintValue print = funcs[j];
print(i);
}
}
}
static void Main() {
PrintValue[] funcs = {new PrintValue(Plain), new PrintValue(Square)};
PrintNumbers(4, funcs);
}
}
1.11. Typy wyliczeniowe
1.12. Przestrzenie nazw i podzespoły
|