TREŚĆ

Wprowadzenie do języka C#

(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

Valid HTML 4.01!