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:
- liczby całkowite
- liczby rzeczywiste
- liczby dziesiętne
- znaki i ciągi znaków
- wartości logiczne
- obiekty
3.2.1. Wstępnie zdefiniowane typy danych
Typ C# | Typ .NET | Reprezentowana wartość | Sufiks wartości literałowych | Kodowanie | Zakres wartości |
Znak (znaki) | znak | 2 bajty | znak Unicode (UTF-16) | ||
Ciąg znaków (C) | ciąg znaków | odwołanie do sekwencji znaków Unicode | |||
Int32 (S) | liczba całkowita | 4 bajty | [-231, 231-1] [–2147483648, 2147483647] | ||
UInt32 (S) | .. | U | 4 bajty | [0, 232-1] [0, 4294967295] | |
Int64 (S) | .. | L | 8 bajtów | [-263, 263 -1] [–9223372036854775808, 9223372036854775807] | |
UInt64 (S) | .. | UL | 8 bajtów | [0, 264 -1] [0, 18446744073709551615] | |
.. | 1 bajt | [-27 , 27 -1] [-128,+127] | |||
Bajt(y) | .. | 1 bajt | [0 , 28 -1] [0,255] | ||
Int16 (S) | .. | 2 bajty | [-215, 215-1] [-32768, 32767] | ||
UInt16 (S) | .. | 2 bajty | [0, 216-1] [0,65535] | ||
Pojedynczy (S) | liczba rzeczywista | F | 4 bajty | [1.5 10-45, 3.4 10+38] jako wartość bezwzględna | |
Typ Double (S) | .. | D | 8 bajtów | [-1.7 10+308, 1.7 10+308] w wartościach bezwzględnych | |
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 | |
Boolean (S) | .. | 1 bajt | true, false | ||
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ć:
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:
Wyświetlane są typy .NET, a nie aliasy C#.
3.2.2. Notacja danych literałowych
145, -7, 0xFF (szesnastkowo) | |
100000L | |
134,789, -45E-18 (-45 × 10⁻¹⁸) | |
134.789F, -45E-18F (-45 10-18) | |
100000M | |
„A”, „b” | |
„dzisiaj” „c:\\rozdział1\\akapit3” @„c:\rozdział1\akapit3” | |
prawda, fałsz | |
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:
Na przykład:
Po co deklarować stałe?
- Program będzie łatwiejszy do odczytania, jeśli nadamy stałej znaczącą nazwę:
- 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ść:
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:
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:
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:
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.ToString() | |
int.Parse(ciąg) lub System.Int32.Parse | |
long.Parse(ciąg) lub System.Int64.Parse | |
double.Parse (łańcuch) lub System.Double.Parse (łańcuch) | |
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:
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ć
ale
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:
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:
lub, prościej:
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:
lub, prościej:
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:
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.
![]() |
-
odczyt informacji z klawiatury
-
przetwarzanie informacji
-
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:
Klasa Console udostępnia metodę ReadLine, domyślnie powiązaną ze strumieniem In. Można zatem napisać:
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:
- 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:
- standardowe urządzenie wejściowe – domyślnie oznacza klawiaturę i ma numer 0
- standardowe urządzenie wyjściowe – domyślnie oznacza ekran i ma numer 1
- 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ń:
Za argumentami argi programu pg można przekierować standardowe urządzenia wejścia/wyjścia do plików:
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. | |
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 | |
to samo, ale zapisane dane są dodawane do aktualnej zawartości pliku out.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 | |
to samo, ale zapisane dane są dodawane do aktualnej zawartości pliku 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:
- 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
jest poprawna. Ze względu na kolejność operacji najpierw zostanie obliczony operator = znajdujący się najbardziej po prawej stronie. Mamy zatem
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:
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:
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:
pierwiastek kwadratowy | |
cosinus | |
Sinus | |
Tangens | |
x do potęgi y (x>0) | |
Funkcja wykładnicza | |
Logarytm naturalny | |
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:
Pełna definicja klasy Math wygląda następująco:





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):
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.
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:
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ę
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:
3.3.5.5. Wyrażenia boolowskie
Dostępne operatory to AND (&&) OR (||) NOT (!). Wynikiem wyrażenia boolowskiego jest wartość logiczna.
Priorytety operatorów:
- !
- &&
- ||
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.
przesuwa i o n bitów w lewo. Bity wejściowe to zera. | |
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. | |
wykonuje operację logicznego OR bit po bicie na i i j. | |
wykonuje operację logicznego bit po bicie między i a j (OU). | |
uzupełnia i do 1 | |
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:
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
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
gd | |
dg | |
dg | |
gd | |
gd | |
gd | |
gd | |
gd | |
gd | |
gd | |
gd | |
gd | |
gd | |
dg | |
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:
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:
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ą $?.
przerwie wykonywanie programu, zwracając wartość stanu równą 0.
3.4.2. Prosta struktura wyboru
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ć:
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#
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:
co można przetłumaczyć na strukturę tantque:
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:
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”:
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”:
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:
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ą
wyjście z pętli for, while, do ... while. | |
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ć:
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:
- 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:
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
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) ? 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:
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:
Należy zauważyć, że sygnatura
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:
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:
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:
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:
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:





