Skip to content

3. Podstawy języka C#

3.1. Introduction

Na początku traktujemy C# jako klasyczny język programowania. Kwestie związane z klasami omówimy w dalszej części. W programie występują dwa elementy:

  • dane
  • instrukcje, które nimi operują

Zazwyczaj staramy się oddzielić dane od instrukcji:

3.2. Dane w języku C#

W języku C# stosuje się następujące typy danych:

  1. liczby całkowite
  1. liczby rzeczywiste
  2. liczby dziesiętne
  3. znaki i ciągi znaków
  4. wartości logiczne
  5. obiekty

3.2.1. Wstępnie zdefiniowane typy danych

Typ C#
Typ .NET
Reprezentowana wartość
Sufiks
wartości literałowych
Kodowanie
Zakres wartości
char
Znak (znaki)
znak
 
2 bajty
znak Unicode (UTF-16)
string
Ciąg znaków (C)
ciąg znaków
  
odwołanie do sekwencji znaków Unicode
int
Int32 (S)
liczba całkowita
 
4 bajty
[-231, 231-1] [–2147483648, 2147483647]
uint
UInt32 (S)
..
U
4 bajty
[0, 232-1] [0, 4294967295]
long
Int64 (S)
..
L
8 bajtów
[-263, 263 -1] [–9223372036854775808, 9223372036854775807]
ulong
UInt64 (S)
..
UL
8 bajtów
[0, 264 -1] [0, 18446744073709551615]
sbyte
 
..
 
1 bajt
[-27 , 27 -1] [-128,+127]
byte
Bajt(y)
..
 
1 bajt
[0 , 28 -1] [0,255]
short
Int16 (S)
..
 
2 bajty
[-215, 215-1] [-32768, 32767]
ushort
UInt16 (S)
..
 
2 bajty
[0, 216-1] [0,65535]
float
Pojedynczy (S)
liczba rzeczywista
F
4 bajty
[1.5 10-45, 3.4 10+38] jako wartość bezwzględna
double
Typ Double (S)
..
D
8 bajtów
[-1.7 10+308, 1.7 10+308] w wartościach bezwzględnych
decimal
Liczba dziesiętna (S)
liczba dziesiętna
M
16 bajtów
[1.0 10-28,7.9 10+28] jako wartość bezwzględna z 28 cyframi znaczącymi
bool
Boolean (S)
..
 
1 bajt
true, false
object
Obiekt (C)
odwołanie do obiektu
  
odwołanie do obiektu

Powyżej zestawiono typy C# z ich odpowiednikami w formacie .NET, z adnotacją (S), jeśli typ ten jest strukturą, oraz (C), jeśli jest klasą. Okazuje się, że istnieją dwa możliwe typy dla liczby całkowitej 32-bitowej: int i Int32. Typ int jest typem języka C#. Int32 jest strukturą należącą do przestrzeni nazw System. Jej pełna nazwa brzmi zatem System.Int32. Typ int jest aliasem w języku C#, który odnosi się do struktury .NET System.Int32. Podobnie typ C# string jest aliasem dla typu .NET System.String. System.String jest klasą, a nie strukturą. Oba pojęcia są do siebie zbliżone, jednak istnieje między nimi następująca zasadnicza różnica:

  • zmienną typu Structure obsługuje się poprzez jej wartość
  • zmienną typu Classe obsługuje się poprzez jej adres (odwołanie w języku obiektowym).

Zarówno structure, jak i classe są typami złożonymi posiadającymi atrybuty i metody. W związku z tym można zapisać:

string nomDuType=3.GetType().FullName;

W powyższym przykładzie literał 3 ma domyślnie typ C# int, a zatem typ .NET System.Int32. Struktura ta posiada metodę GetType(), która zwraca obiekt zawierający cechy typu danych System.Int32. Wśród nich właściwość FullName zwraca pełną nazwę typu. Widać zatem, że literał 3 jest obiektem bardziej złożonym, niż mogłoby się wydawać na pierwszy rzut oka.

Oto program ilustrujący te różne kwestie:


using System;

namespace Chap1 {
    class P00 {
        static void Main(string[] args) {
            // przykład 1
            int ent = 2;
            float fl = 10.5F;
            double d = -4.6;
            string s = "essai";
            uint ui = 5;
            long l = 1000;
            ulong ul = 1001;
            byte octet = 5;
            short sh = -4;
            ushort ush = 10;
            decimal dec = 10.67M;
            bool b = true;
            Console.WriteLine("Type de ent[{1}] : [{0},{2}]", ent.GetType().FullName, ent,sizeof(int));
            Console.WriteLine("Type de fl[{1}]: [{0},{2}]", fl.GetType().FullName, fl, sizeof(float));
            Console.WriteLine("Type de d[{1}] : [{0},{2}]", d.GetType().FullName, d, sizeof(double));
            Console.WriteLine("Type de s[{1}] : [{0}]", s.GetType().FullName, s);
            Console.WriteLine("Type de ui[{1}] : [{0},{2}]", ui.GetType().FullName, ui, sizeof(uint));
            Console.WriteLine("Type de l[{1}] : [{0},{2}]", l.GetType().FullName, l, sizeof(long));
            Console.WriteLine("Type de ul[{1}] : [{0},{2}]", ul.GetType().FullName, ul, sizeof(ulong));
            Console.WriteLine("Type de b[{1}] : [{0},{2}]", octet.GetType().FullName, octet, sizeof(byte));
            Console.WriteLine("Type de sh[{1}] : [{0},{2}]", sh.GetType().FullName, sh, sizeof(short));
            Console.WriteLine("Type de ush[{1}] : [{0},{2}]", ush.GetType().FullName, ush, sizeof(ushort));
            Console.WriteLine("Type de dec[{1}] : [{0},{2}]", dec.GetType().FullName, dec, sizeof(decimal));
            Console.WriteLine("Type de b[{1}] : [{0},{2}]", b.GetType().FullName, b, sizeof(bool));
        }
    }
}
  • wiersz 7: deklaracja liczby całkowitej ent
  • wiersz 19: typ zmiennej v można uzyskać za pomocą funkcji v.GetType().FullName. Rozmiar struktury S można uzyskać za pomocą funkcji sizeof(S). Instrukcja Console.WriteLine("... {0} ... {1} ...", param0, param1, ...) wyświetla na ekranie tekst będący jej pierwszym parametrem, zastępując każde oznaczenie {i} wartością wyrażenia parami.
  • wiersz 22: ponieważ typ string określa klasę, a nie strukturę, nie można użyć operatora sizeof.

Oto wynik wykonania:

Type de ent[2] : [System.Int32,4]
Type de fl[10,5]: [System.Single,4]
Type de d[-4,6] : [System.Double,8]
Type de s[essai] : [System.String]
Type de ui[5] : [System.UInt32,4]
Type de l[1000] : [System.Int64,8]
Type de ul[1001] : [System.UInt64,8]
Type de b[5] : [System.Byte,1]
Type de sh[-4] : [System.Int16,2]
Type de ush[10] : [System.UInt16,2]
Type de dec[10,67] : [System.Decimal,16]
Type de b[True] : [System.Boolean,1]

Wyświetlane są typy .NET, a nie aliasy C#.

3.2.2. Notacja danych literałowych

entier int (32 bits)
145, -7, 0xFF (szesnastkowo)
entier long (64 bits) - suffixe L
100000L
réel double
134,789, -45E-18 (-45 × 10⁻¹⁸)
réel float (suffixe F)
134.789F, -45E-18F (-45 10-18)
réel decimal (suffixe M)
100000M
caractère char
„A”, „b”
chaîne de caractères string
„dzisiaj” „c:\\rozdział1\\akapit3” @„c:\rozdział1\akapit3”
booléen bool
prawda, fałsz
date
new DateTime(1954,10,13) (rok, miesiąc, dzień) dla 13.10.1954

Zwróćmy uwagę na dwa ciągi literałowe: „c:\\chap1\\paragraph3” oraz @„c:\chap1\paragraph3”. W ciągach literałowych znak \ jest interpretowany. Zatem „\n” oznacza znak końca linii, a nie sekwencję dwóch znaków \ i n. Gdybyśmy chcieli uzyskać tę sekwencję, należałoby napisać „\\n”, gdzie sekwencja \\ jest interpretowana jako pojedynczy znak \. Można również napisać @"\n", aby uzyskać ten sam wynik. Składnia @"tekst" wymaga, aby tekst został potraktowany dokładnie tak, jak jest zapisany. Czasami nazywa się to ciągiem dosłownym.

3.2.3. Deklaracja danych

3.2.3.1. Rola deklaracji

Program operuje na danych charakteryzujących się nazwą i typem. Dane te są przechowywane w pamięci. Podczas kompilacji programu kompilator przypisuje każdej danym miejsce w pamięci określone adresem i rozmiarem. Czyni to na podstawie deklaracji wprowadzonych przez programistę.

Ponadto deklaracje te pozwalają kompilatorowi wykrywać błędy programistyczne. Na przykład operacja

x=x*2;

zostanie uznana za błędną, jeśli x jest na przykład ciągiem znaków.

3.2.3.2. Deklaracja stałych

Składnia deklaracji stałej jest następująca:

    const type nom=valeur;          //definiuje stałą nazwa=wartość

Na przykład:

const float myPI=3.141592F;    

Po co deklarować stałe?

  1. Program będzie łatwiejszy do odczytania, jeśli nadamy stałej znaczącą nazwę:
    const  float taux_tva=0.186F;
  1. Modyfikacja programu będzie łatwiejsza, jeśli „stała” ulegnie zmianie. Tak więc w powyższym przypadku, jeśli stawka VAT wzrośnie do 33%, jedyną zmianą, jaką trzeba będzie wprowadzić, będzie zmiana instrukcji definiującej jej wartość:
    const float taux_tva=0.33F;

Gdyby w programie użyto bezpośrednio wartości 0,186, konieczna byłaby zmiana wielu instrukcji.

3.2.3.3. Deklaracja zmiennych

Zmienna jest identyfikowana przez nazwę i odnosi się do typu danych. W języku C# rozróżnia się wielkie i małe litery. Zatem zmienne FIN i fin są różne.

Zmienne można zainicjować podczas ich deklaracji. Składnia deklaracji jednej lub kilku zmiennych jest następująca:

 Identificateur_de_type variable1[=valeur1],variable2=[valeur2],...;

gdzie Identificateur_de_type jest typem predefiniowanym lub typem zdefiniowanym przez programistę. Opcjonalnie zmienną można zainicjować w momencie jej deklaracji.

Można również nie określać dokładnego typu zmiennej, używając słowa kluczowego var zamiast Identificateur_de_type:

 var variable1=valeur1,variable2=valeur2,...;

Słowo kluczowe „var” nie oznacza, że zmienne nie mają określonego typu. Zmienna variablei ma typ danych valeuri, który został jej przypisany. Inicjalizacja jest tutaj obowiązkowa, aby kompilator mógł wywnioskować typ zmiennej.

Oto przykład:


using System;

namespace Chap1 {
    class P00 {
        static void Main(string[] args) {
            int i=2;
            Console.WriteLine("Type de int i=2 : {0},{1}",i.GetType().Name,i.GetType().FullName);
            var j = 3;
            Console.WriteLine("Type de var j=3 : {0},{1}", j.GetType().Name, j.GetType().FullName);
            var aujourdhui = DateTime.Now;
            Console.WriteLine("Type de var aujourdhui : {0},{1}", aujourdhui.GetType().Name, aujourdhui.GetType().FullName);
        }
    }
}
  • wiersz 6: dane o jawnie określonym typie
  • wiersz 7: (dane).GetType().Name to skrócona nazwa (dane), (dane).GetType().FullName to pełna nazwa (dane)
  • wiersz 8: dane z typem domyślnym. Ponieważ 3 jest typu int, j będzie typu int.
  • wiersz 10: dane typowane niejawnie. Ponieważ DateTime.Now jest typu DateTime, aujourdhui będzie typu DateTime.

Podczas wykonywania otrzymujemy następujący wynik:

1
2
3
Type de int i=2 : Int32,System.Int32
Type de var j=3 : Int32,System.Int32
Type de var aujourdhui : DateTime,System.DateTime

Zmienna, której typ został niejawnie określony za pomocą słowa kluczowego var, nie może następnie zmienić typu. W związku z tym nie można by napisać po linii 10 kodu następującej linii:


            var aujourdhui = "aujourd'hui";

W dalszej części zobaczymy, że w wyrażeniu można zadeklarować typ „w locie”. Jest to wówczas typ anonimowy, czyli typ, któremu użytkownik nie nadał nazwy. To kompilator nada nazwę temu nowemu typowi. Jeśli dane typu anonimowego mają zostać przypisane do zmiennej, jedynym sposobem jej zadeklarowania jest użycie słowa kluczowego var.

3.2.4. Konwersje między liczbami a ciągami znaków

nombre -> chaîne
nombre.ToString()
chaine -> int
int.Parse(ciąg) lub System.Int32.Parse
chaîne -> long
long.Parse(ciąg) lub System.Int64.Parse
chaîne -> double
double.Parse (łańcuch) lub System.Double.Parse (łańcuch)
chaîne -> float
float.Parse (ciąg znaków) lub System.Float.Parse (ciąg znaków)

Konwersja ciągu znaków na liczbę może się nie powieść, jeśli ciąg ten nie reprezentuje prawidłowej liczby. W takim przypadku generowany jest błąd krytyczny zwany wyjątkiem. Błąd ten można obsłużyć za pomocą następującej klauzuli try/catch:


try{
    appel de la fonction susceptible de générer l'exception
} catch (Exception e){
    traiter l'exception e
}
instruction suivante

Jeśli funkcja nie generuje wyjątku, przechodzi się do następnej instrukcji, w przeciwnym razie przechodzi się do treści klauzuli catch, a następnie do następnej instrukcji. Do kwestii obsługi wyjątków powrócimy w dalszej części. Oto program przedstawiający kilka technik konwersji między liczbami a ciągami znaków. W tym przykładzie funkcja affiche wyświetla na ekranie wartość swojego parametru. Tak więc affiche(S) wyświetla na ekranie wartość S, gdzie S jest typu string.


using System;

namespace Chap1 {
    class P01 {
        static void Main(string[] args) {

            // dane
            const int i = 10;
            const long l = 100000;
            const float f = 45.78F;
            double d = -14.98;

            // liczba --> ciąg znaków
            affiche(i.ToString());
            affiche(l.ToString());
            affiche(f.ToString());
            affiche(d.ToString());

            //wartość logiczna --> ciąg znaków
            const bool b = false;
            affiche(b.ToString());

            // ciąg znaków --> liczba całkowita
            int i1;
            i1 = int.Parse("10");
            affiche(i1.ToString());
            try {
                i1 = int.Parse("10.67");
                affiche(i1.ToString());
            } catch (Exception e) {
                affiche("Erreur : " + e.Message);
            }

            // ciąg znaków --> long
            long l1;
            l1 = long.Parse("100");
            affiche(l1.ToString());
            try {
                l1 = long.Parse("10.675");
                affiche(l1.ToString());
            } catch (Exception e) {
                affiche("Erreur : " + e.Message);
            }

            // ciąg znaków --> double
            double d1;
            d1 = double.Parse("100,87");
            affiche(d1.ToString());
            try {
                d1 = double.Parse("abcd");
                affiche(d1.ToString());
            } catch (Exception e) {
                affiche("Erreur : " + e.Message);
            }

            // ciąg znaków --> zmiennoprzecinkowa
            float f1;
            f1 = float.Parse("100,87");
            affiche(f1.ToString());
            try {
                d1 = float.Parse("abcd");
                affiche(f1.ToString());
            } catch (Exception e) {
                affiche("Erreur : " + e.Message);
            }

        }// koniec main

        public static void affiche(string S) {
            Console.Out.WriteLine("S={0}",S);
        }
    }// koniec klasy
}

W wierszach 30–32 obsługuje się ewentualny wyjątek, który może wystąpić. e.Message to komunikat o błędzie związany z wyjątkiem e.

Uzyskane wyniki są następujące:

S=10
S=100000
S=45,78
S=-14,98
S=False
S=10
S=Erreur : Input string was not in a correct format.
S=100
S=Erreur : Input string was not in a correct format.
S=100,87
S=Erreur : Input string was not in a correct format.
S=100,87
S=Erreur : Input string was not in a correct format.

Należy zauważyć, że liczby rzeczywiste w postaci ciągu znaków muszą być zapisane z przecinkiem, a nie kropką. Należy więc zapisać

double d1=10.7; 

ale

double d2=int.Parse("10,7");

3.2.5. Tablice danych

Tablica w języku C# to obiekt umożliwiający zgromadzenie danych tego samego typu pod jednym identyfikatorem. Jej deklaracja wygląda następująco:

Type[] tableau[]=new Type[n]

n to liczba danych, które może zawierać tablica. Składnia Tablica[i] odnosi się do danych o numerze i, gdzie i należy do przedziału [0,n-1]. Każde odwołanie do elementu Tabela[i], w którym i nie należy do przedziału [0,n-1], spowoduje wyjątek. Tabela może zostać zainicjowana w momencie jej deklaracji:

    int[] entiers=new int[] {0,10,20,30};

lub, prościej:

    int[] entiers={0,10,20,30};

Tablice posiadają właściwość Length, która określa liczbę elementów tablicy.

Tablicę dwuwymiarową można zadeklarować w następujący sposób:

Type[,] tablica = new Type[n,m];

gdzie n to liczba wierszy, a m to liczba kolumn. Składnia Tableau[i,j] oznacza element j w wierszu i tablicy. Tablicę dwuwymiarową można również zainicjować w momencie jej deklaracji:

    double[,] réels=new double[,] { {0.5, 1.7}, {8.4, -6}};

lub, prościej:

    double[,] réels={ {0.5, 1.7}, {8.4, -6}};

Liczbę elementów w każdym z wymiarów można uzyskać za pomocą metody GetLength(i), gdzie i=0 oznacza wymiar odpowiadający pierwszemu indeksowi, i=1 – wymiar odpowiadający drugiemu indeksowi, …

Całkowitą liczbę wymiarów uzyskuje się za pomocą właściwości Rank, a całkowitą liczbę elementów – za pomocą właściwości Length.

Tablica tablic jest deklarowana w następujący sposób:

Type[][] tablica = new Type[n][];

Powyższa deklaracja tworzy tablicę o n wierszach. Każdy element tablica[i] jest odwołaniem do tablicy jednowymiarowej. Odwołania te tablica[i] nie są inicjowane podczas powyższej deklaracji. Ich wartością jest odwołanie null.

Poniższy przykład ilustruje tworzenie tablicy tablic:


            // tablica tablic
            string[][] noms = new string[3][];
            for (int i = 0; i < noms.Length; i++) {
                noms[i] = new string[i + 1];
            }//for
            // inicjalizacja
            for (int i = 0; i < noms.Length; i++) {
                for (int j = 0; j < noms[i].Length; j++) {
                    noms[i][j] = "nom" + i + j;
                }//for j
            }//for i
  • wiersz 2: tablica noms zawierająca 3 elementy typu string[][]. Każdy element jest wskaźnikiem tablicy (odwołaniem do obiektu), której elementy są typu string[].
  • wiersze 3–5: trzy elementy tablicy typu noms są zainicjowane. Każdy z nich „wskazuje” teraz na tablicę elementów typu string[]. noms[i][j] jest elementem j tablicy typu string [], do której odwołuje się noms[i].
  • linia 9: inicjalizacja elementu noms[i][j] w ramach podwójnej pętli. W tym przypadku noms[i] jest tablicą zawierającą i+1 elementów. Ponieważ noms[i] jest tablicą, noms[i].Length oznacza liczbę jej elementów.

Oto przykład łączący trzy rodzaje tablic, które właśnie przedstawiliśmy:


using System;

namespace Chap1 {
    // tabele

    using System;

    // klasa testowa
    public class P02 {
        public static void Main() {
            // zainicjowana tablica jednowymiarowa
            int[] entiers = new int[] { 0, 10, 20, 30 };
            for (int i = 0; i < entiers.Length; i++) {
                Console.Out.WriteLine("entiers[{0}]={1}", i, entiers[i]);
            }//dla

            // zainicjowana tablica dwuwymiarowa
            double[,] réels = new double[,] { { 0.5, 1.7 }, { 8.4, -6 } };
            for (int i = 0; i < réels.GetLength(0); i++) {
                for (int j = 0; j < réels.GetLength(1); j++) {
                    Console.Out.WriteLine("réels[{0},{1}]={2}", i, j, réels[i, j]);
                }//for j
            }//for i

            // tablica tablic
            string[][] noms = new string[3][];
            for (int i = 0; i < noms.Length; i++) {
                noms[i] = new string[i + 1];
            }//dla
            // inicjalizacja
            for (int i = 0; i < noms.Length; i++) {
                for (int j = 0; j < noms[i].Length; j++) {
                    noms[i][j] = "nom" + i + j;
                }//dla j
            }//dla i
            // wyświetlanie
            for (int i = 0; i < noms.Length; i++) {
                for (int j = 0; j < noms[i].Length; j++) {
                    Console.Out.WriteLine("noms[{0}][{1}]={2}", i, j, noms[i][j]);
                }//dla j
            }//dla i
        }//Strona główna
    }//klasa
}//przestrzeń nazw

Podczas wykonywania otrzymujemy następujące wyniki:

entiers[0]=0
entiers[1]=10
entiers[2]=20
entiers[3]=30
réels[0,0]=0,5
réels[0,1]=1,7
réels[1,0]=8,4
réels[1,1]=-6
noms[0][0]=nom00
noms[1][0]=nom10
noms[1][1]=nom11
noms[2][0]=nom20
noms[2][1]=nom21
noms[2][2]=nom22

3.3. Podstawowe instrukcje języka C#

Rozróżniamy

1 polecenia podstawowe wykonywane przez komputer.

2 instrukcje sterujące przebiegiem programu.

Instrukcje podstawowe stają się jasne, gdy przyjrzymy się budowie mikrokomputera i jego urządzeń peryferyjnych.

  1. odczyt informacji z klawiatury

  2. przetwarzanie informacji

  3. zapisywanie informacji na ekranie

3.3.1. Zapis na ekranie

Istnieją różne instrukcje wyświetlania na ekranie:

Console.Out.WriteLine(expression)
Console.WriteLine(expression)
Console.Error.WriteLine (expression)

gdzie expression oznacza dowolny typ danych, który można przekonwertować na ciąg znaków w celu wyświetlenia na ekranie. Wszystkie obiekty w języku C# lub .NET posiadają metodę ToString(), która służy do przeprowadzenia tej konwersji.

Klasa System.Console zapewnia dostęp do operacji zapisu na ekranie (Write, WriteLine). Klasa Console posiada dwie właściwości: Out i Error, które są strumieniami zapisu typu TextWriter:

  • Console.WriteLine() jest równoważne Console.Out.WriteLine() i zapisuje dane do strumienia Out, który zazwyczaj jest powiązany z ekranem.
  • Console.Error.WriteLine() zapisuje dane do strumienia Error, który zazwyczaj jest również powiązany z ekranem.

Strumienie Out i Error można przekierować do plików tekstowych w trakcie wykonywania programu, co omówimy wkrótce.

3.3.2. Odczytywanie danych wprowadzonych z klawiatury

Strumień danych pochodzących z klawiatury jest reprezentowany przez obiekt Console.In typu TextReader. Ten typ obiektów umożliwia odczytanie wiersza tekstu za pomocą metody ReadLine:

    string ligne=Console.In.ReadLine();

Klasa Console udostępnia metodę ReadLine, domyślnie powiązaną ze strumieniem In. Można zatem napisać:

    string ligne=Console.ReadLine();

Wiersz wpisany z klawiatury jest zapisywany w zmiennej ligne i może być następnie wykorzystany przez program. Strumień In można przekierować do pliku, podobnie jak strumienie Out i Error.

3.3.3. Przykład operacji wejścia-wyjścia

Oto krótki program ilustrujący operacje wejścia i wyjścia z klawiatury/ekranu:


using System;

namespace Chap1 {
    // klasa testowa
    public class P03 {
        public static void Main() {

            // zapis do strumienia Out
            object obj = new object();
            Console.Out.WriteLine(obj);

            // zapis do strumienia Error
            int i = 10;
            Console.Error.WriteLine("i=" + i);

            // odczyt wiersza wprowadzonego z klawiatury
            Console.Write("Tapez une ligne : ");
            string ligne = Console.ReadLine();
            Console.WriteLine("ligne={0}", ligne);
        }//koniec funkcji main
    }//koniec klasy
}
  • wiersz 9: obj to odwołanie do obiektu
  • wiersz 10: obj jest wyświetlane na ekranie. Domyślnie wywoływana jest metoda obj.ToString().
  • wiersz 14: można również zapisać:

            Console.Error.WriteLine("i={0}",i);

Pierwszy parametr „i={0}” określa format wyświetlania, pozostałe parametry to wyrażenia, które mają zostać wyświetlone. Elementy {n} są parametrami „pozycyjnymi”. Podczas wykonywania parametr {n} jest zastępowany wartością wyrażenia o numerze n.

Wynik wykonania jest następujący:

1
2
3
4
System.Object
i=10
Tapez une ligne : je suis là
ligne=je suis là
  • wiersz 1: wynik wygenerowany przez wiersz 10 kodu. Metoda obj.ToString() wyświetliła nazwę typu zmiennej obj: System.Object. Typ object jest aliasem w języku C# dla typu .NET System.Object.

3.3.4. Przekierowanie wejścia/wyjścia

W ramach typów DOS i UNIX istnieją trzy standardowe urządzenia o nazwach:

  1. standardowe urządzenie wejściowe – domyślnie oznacza klawiaturę i ma numer 0
  2. standardowe urządzenie wyjściowe – domyślnie oznacza ekran i ma numer 1
  3. standardowe urządzenie błędów – domyślnie oznacza ekran i ma numer 2

W języku C# strumień zapisu Console.Out zapisuje dane na urządzeniu 1, strumień zapisu Console.Error zapisuje dane na urządzeniu 2, a strumień odczytu Console.In odczytuje dane z urządzenia 0.

Uruchamiając program w systemie DOS lub Unix, można określić, które urządzenia będą oznaczone jako 0, 1 i 2 dla uruchamianego programu. Rozważmy następujący wiersz poleceń:

pg arg1 arg2 .. argn

Za argumentami argi programu pg można przekierować standardowe urządzenia wejścia/wyjścia do plików:

0<in.txt
standardowy strumień wejściowy nr 0 jest przekierowany do pliku in.txt. W programie strumień Console.In będzie zatem pobierał dane z pliku in.txt.
1>out.txt
przekierowuje strumień wyjściowy nr 1 do pliku out.txt. W związku z tym w programie strumień Console.Out zapisze swoje dane do pliku out.txt
1>>out.txt
to samo, ale zapisane dane są dodawane do aktualnej zawartości pliku out.txt.
2>error.txt
przekierowuje wyjście nr 2 do pliku error.txt. W związku z tym w programie strumień Console.Error zapisze swoje dane w pliku error.txt
2>>error.txt
to samo, ale zapisane dane są dodawane do aktualnej zawartości pliku error.txt.
1>out.txt 2>error.txt
Urządzenia 1 i 2 są przekierowane do plików

Należy zauważyć, że aby przekierować strumienie wejścia/wyjścia programu pg do plików, nie ma potrzeby modyfikowania programu pg. To system operacyjny określa charakter urządzeń 0, 1 i 2. Rozważmy następujący program:


using System;

namespace Chap1 {

    // przekierowania
    public class P04 {
        public static void Main(string[] args) {
            // odczyt strumienia In
            string data = Console.In.ReadLine();
            // zapis strumienia Out
            Console.Out.WriteLine("écriture dans flux Out : " + data);
            // zapis strumienia Error
            Console.Error.WriteLine("écriture dans flux Error : " + data);
        }//Główna
    }//klasa
}

Wygenerujmy plik wykonywalny na podstawie tego kodu źródłowego:

  • w [1]: plik wykonywalny tworzy się poprzez kliknięcie prawym przyciskiem myszy na projekcie / Build
  • w [2]: w oknie DOS plik wykonywalny 04.exe został utworzony w folderze bin/Release projektu.

W oknie DOS [2] wpiszmy następujące polecenia:

1
2
3
4
5
6
7
8
...\04\bin\Release>echo test >in.txt
...\04\bin\Release>more in.txt
test
...\04\bin\Release>04 0<in.txt 1>out.txt 2>err.txt
...\04\bin\Release>more out.txt
écriture dans flux Out : test
...\04\bin\Release>more err.txt
écriture dans flux Error : test
  • wiersz 1: umieszczamy ciąg znaków test w pliku in.txt
  • wiersze 2–3: wyświetlamy zawartość pliku in.txt w celu weryfikacji
  • wiersz 4: uruchomienie programu 04.exe. Strumień In jest przekierowywany do pliku in.txt, strumień Out do pliku out.txt, strumień Error do pliku err.txt. Wykonanie nie powoduje wyświetlenia żadnych danych.
  • wiersze 5–6: zawartość pliku out.txt. Z tej zawartości wynika, że:
  • plik in.txt został odczytany
  • wyjście na ekran zostało przekierowane do pliku out.txt
  • wiersze 7–8: analogiczna weryfikacja dla pliku err.txt

Wyraźnie widać, że strumienie Out i In nie zapisują danych na tych samych urządzeniach, ponieważ udało się je przekierować oddzielnie.

3.3.5. Przypisanie wartości wyrażenia do zmiennej

W tym miejscu skupiamy się na operacji variable=expression;

Wyrażenie może być typu: arytmetycznego, relacyjnego, boolowskiego, znakowego

3.3.5.1. Interpretacja operacji przypisania

Operacja variable=expression;

jest sama w sobie wyrażeniem, którego obliczenie przebiega w następujący sposób:

  • oceniana jest prawa strona operacji przypisania: wynikiem jest wartość V.
  • wartość V jest przypisywana do zmiennej
  • wartość V jest również wartością operacji przypisania, traktowanej tym razem jako wyrażenie.

W ten sposób operacja

    V1=V2=expression

jest poprawna. Ze względu na kolejność operacji najpierw zostanie obliczony operator = znajdujący się najbardziej po prawej stronie. Mamy zatem

    V1=(V2=expression)

Wyrażenie V2=wyrażenie jest obliczane i ma wartość V. Obliczenie tego wyrażenia spowodowało przypisanie wartości V2 do zmiennej V. Następny operator = jest wówczas obliczany w postaci:

    V1=V

Wartością tego wyrażenia jest nadal V. Jego obliczenie powoduje przypisanie wartości V do V1.

Zatem operacja V1=V2=wyrażenie

jest wyrażeniem, którego obliczenie

  • powoduje przypisanie wartości expression do zmiennych V1 i V2
  • zwraca wartość expression.

Można to uogólnić na wyrażenie typu:

V1=V2=....=Vn=expression

3.3.5.2. Wyrażenie arytmetyczne

Operatory wyrażeń arytmetycznych są następujące:

  • dodawanie

  • odejmowanie

* mnożenie

/ dzielenie: wynikiem jest iloraz dokładny, jeśli co najmniej jeden z operandów jest liczbą rzeczywistą. Jeśli oba operandy są liczbami całkowitymi, wynikiem jest iloraz całkowity. Tak więc 5/2 -> 2, a 5,0/2 -> 2,5.

% dzielenie: wynikiem jest reszta niezależnie od rodzaju operandów, a iloraz jest liczbą całkowitą. Jest to zatem operacja modulo.

Istnieje wiele funkcji matematycznych. Oto kilka z nich:

double Sqrt(double x)
pierwiastek kwadratowy
double Cos(double x)
cosinus
double Sin(double x)
Sinus
double Tan(double x)
Tangens
double Pow(double x,double y)
x do potęgi y (x>0)
double Exp(double x)
Funkcja wykładnicza
double Log(double x)
Logarytm naturalny
double Abs(double x)
wartość bezwzględna

itd...

Wszystkie te funkcje są zdefiniowane w klasie C# o nazwie Math. Korzystając z nich, należy poprzedzić je nazwą klasy, w której są zdefiniowane. W ten sposób napiszemy:

double x, y=4;
x=Math.Sqrt(y);

Pełna definicja klasy Math wygląda następująco:

Image

Image

Image

Image

Image

3.3.5.3. Priorytety w obliczaniu wyrażeń arytmetycznych

Kolejność operatorów podczas obliczania wyrażenia arytmetycznego jest następująca (od najwyższego do najniższego priorytetu):

[fonctions], [ ( )],[ *, /, %], [+, -]

Operatory z tego samego bloku [ ] mają tę samą kolejność.

3.3.5.4. Wyrażenia relacyjne

Dostępne są następujące operatory:

       <, <=, ==, !=, >, >=

Priorytety operatorów

>, >=, <, <=
==, !=

Wynikiem wyrażenia relacyjnego jest wartość logiczna false, jeśli wyrażenie jest fałszywe, w przeciwnym razie true.

      bool fin;
      int x=...;
      fin=x>4;

Porównanie dwóch znaków

Niech będą dwa znaki C1 i C2. Można je porównać za pomocą operatorów

    <, <=, ==, !=, >, >=

Porównywane są wówczas ich kody Unicode, które są liczbami. Zgodnie z kolejnością Unicode zachodzą następujące relacje:

espace < .. < '0' < '1' < .. < '9' < .. < 'A' < 'B' < .. < 'Z' < .. < 'a' < 'b' < .. <'z'

Porównanie dwóch ciągów znaków

Są one porównywane znak po znaku. Pierwsza napotkana nierówność między dwoma znakami powoduje nierówność o tym samym znaczeniu w odniesieniu do ciągów znaków.

Przykłady:

Załóżmy, że porównujemy ciągi „Kot” i „Pies”

Ta ostatnia nierówność pozwala stwierdzić, że „Chat” < „Chien”.

Załóżmy, że porównujemy ciągi „Chat” i „Chaton”. Równość występuje przez cały czas, aż do wyczerpania ciągu „Chat”. W tym przypadku wyczerpany ciąg zostaje uznany za „mniejszy”. Mamy zatem relację

    "Chat" < "Chaton".

Funkcje porównywania dwóch ciągów znaków

Do sprawdzenia, czy dwa ciągi znaków są równe, czy nie, można użyć operatorów relacyjnych == i != lub metody Equals klasy System.String. W przypadku relacji < <= > >= należy użyć metody CompareTo klasy System.String:


using System;

namespace Chap1 {
    class P05 {
        static void Main(string[] args) {
            string chaine1="chat", chaine2="chien";
            int n = chaine1.CompareTo(chaine2);
            bool egal = chaine1.Equals(chaine2);
            Console.WriteLine("i={0}, egal={1}", n, egal);
            Console.WriteLine("chien==chaine1:{0},chien!=chaine2:{1}", "chien"==chaine1,"chien" != chaine2);
        }
    }
}

W wierszu 7 zmienna i będzie miała wartość:

0 , jeśli oba ciągi znaków są równe

1 jeśli ciąg nr 1 > ciąg nr 2

-1 jeśli ciąg nr 1 < ciąg nr 2

W wierszu 8 zmienna egal przyjmie wartość true, jeśli oba ciągi są równe, a w przeciwnym razie – false. W wierszu 10 używa się operatorów == i != do sprawdzenia, czy dwa ciągi są równe, czy nie.

Wyniki wykonania:

i=-1, egal=False
chien==chaine1:False,chien!=chaine2:False

3.3.5.5. Wyrażenia boolowskie

Dostępne operatory to AND (&&) OR (||) NOT (!). Wynikiem wyrażenia boolowskiego jest wartość logiczna.

Priorytety operatorów:

  1. !
  2. &&
  3. ||

            double x = 3.5;
            bool valide = x > 2 && x < 4;

Operatory relacyjne mają pierwszeństwo przed operatorami && i ||.

3.3.5.6. Operacje bitowe

Operatory

Niech i i j będą dwiema liczbami całkowitymi.

i<<n
przesuwa i o n bitów w lewo. Bity wejściowe to zera.
i>>n
przesuwa i o n bitów w prawo. Jeśli i jest liczbą całkowitą ze znakiem (signed char, int, long), bit znaku pozostaje zachowany.
i & j
wykonuje operację logicznego OR bit po bicie na i i j.
i | j
wykonuje operację logicznego bit po bicie między i a j (OU).
~i
uzupełnia i do 1
i^j
tworzy OU EXCLUSIF z i i j

Niech będzie następujący kod:


short i = 100, j = -13;
ushort k = 0xF123;
Console.WriteLine("i=0x{0:x4}, j=0x{1:x4}, k=0x{2:x4}", i,j,k);
Console.WriteLine("i<<4=0x{0:x4}, i>>4=0x{1:x4},k>>4=0x{2:x4},i&j=0x{3:x4},i|j=0x{4:x4},~i=0x{5:x4},j<<2=0x{6:x4},j>>2=0x{7:x4}", i << 4, i >> 4, k >> 4, (short)(i & j), (short)(i | j), (short)(~i), (short)(j << 2), (short)(j >> 2));
  • format {0:x4} wyświetla parametr nr 0 w formacie szesnastkowym (x) z 4 znakami (4).

Wyniki wykonania są następujące:

i=0x0064, j=0xfff3, k=0xf123
i<<4=0x0640, i>>4=0x0006,k>>4=0x0f12,i&j=0x0060,i|j=0xfff7,~i=0xff9b,j<<2=0xffcc,j>>2=0xfffc

3.3.5.7. Kombinacja operatorów

a=a+b można zapisać jako a+=b

a=a-b można zapisać jako a-=b

To samo dotyczy operatorów /, %, *, <<, >>, &, |, ^. Tak więc a=a/2; można zapisać jako a/=2;

3.3.5.8. Operatory inkrementacji i dekrementacji

Zapis variable++ oznacza variable=variable+1 lub też variable+=1

Zapis variable-- oznacza variable=variable-1 lub też variable-=1.

3.3.5.9. Operator trójargumentowy?

Wyrażenie

    expr_cond ? expr1:expr2

ocenia się w następujący sposób:

1 wyrażenie expr_cond jest oceniane. Jest to wyrażenie warunkowe o wartości vrai lub faux

2 Jeśli wyrażenie jest prawdziwe, jego wartością jest expr1, a wyrażenie expr2 nie jest obliczane.

3 Jeśli jest fałszywe, dzieje się odwrotnie: wartością wyrażenia jest expr2, a expr1 nie jest obliczane.

Operacja i=(j>4 ? j+1:j-1); przypisze do zmiennej i wartość: j+1, jeśli j>4, w przeciwnym razie j-1. Jest to to samo, co zapis if(j>4) i=j+1; else i=j-1;, ale jest bardziej zwięzłe.

3.3.5.10. Ogólna kolejność operacji

() []  fonction                  
gd
! ~ ++ --                        
dg
new (type) opérateurs cast       
dg
*  /  %                          
gd
+  -                             
gd
<<  >>                           
gd
< <=  > >= instanceof            
gd
==    !=                         
gd
&                                
gd
^                                
gd
|                                
gd
&&                               
gd
||                               
gd
?   :                            
dg
= += -= etc. .                   
dg

gd oznacza, że przy równej priorytetowości stosowana jest reguła „od lewej do prawej”. Oznacza to, że gdy w wyrażeniu występują operatory o tej samej priorytetowości, w pierwszej kolejności obliczany jest operator znajdujący się najbardziej po lewej stronie wyrażenia. dg oznacza regułę „od prawej do lewej”.

3.3.5.11. Zmiany typu

W wyrażeniu można tymczasowo zmienić typ danej wartości. Nazywa się to rzutowaniem typu (ang. type casting). Składnia zmiany typu wartości w wyrażeniu jest następująca:

    (type) valeur

Wartość przyjmuje wówczas wskazany typ. Powoduje to zmianę reprezentacji wartości.


using System;

namespace Chap1 {
    class P06 {
        static void Main(string[] args) {
            int i = 3, j = 4;
            float f1=i/j;
            float f2=(float)i/j;
            Console.WriteLine("f1={0}, f2={1}",f1,f2);
        }
    }
}
  • w wierszu 7 wartość f1 będzie wynosić 0,0. Działanie dzielenia 3/4 jest dzieleniem całkowitym, ponieważ oba operandy są typu int.
  • w wierszu 8 (float)i to wartość i przekształcona na float. Teraz mamy dzielenie liczby rzeczywistej typu float przez liczbę całkowitą typu int. Następnie wykonywane jest dzielenie liczb rzeczywistych. Wartość j zostanie również przekształcona na typ float, a następnie zostanie wykonane dzielenie tych dwóch liczb rzeczywistych. f2 będzie wówczas miało wartość 0,75.

Oto wyniki wykonania:

f1=0, f2=0,75

W operacji (float)i:

  • i jest wartością zakodowaną dokładnie na 2 bajty
  • (float), natomiast i jest tą samą wartością zakodowaną w przybliżeniu jako liczba zmiennoprzecinkowa na 4 bajty

Następuje zatem transkodowanie wartości i. Transkodowanie to trwa jedynie przez czas trwania obliczeń, przy czym zmienna i zachowuje zawsze swój typ int.

3.4. Instrukcje kontrolujące przebieg programu

3.4.1. Zatrzymanie

Metoda Exit zdefiniowana w klasie Environment umożliwia zatrzymanie wykonywania programu.

syntaxe        void Exit(int status)
action        arrête le processus en cours et rend la valeur status au processus père

Exit powoduje zakończenie bieżącego procesu i zwraca kontrolę do procesu wywołującego. Wartość zmiennej status może zostać wykorzystana przez ten proces. W przypadku DOS ta zmienna statusowa jest zwracana do zmiennej systemowej ERRORLEVEL, której wartość można sprawdzić w pliku wsadowym. W systemie Unix, przy użyciu interpretera poleceń Bourne Shell, wartość status jest przechwytywana przez zmienną $?.

    Environment.Exit(0);

przerwie wykonywanie programu, zwracając wartość stanu równą 0.

3.4.2. Prosta struktura wyboru

 syntaxe :  if (condition) {actions_condition_vraie;} else {actions_condition_fausse;}

uwagi:

  • warunek jest ujęty w nawiasy.
  • każda akcja kończy się średnikiem.
  • nawiasy klamrowe nie są zakończone średnikiem.
  • nawiasy klamrowe są konieczne tylko wtedy, gdy występuje więcej niż jedna akcja.
  • klauzula else może nie występować.
  • Nie ma klauzuli then.

Algorytmicznym odpowiednikiem tej struktury jest struktura „jeśli … to … w przeciwnym razie”:

przykład


    if (x>0)  { nx=nx+1;sx=sx+x;} else dx=dx-x;

Struktury wyboru można zagnieżdżać:

if(condition1)
if (condition2)
        {......}
      else         //warunek2
         {......}
else         //warunek1
     {.......}

Czasami pojawia się następujący problem:


using System;

namespace Chap1 {
    class P07 {
        static void Main(string[] args) {
            int n = 5;
            if (n > 1)
                if (n > 6)
                    Console.Out.WriteLine(">6");
                else Console.Out.WriteLine("<=6");
        }
    }
}

W powyższym przykładzie, do którego if odnosi się else z wiersza 10? Zasada jest taka, że else zawsze odnosi się do najbliższego if: w tym przykładzie jest to if(n>6), w wierszu 8. Rozważmy inny przykład:


            if (n2 > 1) {
                if (n2 > 6) Console.Out.WriteLine(">6");
       } else Console.Out.WriteLine("<=1");    

W tym przypadku chcieliśmy przypisać else do if(n2>1), a nie else do if(n2>6). Z uwagi na poprzednią uwagę jesteśmy zmuszeni użyć nawiasów klamrowych w wyrażeniu if(n2>1) {...} else ...

3.4.3. Struktura warunkowa

Składnia jest następująca:

switch(expression) {
    case v1:     
            actions1;
            break;
    case v2:     
            actions2;
            break;
         . .. .. .. .. ..
    default:     
            actions_sinon;
            break;
}

uwagi

  • wartość wyrażenia kontrolnego dla switch może być liczbą całkowitą, znakiem lub ciągiem znaków
  • Wyrażenie kontrolne jest ujęte w nawiasy.
  • Klauzula default może nie występować.
  • Wartości vi są możliwymi wartościami wyrażenia. Jeśli wartość wyrażenia wynosi vi, wykonywane są działania określone w klauzuli case vi.
  • Instrukcja break powoduje wyjście ze struktury case.
  • Każdy blok instrukcji powiązany z wartością vi musi kończyć się instrukcją rozgałęzienia (break, goto, return itp.), w przeciwnym razie kompilator zgłasza błąd.

Przykład

W algorytmice

                selon la valeur de choix
                    cas 0
                         fin du module
                    cas 1
                         exécuter module M1
                    cas 2
                        exécuter module M2
                    sinon
                         erreur<--vrai
                findescas

W języku C#

int choix = 2; 
bool erreur = false;
switch (choix) {
   case 0: return;
   case 1: M1(); break;
   case 2: M2(); break;
   default: erreur = true; break;
}
}// koniec Main

static void M1() {
    Console.WriteLine("M1");
}

static void M2() {
    Console.WriteLine("M2");
}
}

3.4.4. Struktury powtórzeń

3.4.4.1. Znana liczba powtórzeń

Struktura for

Składnia jest następująca:


    for (i=id;i<=if;i=i+ip){ 
       actions; 
      } 

Uwagi

  • Trzy argumenty funkcji for znajdują się w nawiasie i są oddzielone średnikami.
  • Każda akcja w for kończy się średnikiem.
  • Nawias klamrowy jest konieczny tylko wtedy, gdy występuje więcej niż jedna akcja.
  • Po nawiasie klamrowym nie następuje średnik.

Algorytmicznym odpowiednikiem jest struktura pour:

pour i variant de id à if avec un pas de ip
    actions
finpour

co można przetłumaczyć na strukturę tantque:

    i  id
    tantque i<=if
        actions
        i i+ip
    fintantque

Struktura foreach

Składnia jest następująca:


foreach (Type variable in collection)
    instructions; 
}

Uwagi

  • collection jest kolekcją obiektów, którą można wyliczyć. Kolekcją obiektów, którą już znamy, jest tablica
  • Type to typ obiektów w tej kolekcji. W przypadku tablicy byłby to typ elementów tej tablicy
  • variable jest zmienną lokalną pętli, która będzie kolejno przyjmować wszystkie wartości z kolekcji.

Zatem poniższy kod:


            string[] amis = { "paul", "hélène", "jacques", "sylvie" };
            foreach (string nom in amis) {
                Console.WriteLine(nom);
         }

wyświetliłby:

paul
hélène
jacques
sylvie

3.4.4.2. Nieznana liczba powtórzeń

W języku C# istnieje wiele struktur przeznaczonych do tego celu.

Struktura „while”


    while(condition){
          actions;
        } 

Pętla jest wykonywana tak długo, jak długo warunek jest spełniony. Pętla może nigdy nie zostać wykonana.

Uwagi:

  • warunek jest ujęty w nawiasy.
  • każda akcja kończy się średnikiem.
  • nawias klamrowy jest konieczny tylko wtedy, gdy występuje więcej niż jedna akcja.
  • po nawiasie klamrowym nie stawia się średnika.

Odpowiadającą jej strukturą algorytmiczną jest struktura „tantque”:

tantque condition
    actions
fintantque

Pętla „powtarzaj do” (do while)

Składnia jest następująca:


    do{
       instructions;
    }while(condition); 

Pętla jest wykonywana, dopóki warunek nie stanie się fałszywy. W tym przypadku pętla jest wykonywana co najmniej raz.

Uwagi

  • warunek jest ujęty w nawiasy.
  • Każda akcja kończy się średnikiem.
  • Nawias klamrowy jest konieczny tylko wtedy, gdy występuje więcej niż jedna instrukcja.
  • po nawiasie klamrowym nie stawia się średnika.

Odpowiadającą jej strukturą algorytmiczną jest struktura „powtarzaj … aż do”:

répéter
    actions
jusqu'à condition

Struktura pętli for

Składnia jest następująca:


for(instructions_départ;condition;instructions_fin_boucle){
    instructions; 
}

Pętla jest wykonywana tak długo, jak długo warunek jest prawdziwy (oceniany przed każdą iteracją pętli). Instructions_départ są wykonywane przed pierwszym wejściem do pętli. Instructions_fin_boucle są wykonywane po każdej iteracji pętli.

uwagi

  • poszczególne instrukcje w instructions_depart i instructions_fin_boucle są oddzielone przecinkami.

Odpowiednia struktura algorytmiczna wygląda następująco:

instructions_départ
tantque condition
    actions
    instructions_fin_boucle
fintantque

Przykłady

Poniższe fragmenty kodu obliczają sumę pierwszych 10 liczb całkowitych.

            int i, somme, n=10;
            for (i = 1, somme = 0; i <= n; i = i + 1)
                somme = somme + i;

            for (i = 1, somme = 0; i <= n; somme = somme + i, i = i + 1) ;

            i = 1; somme = 0;
            while (i <= n) { somme += i; i++; }

            i = 1; somme = 0;
            do somme += i++;
            while (i <= n);

3.4.4.3. Instrukcje dotyczące zarządzania pętlą

break
wyjście z pętli for, while, do ... while.
continue
przechodzi do następnej iteracji pętli for, while, do ... while

3.5. Obsługa wyjątków

Wiele funkcji języka C# może generować wyjątki, czyli błędy. Gdy funkcja może wygenerować wyjątek, programista powinien go obsłużyć, aby uzyskać programy bardziej odporne na błędy: należy zawsze unikać niekontrolowanego „zawieszenia” aplikacji.

Obsługa wyjątku odbywa się zgodnie z następującym schematem:

try{
    code susceptible de générer une exception
} catch (Exception e){
    traiter l'exception e
}
instruction suivante

Jeśli funkcja nie generuje wyjątku, przechodzi się do następnej instrukcji; w przeciwnym razie przechodzi się do treści klauzuli catch, a następnie do następnej instrukcji. Należy zwrócić uwagę na następujące kwestie:

  • e jest obiektem typu Exception lub jego pochodną. Można być bardziej precyzyjnym, używając typów takich jak IndexOutOfRangeException, FormatException, SystemException itp.: istnieje wiele typów wyjątków. Pisząc catch (Exception e), wskazujemy, że chcemy obsłużyć wszystkie typy wyjątków. Jeśli kod klauzuli try może generować kilka typów wyjątków, możemy chcieć być bardziej precyzyjni, obsługując wyjątek za pomocą kilku klauzul catch:
try{
    code susceptible de générer les exceptions
} catch ( IndexOutOfRangeException e1){
    traiter l'exception e1
}
} catch ( FormatException e2){
    traiter l'exception e2
}
instruction suivante
  • Do klauzul try/catch można dodać klauzulę finally:
try{
    code susceptible de générer une exception
} catch (Exception e){
    traiter l'exception e
}
finally{
    code exécuté après try ou catch
}
instruction suivante

Niezależnie od tego, czy wystąpi wyjątek, czy nie, kod klauzuli finally zostanie zawsze wykonany.

  • W klauzuli catch może się zdarzyć, że nie będziemy chcieli korzystać z dostępnego obiektu Exception. Zamiast pisać catch (Exception e){..}, piszemy wówczas catch(Exception){...} lub, prościej, catch {...}.
  • Klasa Exception posiada właściwość Message, która zawiera komunikat opisujący wystąpiły błąd. Jeśli więc chcemy go wyświetlić, napiszemy:
catch (Exception ex){
    Console.WriteLine("L'erreur suivante s'est produite : {0}",ex.Message);
    ...
}//catch
  • Klasa Exception posiada metodę ToString, która zwraca ciąg znaków wskazujący typ wyjątku oraz wartość właściwości Message. Można zatem napisać:
catch (Exception ex){
    Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.ToString());
    ...
}//catch

Można również zapisać:

catch (Exception ex){
    Console.WriteLine("L'erreur suivante s'est produite : {0}",ex);
    ...
}//catch

Kompilator przypisze parametrowi {0} wartość ex.ToString().

Poniższy przykład przedstawia wyjątek wygenerowany w wyniku użycia nieistniejącego elementu tablicy:


using System;

namespace Chap1 {
    class P08 {
        static void Main(string[] args) {
            // deklaracja i inicjalizacja tablicy
            int[] tab = { 0, 1, 2, 3 };
            int i;
            // wyświetlanie tablicy za pomocą pętli for
            for (i = 0; i < tab.Length; i++)
                Console.WriteLine("tab[{0}]={1}", i, tab[i]);
            // wyświetlanie tablicy za pomocą pętli for each
            foreach (int élmt in tab) {
                Console.WriteLine(élmt);
            }
            // generowanie wyjątku
            try {
                tab[100] = 6;
            } catch (Exception e) {
                Console.Error.WriteLine("L'erreur suivante s'est produite : " + e);
                return;
            }//try-catch
            finally {
                Console.WriteLine("finally ...");
            }
        }
    }
}

W powyższym przykładzie wiersz 18 wygeneruje wyjątek, ponieważ tablica tab nie zawiera elementu o numerze 100. Wykonanie programu daje następujące wyniki:

tab[0]=0
tab[1]=1
tab[2]=2
tab[3]=3
0
1
2
3
L'erreur suivante s'est produite : System.IndexOutOfRangeException: L'index se trouve en dehors des limites du tableau.
   à Chap1.P08.Main(String[] args) dans C:\data\travail\2007-2008\c# 2008\poly\Chap1\08\Program.cs:wiersz 7
finally ...
  • wiersz 9: wystąpił wyjątek [System.IndexOutOfRangeException]
  • wiersz 11: wykonano klauzulę finally (wiersze 23–25) kodu, mimo że w wierszu 21 znajdowała się instrukcja return nakazująca wyjście z metody. Należy zauważyć, że klauzula finally jest zawsze wykonywana.

Oto kolejny przykład, w którym obsługujemy wyjątek spowodowany przypisaniem ciągu znaków do zmiennej typu całkowitego, gdy ciąg ten nie reprezentuje liczby całkowitej:


using System;

namespace Chap1 {
    class P08 {
        static void Main(string[] args) {

            // przykład 2
            // Proszę o podanie imienia
            Console.Write("Nom : ");
            // odczyt odpowiedzi
            string nom = Console.ReadLine();
            // pyta się o wiek
            int age = 0;
            bool ageOK = false;
            while (!ageOK) {
                // pytanie
                Console.Write("âge : ");
                // odczyt i weryfikacja odpowiedzi
                try {
                    age = int.Parse(Console.ReadLine());
                    ageOK = age>=1;
                } catch {
                }//try-catch
                if (!ageOK) {
                    Console.WriteLine("Age incorrect, recommencez...");
                }
            }//while
            // wyświetlanie wyniku końcowego
            Console.WriteLine("Vous vous appelez {0} et vous avez {1} an(s)",nom,age);
        }
    }
}
  • wiersze 15–27: pętla służąca do wprowadzania wieku osoby
  • wiersz 20: wiersz wpisany z klawiatury jest przekształcany na liczbę całkowitą za pomocą metody int.Parse. Metoda ta zgłasza wyjątek, jeśli konwersja nie jest możliwa. Dlatego operacja ta została umieszczona w bloku try / catch.
  • wiersze 22–23: jeśli zostanie zgłoszony wyjątek, program przechodzi do bloku catch, gdzie nie wykonuje się żadnych czynności. W ten sposób wartość logiczna ageOK, ustawiona na false w wierszu 14, pozostanie na poziomie false.
  • wiersz 21: jeśli dotrzemy do tego wiersza, oznacza to, że konwersja typu string na int zakończyła się powodzeniem. Sprawdzamy jednak, czy uzyskana liczba całkowita jest większa lub równa 1.
  • wiersze 24–26: jeśli wiek jest nieprawidłowy, wyświetlany jest komunikat o błędzie.

Przykładowe wyniki wykonania:

1
2
3
Nom : dupont
âge : 23
Vous vous appelez dupont et vous avez 23 an(s)
1
2
3
4
5
6
7
Nom : durand
âge : x
Age incorrect, recommencez...
âge : -4
Age incorrect, recommencez...
âge : 12
Vous vous appelez durand et vous avez 12 an(s)

3.6. Przykładowa aplikacja – V1

Zamierzamy napisać program umożliwiający obliczenie podatku podatnika. Rozważamy uproszczony przypadek podatnika, który ma do zadeklarowania wyłącznie swoje wynagrodzenie (dane z 2004 r. dotyczące dochodów z 2003 r.):

  • obliczamy liczbę udziałów pracownika nbParts = nbEnfants/2 + 1, jeśli jest on stanu wolnego, nbEnfants/2 + 2, jeśli jest żonaty, gdzie nbEnfants oznacza liczbę jego dzieci.
  • jeśli ma co najmniej troje dzieci, przysługuje mu dodatkowa połowa udziału
  • jego dochód podlegający opodatkowaniu oblicza się jako R = 0,72 × S, gdzie S to jego roczne wynagrodzenie
  • oblicza się jego współczynnik rodzinny QF = R/nbParts
  • oblicza się podatek I. Rozważmy następującą tabelę:
4262
0
0
8382
0,0683
291,09
14753
0,1914
1322,92
23 888
0,2826
2668,39
38 868
0,3738
4846,98
47932
0,4262
6883,66
0
0,4809
9505,54

Każdy wiersz zawiera 3 pola. Aby obliczyć podatek I, należy znaleźć pierwszy wiersz, w którym QF <= pole1. Na przykład, jeśli QF = 5000, znajdziemy wiersz

    8382        0.0683        291.09

Podatek I wynosi wówczas 0,0683*R – 291,09*nbParts. Jeśli QF jest takie, że warunek QF <= pole1 nigdy nie jest spełniony, wówczas stosuje się współczynniki z ostatniego wiersza. W tym przypadku:

0 0,4809 9505,54

co daje podatek I = 0,4809*R – 9505,54*nbParts.

Odpowiedni program w języku C# wygląda następująco:


using System;

namespace Chap1 {
    class Impots {
        static void Main(string[] args) {
            // tabele danych niezbędne do obliczenia podatku
            decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
            decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
            decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };

            // pobieranie stanu cywilnego
            bool OK = false;
            string reponse = null;
            while (!OK) {
                Console.Write("Etes-vous marié(e) (O/N) ? ");
                reponse = Console.ReadLine().Trim().ToLower();
                if (reponse != "o" && reponse != "n")
                    Console.Error.WriteLine("Réponse incorrecte. Recommencez");
                else OK = true;
            }//while
            bool marie = reponse == "o";

            // liczba dzieci
            OK = false;
            int nbEnfants = 0;
            while (!OK) {
                Console.Write("Nombre d'enfants : ");
                try {
                    nbEnfants = int.Parse(Console.ReadLine());
                    OK = nbEnfants >= 0;
                } catch {
                }// try
                if (!OK) {
                    Console.WriteLine("Réponse incorrecte. Recommencez");
                }
            }// while

            // wynagrodzenie
            OK = false;
            int salaire = 0;
            while (!OK) {
                Console.Write("Salaire annuel : ");
                try {
                    salaire = int.Parse(Console.ReadLine());
                    OK = salaire >= 0;
                } catch {
                }// try
                if (!OK) {
                    Console.WriteLine("Réponse incorrecte. Recommencez");
                }
            }// while

            // obliczenie liczby udziałów
            decimal nbParts;
            if (marie) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;

            // dochód podlegający opodatkowaniu
            decimal revenu = 0.72M * salaire;

            // współczynnik rodzinny
            decimal QF = revenu / nbParts;

            // wyszukiwanie przedziału podatkowego odpowiadającego QF
            int i;
            int nbTranches = limites.Length;
            limites[nbTranches - 1] = QF;
            i = 0;
            while (QF > limites[i]) i++;
            // podatek
            int impots = (int)(coeffR[i] * revenu - coeffN[i] * nbParts);

            // wyświetlanie wyniku
            Console.WriteLine("Impôt à payer : {0} euros", impots);
        }
    }
}
  • wiersze 7–9: wartości liczbowe mają na końcu sufiks M (Money), aby miały typ decimal.
  • wiersz 16:
  • Console.ReadLine() zwraca ciąg znaków C1 wpisany z klawiatury
  • C1.Trim() usuwa spacje na początku i na końcu ciągu C1 – zwraca ciąg C2
  • C2.ToLower() zwraca ciąg znaków C3, który jest ciągiem C2 przekształconym na małe litery.
  • wiersz 21: wartość logiczna marie przyjmuje wartość true lub false z relacji reponse=="o"
  • wiersz 29: ciąg znaków wpisany z klawiatury jest przekształcany na typ int. Jeśli przekształcenie się nie powiedzie, zgłaszany jest wyjątek.
  • wiersz 30: wartość logiczna OK przyjmuje wartość true lub false z relacji nbEnfants>=0
  • wiersze 55–56: nie można po prostu zapisać wyrażenia nbEnfants/2. Gdyby nbEnfants było równe 3, mielibyśmy 3/2, czyli dzielenie całkowite, którego wynikiem byłoby 1, a nie 1,5. Dlatego zapisujemy (decimal)nbEnfants, aby jeden z operandów dzielenia stał się liczbą rzeczywistą i w ten sposób uzyskać dzielenie między liczbami rzeczywistymi.

Oto przykłady wykonania:

Etes-vous marié(e) (O/N) ? o
Nombre d'enfants : 2
Salaire annuel : 60000
Impôt à payer : 4282 euros
Etes-vous marié(e) (O/N) ? oui
Réponse incorrecte. Recommencez
Etes-vous marié(e) (O/N) ? o
Nombre d'enfants : trois
Réponse incorrecte. Recommencez
Nombre d'enfants : 3
Salaire annuel : 60000 euros
Réponse incorrecte. Recommencez
Salaire annuel : 60000
Impôt à payer : 2959 euros

3.7. Argumenty programu głównego

Główna funkcja Main może przyjmować jako parametr tablicę ciągów znaków: String[] (lub string[]). Tablica ta zawiera argumenty wiersza poleceń użytego do uruchomienia aplikacji. Jeśli więc uruchomimy program P za pomocą następującego polecenia (DOS):


        P arg0 arg1 … argn

i jeśli funkcja Main jest zadeklarowana w następujący sposób:

public static void Main(string[] args)

to otrzymamy args[0]="arg0", args[1]="arg1" … Oto przykład:


using System;

namespace Chap1 {
    class P10 {
        static void Main(string[] args) {
            // wyświetla listę otrzymanych parametrów
            Console.WriteLine("Il y a  " + args.Length + " arguments");
            for (int i = 0; i < args.Length; i++) {
                Console.Out.WriteLine("arguments[" + i + "]=" + args[i]);
            }
        }
    }
}

Aby przekazać argumenty do wykonywanego kodu, należy postępować w następujący sposób:

  • w [1]: kliknij prawym przyciskiem myszy na projekt / Properties
  • w [2]: zakładka [Debug]
  • w [3]: wprowadź argumenty

Wyniki wykonania są następujące:

1
2
3
4
5
Il y a  4 arguments
arguments[0]=a0
arguments[1]=a1
arguments[2]=a2
arguments[3]=a3

Należy zauważyć, że sygnatura

public static void Main()

jest prawidłowa, jeśli funkcja Main nie oczekuje żadnych parametrów.

3.8. Wyliczenia

Wymiennik to typ danych, którego domena wartości stanowi zbiór stałych liczb całkowitych. Rozważmy program, który ma zarządzać wpisami dotyczącymi egzaminu. Byłoby ich pięć: Passable, AssezBien, Bien, TrèsBien, Excellent.

Można by zatem zdefiniować wyliczenie dla tych pięciu stałych:


        enum Mentions { Passable, AssezBien, Bien, TrèsBien, Excellent };

Wewnętrznie te pięć stałych jest kodowanych za pomocą kolejnych liczb całkowitych, zaczynających się od 0 dla pierwszej stałej, 1 dla następnej itd. Zmienną można zadeklarować tak, aby przyjmowała wartości z tej wyliczalnej:


            // zmienna pobierająca wartości z wyliczenia „Oznaczenia”
Mentions maMention = Mentions.Passable;

Można porównać zmienną z różnymi możliwymi wartościami wyliczenia:


            if (maMention == Mentions.Passable) {
                Console.WriteLine("Peut mieux faire");
}

Można uzyskać wszystkie wartości wyliczenia:


            // lista oznaczeń w postaci ciągów znaków
            foreach (Mentions m in Enum.GetValues(maMention.GetType())) {
                Console.WriteLine(m);
}

Podobnie jak typ prosty int jest równoważny strukturze System.Int32, tak typ prosty enum jest równoważny strukturze System.Enum. Struktura ta posiada metodę statyczną GetValues, która pozwala uzyskać wszystkie wartości typu wyliczeniowego przekazanego jako parametr. Parametr ten musi być obiektem typu Type, który jest klasą zawierającą informacje o typie danych. Typ zmiennej v uzyskuje się za pomocą funkcji v.GetType(). Typ typu T uzyskuje się za pomocą funkcji typeof(T). Zatem w tym przypadku funkcja maMention.GetType() zwraca obiekt Type z wyliczenia Mentions, a funkcja Enum.GetValues(maMention.GetType()) zwraca listę wartości wyliczenia Mentions.

Jeśli teraz wpiszemy


            //lista oznaczeń w postaci liczb całkowitych
            foreach (int m in Enum.GetValues(typeof(Mentions))) {
                Console.WriteLine(m);
}

Wiersz 2: zmienna pętli jest typu całkowitego. Otrzymujemy wówczas listę wartości wyliczenia w postaci liczb całkowitych. Obiekt typu System.Type odpowiadający typowi danych Mentions uzyskuje się poprzez typeof(Mentions). Można by było napisać, tak jak poprzednio, maMention.GetType().

Poniższy program ilustruje to, co właśnie zostało opisane:


using System;

namespace Chap1 {
    class P11 {
        enum Mentions { Passable, AssezBien, Bien, TrèsBien, Excellent };
        static void Main(string[] args) {
            // zmienna, której wartości pochodzą z wyliczenia „Mentions”
            Mentions maMention = Mentions.Passable;
            // wyświetlanie wartości zmiennej
            Console.WriteLine("mention=" + maMention);
            // test z wartością z wyliczenia
            if (maMention == Mentions.Passable) {
                Console.WriteLine("Peut mieux faire");
            }
            // lista wpisów w postaci ciągów znaków
            foreach (Mentions m in Enum.GetValues(maMention.GetType())) {
                Console.WriteLine(m);
            }
            //lista pozycji w postaci liczb całkowitych
            foreach (int m in Enum.GetValues(typeof(Mentions))) {
                Console.WriteLine(m);
            }
        }
    }
}

Wyniki wykonania są następujące:

mention=Passable
Peut mieux faire
Passable
AssezBien
Bien
TrèsBien
Excellent
0
1
2
3
4

3.9. Przekazywanie parametrów do funkcji

W tym miejscu skupimy się na sposobie przekazywania parametrów do funkcji. Rozważmy następującą funkcję statyczną:


        private static void ChangeInt(int a) {
            a = 30;
            Console.WriteLine("Paramètre formel a=" + a);
}

W definicji funkcji, w wierszu 1, a nazywany jest parametrem formalnym. Znajduje się tam wyłącznie na potrzeby definicji funkcji changeInt. Równie dobrze mógłby nosić nazwę b. Rozważmy teraz wykorzystanie tej funkcji:


        public static void Main() {
            int age = 20;
            ChangeInt(age);
            Console.WriteLine("Paramètre effectif age=" + age);
}

W instrukcji w wierszu 3, ChangeInt(age), age jest parametrem rzeczywistym, który przekaże swoją wartość do parametru formalnego a. Interesuje nas sposób, w jaki parametr formalny pobiera wartość z parametru rzeczywistego.

3.9.1. Przekazywanie przez wartość

Poniższy przykład pokazuje, że parametry funkcji są domyślnie przekazywane przez wartość, co oznacza, że wartość parametru rzeczywistego jest kopiowana do odpowiadającego mu parametru formalnego. Mamy do czynienia z dwoma odrębnymi jednostkami. Jeśli funkcja modyfikuje parametr formalny, parametr rzeczywisty pozostaje niezmieniony.


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            int age = 20;
            ChangeInt(age);
            Console.WriteLine("Paramètre effectif age=" + age);
        }
        private static void ChangeInt(int a) {
            a = 30;
            Console.WriteLine("Paramètre formel a=" + a);
        }
    }
}

Otrzymane wyniki są następujące:

Paramètre formel a=30
Paramètre effectif age=20

Wartość 20 parametru rzeczywistego age została skopiowana do parametru formalnego a (wiersz 10). Ten ostatni został następnie zmodyfikowany (wiersz 11). Parametr rzeczywisty pozostał niezmieniony. Ten sposób przekazywania nadaje się do parametrów wejściowych funkcji.

3.9.2. Przekazywanie przez odwołanie

W przypadku przekazywania przez odwołanie parametr rzeczywisty i parametr formalny stanowią jedną i tę samą jednostkę. Jeśli funkcja modyfikuje parametr formalny, modyfikowany jest również parametr rzeczywisty. W języku C# oba parametry muszą być poprzedzone słowem kluczowym ref:

Oto przykład:


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // przykład 2
            int age2 = 20;
            ChangeInt2(ref age2);
            Console.WriteLine("Paramètre effectif age2=" + age2);
        }
        private static void ChangeInt2(ref int a2) {
            a2 = 30;
            Console.WriteLine("Paramètre formel a2=" + a2);
        }
    }
}

oraz wyniki wykonania:

Paramètre formel a2=30
Paramètre effectif age2=30

Parametr rzeczywisty odzwierciedla zmianę parametru formalnego. Ten sposób przekazywania nadaje się do parametrów wyjściowych funkcji.

3.9.3. Przekazywanie przez odwołanie za pomocą słowa kluczowego „out”

Rozważmy poprzedni przykład, w którym zmienna age2 nie zostałaby zainicjowana przed wywołaniem funkcji changeInt:


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // przykład 2
            int age2;
            ChangeInt2(ref age2);
            Console.WriteLine("Paramètre effectif age2=" + age2);
        }
        private static void ChangeInt2(ref int a2) {
            a2 = 30;
            Console.WriteLine("Paramètre formel a2=" + a2);
        }
    }
}

Podczas kompilacji tego programu pojawia się błąd:

    Use of unassigned local variable 'age2'

Problem ten można obejść, przypisując wartość początkową do zmiennej age2. Można również zastąpić słowo kluczowe ref słowem kluczowym out. W ten sposób określa się, że parametr jest wyłącznie parametrem wyjściowym i w związku z tym nie wymaga wartości początkowej:


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // przykład 3
            int age3;
            ChangeInt3(out age3);
            Console.WriteLine("Paramètre effectif age3=" + age3);
        }
        private static void ChangeInt3(out int a3) {
            a3 = 30;
            Console.WriteLine("Paramètre formel a3=" + a3);
        }
    }
}

Wyniki wykonania są następujące:

Paramètre formel a3=30
Paramètre effectif age3=30