Skip to content

Zadania (21-30)

Operator is sprawdza, czy obiekt jest danego typu (lub typu pochodnego), i zwraca wartość logiczną true/false. Od C# 7 łączy się go z dopasowaniem wzorca, dzięki czemu w jednym kroku można sprawdzić typ i przypisać obiekt do nowej zmiennej.

Operator as próbuje rzutować obiekt na wskazany typ referencyjny lub typ dopuszczający null. Jeśli rzutowanie się nie powiedzie, zwraca null zamiast rzucać wyjątek (w przeciwieństwie do zwykłego rzutowania w nawiasach, które zgłasza InvalidCastException). Dlatego as jest bezpieczniejszy, gdy nie mamy pewności co do typu.

object o = "tekst";
// is sprawdza typ i zwraca bool; tu od razu z przypisaniem do zmiennej s
if (o is string s)
{
Console.WriteLine(s.Length);
}
// as próbuje rzutować; przy niepowodzeniu zwraca null (bez wyjątku)
string t = o as string;
if (t != null)
{
Console.WriteLine(t);
}

Warto zestawić trzy sposoby pracy z typami. Zwykłe rzutowanie (Typ)o zakłada, że znamy typ, a przy pomyłce rzuca wyjątek. as jest bezpieczne, ale działa tylko dla typów referencyjnych i nullowalnych (nie zadziała np. dla zwykłego int). is najpierw sprawdza, więc unika wyjątku.

object o = "tekst";
// 1) zwykłe rzutowanie - szybkie, ale przy złym typie InvalidCastException
string a = (string)o;
// 2) as - przy złym typie null zamiast wyjątku (tylko typy referencyjne/nullable)
string b = o as string;
// as z typem wartościowym wymaga wersji nullowalnej (int?), inaczej błąd kompilacji
object obj = 5;
int? n = obj as int?;

Operator is rozbudowano o wiele wzorców (pattern matching), które są częstym tematem na egzaminie:

object o = 42;
// sprawdzenie typu z przypisaniem (type pattern)
if (o is int i)
Console.WriteLine(i * 2);
// sprawdzenie nulla: is null / is not null (czytelniejsze niż == null)
if (o is not null)
Console.WriteLine("coś jest");
// wzorce relacyjne i logiczne (C# 9): porównania i and/or
int age = 25;
bool isAdult = age is >= 18 and < 65;

is zwraca tylko bool i (opcjonalnie) wiąże zmienną, więc nadaje się do warunków. as zwraca obiekt albo null, więc nadaje się, gdy chcemy dalej użyć rzutowanej wartości. Połączenie is z wzorcem często zastępuje parę as + sprawdzenie nulla.

Skrót do zapamiętania: is sprawdza typ i zwraca bool (z możliwym dopasowaniem wzorca). as próbuje rzutować i przy niepowodzeniu zwraca null zamiast wyjątku.

  • is: test typu -> bool; od C# 7 z pattern matching (o is string s).
  • as: bezpieczne rzutowanie typów referencyjnych/nullable; niepowodzenie -> null.
  • Zwykłe rzutowanie (Typ)o przy błędzie rzuca InvalidCastException.
  • as nie działa na zwykłych typach wartościowych (trzeba int?, double? itd.).
  • as używamy, gdy nie mamy pewności co do typu i chcemy dalej użyć wartości.
  • Wzorce z is: type pattern, is null/is not null, wzorce relacyjne/logiczne (and, or, not).
  • is + wzorzec często zastępuje as + sprawdzenie nulla.

Konstrukcja match (wzorce aktywne) i funkcje rekurencyjne w F#

Section titled “Konstrukcja match (wzorce aktywne) i funkcje rekurencyjne w F#”

Konstrukcja match … with w F# to rozbudowane dopasowanie wzorców, działające jak wielokrotny switch, ale potrafiące dodatkowo rozbijać strukturę danych i wiązać jej części ze zmiennymi. Każdy przypadek to wzorzec poprzedzony kreską pionową, a _ oznacza dowolną wartość.

Wzorce aktywne (active patterns) pozwalają definiować własne kategorie dopasowania w nawiasach z kreskami pionowymi, np. podział liczb na parzyste i nieparzyste. Funkcje rekurencyjne deklaruje się słowem let rec i często łączy z match, gdzie jeden przypadek jest warunkiem zakończenia (bazowym), a drugi zawiera wywołanie rekurencyjne.

// funkcja rekurencyjna - oznaczona slowem rec
let rec factorial n =
// dopasowanie wzorca do wartosci n
match n with
// przypadek bazowy konczacy rekurencje
| 0 -> 1
// przypadek ogolny z wywolaniem rekurencyjnym
| _ -> n * factorial (n - 1)
// aktywny wzorzec dzielacy liczby na Even i Odd
let (|Even|Odd|) n =
if n % 2 = 0 then Even else Odd
// uzycie aktywnego wzorca w match
let describe n =
match n with
| Even -> "parzysta"
| Odd -> "nieparzysta"

Skrót do zapamiętania: match…with to dopasowanie wzorców (switch z rozbijaniem struktury). Wzorce aktywne (|A|B|) to własne kategorie dopasowania. Funkcje rekurencyjne to let rec, zwykle z przypadkiem bazowym w match.

  • match…with: dopasowanie wzorca, | na przypadek, _ = dowolny.
  • Potrafi rozbijać strukturę i wiązać części ze zmiennymi.
  • Wzorce aktywne: własne wzorce definiowane jako (|A|B|).
  • Rekurencja: let rec, przypadek bazowy + wywołanie rekurencyjne.

Program w F# do obchodzenia drzewa binarnego (post-order oraz dwa pozostałe porządki)

Section titled “Program w F# do obchodzenia drzewa binarnego (post-order oraz dwa pozostałe porządki)”

Drzewo binarne reprezentujemy jako typ unii (discriminated union), czyli typ, który ma kilka możliwych wariantów. Tutaj węzeł drzewa to albo Leaf (liść pusty, koniec gałęzi), albo Node przechowujący wartość oraz dwa poddrzewa (lewe i prawe), które też są drzewami. Taka definicja jest rekurencyjna: drzewo składa się z mniejszych drzew, aż dojdziemy do Leaf.

Obchodzenie (traversal) to odwiedzanie wszystkich węzłów w ustalonej kolejności i zbieranie ich wartości do listy. Realizujemy je funkcjami rekurencyjnymi z dopasowaniem match: jeden przypadek to warunek bazowy (Leaf zwraca pustą listę i kończy rekurencję), drugi rozbija Node na wartość v oraz poddrzewa l i r, dla których funkcja woła samą siebie. Trzy porządki różnią się wyłącznie kolejnością sklejania trzech kawałków: lewego poddrzewa, prawego poddrzewa i wartości korzenia. Operator @ łączy listy.

  • pre-order (korzeń, lewe, prawe) - korzeń przed poddrzewami.
  • in-order (lewe, korzeń, prawe) - korzeń między poddrzewami; dla drzewa BST daje wartości po kolei (rosnąco).
  • post-order (lewe, prawe, korzeń) - korzeń po poddrzewach.
// definicja drzewa binarnego: lisc (pusty) lub wezel z wartoscia i dwoma poddrzewami
type Tree =
| Leaf
| Node of int * Tree * Tree
// post-order: lewe, prawe, korzen
let rec postOrder tree =
match tree with
// pusty lisc daje pusta liste
| Leaf -> []
// najpierw lewe poddrzewo, potem prawe, na koncu wartosc wezla
| Node(v, l, r) -> postOrder l @ postOrder r @ [v]
// in-order: lewe, korzen, prawe
let rec inOrder tree =
match tree with
| Leaf -> []
// lewe poddrzewo, wartosc, prawe poddrzewo
| Node(v, l, r) -> inOrder l @ [v] @ inOrder r
// pre-order: korzen, lewe, prawe
let rec preOrder tree =
match tree with
| Leaf -> []
// najpierw wartosc, potem lewe i prawe poddrzewo
| Node(v, l, r) -> [v] @ preOrder l @ preOrder r
// przykladowe drzewo
let tree = Node(1, Node(2, Leaf, Leaf), Node(3, Leaf, Leaf))
// wypisanie wynikow obchodzenia
printfn "post-order: %A" (postOrder tree)
printfn "in-order: %A" (inOrder tree)
printfn "pre-order: %A" (preOrder tree)

Skrót do zapamiętania: Drzewo to typ unii (Leaf | Node of wartosc, lewe, prawe). Trzy rekurencyjne funkcje różnią się tylko kolejnością: post = L,P,korzeń; in = L,korzeń,P; pre = korzeń,L,P. Listy łączy @.

  • Typ: Leaf | Node of int * Tree * Tree.
  • post-order: postOrder l @ postOrder r @ [v].
  • in-order: inOrder l @ [v] @ inOrder r.
  • pre-order: [v] @ preOrder l @ preOrder r.
  • Wszystkie rekurencyjne (let rec) z dopasowaniem match.

W F# operatory można przeciążać, definiując je jako statyczne składowe typu (klasy lub rekordu) o nazwie operatora w nawiasach, np. (+). Działa to analogicznie do C#: gdy użyjemy operatora na obiektach danego typu, wywoła się nasza definicja. Można też definiować własne operatory infiksowe na poziomie modułu.

// typ Vector z przecionym operatorem +
type Vector(x: int, y: int) =
// wlasnosci tylko do odczytu
member this.X = x
member this.Y = y
// przeciazenie operatora + jako statyczna skladowa typu
static member (+) (a: Vector, b: Vector) =
// tworzymy nowy wektor jako sume skladowych
Vector(a.X + b.X, a.Y + b.Y)
// uzycie operatora na obiektach Vector
let v = Vector(1, 2) + Vector(3, 4)

Skrót do zapamiętania: W F# operator przeciąża się jako statyczną składową typu o nazwie operatora w nawiasach, np. static member (+) (a, b) = .... Można też tworzyć własne operatory infiksowe.

  • Składnia: static member (+) (a, b) = ... wewnątrz typu.
  • Działa po użyciu operatora na obiektach tego typu.
  • Można definiować własne operatory infiksowe (na poziomie modułu).
  • Idea jak w C#, inna składnia.

Modele programowania ADO.NET - dwa modele dostępu do baz danych

Section titled “Modele programowania ADO.NET - dwa modele dostępu do baz danych”

ADO.NET udostępnia dwa podstawowe modele dostępu do danych: połączeniowy i bezpołączeniowy. Różnią się one tym, czy połączenie z bazą jest utrzymywane przez cały czas pracy na danych.

Model połączeniowy opiera się na obiekcie DataReader. Połączenie z bazą jest otwarte przez cały czas odczytu, a dane czyta się strumieniowo, tylko do przodu i tylko do odczytu. Jest szybki i oszczędny pamięciowo, ale blokuje połączenie i nie pozwala swobodnie nawigować po danych.

Model bezpołączeniowy opiera się na obiektach DataSet i DataAdapter. Dane pobiera się do pamięci (DataSet to lokalna kopia danych), po czym połączenie jest zamykane. Można na nich pracować w trybie offline, a zmiany zsynchronizować z bazą później przez DataAdapter. Zużywa więcej pamięci, ale jest elastyczny i nie trzyma otwartego połączenia.

Model połączeniowy z użyciem DataReader (połączenie otwarte przez cały odczyt):

// using gwarantuje zamknięcie połączenia po zakończeniu
using (SqlConnection conn = new SqlConnection(connectionString))
{
SqlCommand cmd = new SqlCommand("SELECT Name FROM Users", conn);
conn.Open(); // połączenie otwarte
// DataReader czyta strumieniowo, tylko do przodu i do odczytu
using (SqlDataReader reader = cmd.ExecuteReader())
{
// Read() przesuwa się do kolejnego wiersza, false gdy koniec
while (reader.Read())
{
Console.WriteLine(reader["Name"]);
}
}
} // tutaj połączenie jest zamykane

Model bezpołączeniowy z użyciem DataSet i DataAdapter (połączenie tylko na czas pobrania danych):

DataSet ds = new DataSet();
using (SqlConnection conn = new SqlConnection(connectionString))
{
// adapter pośredniczy między bazą a DataSet
SqlDataAdapter adapter = new SqlDataAdapter("SELECT Name FROM Users", conn);
// Fill sam otwiera i zamyka połączenie, kopiując dane do pamięci
adapter.Fill(ds, "Users");
}
// dalej pracujemy offline, bez otwartego połączenia
foreach (DataRow row in ds.Tables["Users"].Rows)
{
Console.WriteLine(row["Name"]);
}

Skrót do zapamiętania: Połączeniowy (DataReader): połączenie cały czas otwarte, odczyt do przodu, szybki, mało pamięci. Bezpołączeniowy (DataSet/DataAdapter): dane w pamięci, połączenie zamykane, praca offline, więcej pamięci.

  • Połączeniowy: DataReader, otwarte połączenie, odczyt strumieniowy do przodu, szybki.
  • Bezpołączeniowy: DataSet + DataAdapter, dane w pamięci, połączenie zamykane.
  • Połączeniowy oszczędza pamięć, bezpołączeniowy daje elastyczność i pracę offline.
  • Łącznikiem z bazą jest Connection; polecenia wykonuje Command.

Opisać klasę Page stosowaną na poziomie aplikacji ASP.NET

Section titled “Opisać klasę Page stosowaną na poziomie aplikacji ASP.NET”

Klasa Page (System.Web.UI.Page) w ASP.NET Web Forms reprezentuje pojedynczą stronę .aspx. Każda strona aplikacji jest w istocie klasą dziedziczącą z Page, generowaną na podstawie pliku .aspx i powiązanego kodu (code-behind). Obiekt tej klasy jest tworzony przy każdym żądaniu strony.

Klasa Page zarządza cyklem życia strony i udostępnia kluczowe właściwości oraz zdarzenia. Najważniejsze właściwości to IsPostBack (czy strona jest wynikiem ponownego wysłania formularza), Request, Response, Session, ViewState i kontrolki strony. Najważniejsze zdarzenia cyklu życia to Init, Load, zdarzenia kontrolek oraz PreRender i Unload.

Typowy kod code-behind: klasa strony dziedziczy z Page, a w metodach obsługi zdarzeń korzysta z jej właściwości:

// klasa strony powstaje z pliku Default.aspx i dziedziczy z Page
public partial class Default : System.Web.UI.Page
{
// metoda obsługi zdarzenia Load (cykl życia strony)
protected void Page_Load(object sender, EventArgs e)
{
// IsPostBack: false przy pierwszym wejściu, true po wysłaniu formularza
if (!IsPostBack)
{
// wykonuje się tylko raz, przy pierwszym załadowaniu strony
labelInfo.Text = "Witaj na stronie";
}
}
// metoda obsługi kliknięcia przycisku (zdarzenie kontrolki)
protected void Button_Click(object sender, EventArgs e)
{
// dostęp do danych żądania, sesji i odpowiedzi przez właściwości Page
string user = Request.Form["userName"];
Session["lastUser"] = user;
Response.Write("Zapisano: " + user);
}
}

Skrót do zapamiętania: Page (System.Web.UI.Page) reprezentuje stronę .aspx; każda strona to klasa dziedzicząca z Page. Zarządza cyklem życia strony i udostępnia IsPostBack, Request, Response, Session oraz zdarzenia Init/Load/PreRender/Unload.

  • Reprezentuje stronę .aspx; każda strona dziedziczy z System.Web.UI.Page.
  • Tworzona przy każdym żądaniu, zarządza cyklem życia strony.
  • Właściwości: IsPostBack, Request, Response, Session, ViewState, kontrolki.
  • Zdarzenia: Init, Load, PreRender, Unload.

Transakcja to zbiór operacji wykonywanych jako jedna, niepodzielna całość, spełniający zasady ACID (atomowość, spójność, izolacja, trwałość). Albo wszystkie operacje się powiodą i zmiany zostaną zatwierdzone (commit), albo żadna i nastąpi wycofanie (rollback).

Pod względem zasięgu wyróżnia się transakcje lokalne i rozproszone. Transakcja lokalna obejmuje jeden zasób (jedną bazę danych) i jest zarządzana przez jeden menedżer transakcji. Transakcja rozproszona obejmuje wiele zasobów lub baz, koordynowanych przez wspólny menedżer (np. MSDTC), zwykle z protokołem zatwierdzania dwufazowego (two-phase commit). W .NET transakcje wygodnie obsługuje TransactionScope, który sam decyduje, czy potrzebna jest transakcja lokalna czy rozproszona.

Transakcja lokalna sterowana ręcznie przez ADO.NET (commit przy powodzeniu, rollback przy błędzie):

using (SqlConnection conn = new SqlConnection(connectionString))
{
conn.Open();
// rozpoczęcie transakcji
SqlTransaction tx = conn.BeginTransaction();
try
{
// polecenia muszą wiedzieć, do której transakcji należą
SqlCommand cmd = new SqlCommand("UPDATE Accounts SET Balance = Balance - 100 WHERE Id = 1", conn, tx);
cmd.ExecuteNonQuery();
cmd.CommandText = "UPDATE Accounts SET Balance = Balance + 100 WHERE Id = 2";
cmd.ExecuteNonQuery();
// wszystko się udało: zatwierdzenie zmian
tx.Commit();
}
catch
{
// błąd w którejkolwiek operacji: wycofanie całości
tx.Rollback();
}
}

Transakcja przez TransactionScope (commit przez Complete; brak wywołania = rollback, sam dobiera lokalną lub rozproszoną):

using (TransactionScope scope = new TransactionScope())
{
// operacje na jednym lub wielu zasobach...
// (TransactionScope w razie potrzeby awansuje do transakcji rozproszonej)
// Complete oznacza zgodę na commit; bez niego nastąpi rollback
scope.Complete();
} // tutaj transakcja jest zatwierdzana lub wycofywana

Skrót do zapamiętania: Transakcja to niepodzielny zbiór operacji (ACID): commit albo rollback. Rodzaje wg zasięgu: lokalne (jeden zasób/baza, jeden menedżer) i rozproszone (wiele zasobów, koordynacja MSDTC, two-phase commit).

  • Transakcja = niepodzielny zbiór operacji, zasady ACID.
  • Commit (zatwierdzenie) albo rollback (wycofanie).
  • Lokalna: jeden zasób/baza, jeden menedżer transakcji.
  • Rozproszona: wiele zasobów, koordynator (MSDTC), zatwierdzanie dwufazowe.
  • W .NET: TransactionScope.

Wyjaśnić pojęcie plików XML i podać przykład takiego pliku

Section titled “Wyjaśnić pojęcie plików XML i podać przykład takiego pliku”

XML (Extensible Markup Language) to tekstowy, rozszerzalny format opisu danych w strukturze hierarchicznej (drzewiastej). Dane przechowuje się w znacznikach, które programista definiuje samodzielnie, dzięki czemu format jest samoopisujący i czytelny zarówno dla człowieka, jak i dla maszyny. XML służy głównie do przechowywania oraz wymiany danych między różnymi systemami niezależnie od platformy.

Plik XML ma jeden element nadrzędny (korzeń), wewnątrz którego zagnieżdżone są kolejne elementy. Elementy mogą mieć atrybuty i wartości tekstowe.

Budowa pliku. Na początku może stać deklaracja (prolog) określająca wersję i kodowanie znaków. Każdy element ma znacznik otwierający i zamykający (<title>...</title>); element bez treści można skrócić do postaci samozamykającej (<br/>). Element może zawierać tekst, inne elementy oraz atrybuty (pary nazwa=“wartość” w znaczniku otwierającym). Dozwolone są też komentarze <!-- ... --> oraz sekcje CDATA na tekst, którego nie chcemy interpretować.

<?xml version="1.0" encoding="UTF-8"?>
<!-- element korzenia -->
<library>
<!-- atrybut id w znaczniku otwierającym, elementy zagnieżdżone z wartościami -->
<book id="1" lang="pl">
<title>Pan Tadeusz</title>
<author>Adam Mickiewicz</author>
<year>1834</year>
</book>
<book id="2" lang="en">
<title>Hamlet</title>
<author>William Shakespeare</author>
<!-- element samozamykający (bez treści) -->
<year value="1603"/>
</book>
</library>

Poprawność. Rozróżnia się dwa poziomy. Dokument poprawny składniowo (well-formed) przestrzega reguł języka: dokładnie jeden korzeń, każdy znacznik domknięty, prawidłowe zagnieżdżanie (bez krzyżowania), atrybuty w cudzysłowach, rozróżnianie wielkości liter (<Book> to nie to samo co <book>). Dokument poprawny strukturalnie (valid) dodatkowo zgadza się ze schematem opisującym dozwoloną strukturę: DTD lub nowocześniejszy XSD (XML Schema), który określa, jakie elementy, atrybuty i typy są dopuszczalne.

Element a atrybut. Element to nazwany węzeł, który może zawierać inne elementy i tekst; atrybut to dodatkowa informacja o elemencie (zawsze tekst, bez zagnieżdżenia). Z reguły dane właściwe trzyma się w elementach, a metadane (np. id, typ, język) w atrybutach.

XML a JSON/HTML. XML i HTML korzystają ze znaczników, ale HTML ma z góry ustalony, zamknięty zestaw znaczników do prezentacji stron, a w XML znaczniki definiuje użytkownik i służą one do opisu danych. JSON to lżejszy, mniej rozwlekły format do wymiany danych, dziś częstszy w aplikacjach webowych; XML bywa za to bogatszy (atrybuty, komentarze, schematy, przestrzenie nazw). W ekosystemie .NET XML jest powszechny w plikach konfiguracyjnych (np. .config, .csproj).

Obsługa XML w C#. Do podstaw wystarczą klasy XDocument i XElement (przestrzeń System.Xml.Linq). Plik wczytuje XDocument.Load, a po elementach nawiguje się metodami Element/Elements i odczytuje atrybuty przez Attribute.

using System;
using System.Xml.Linq;
// wczytanie pliku XML do pamięci
XDocument doc = XDocument.Load("library.xml");
// przejście po wszystkich elementach <book> wewnątrz korzenia <library>
foreach (XElement book in doc.Root.Elements("book"))
{
// odczyt atrybutu id ze znacznika otwierającego
string id = book.Attribute("id").Value;
// odczyt wartości zagnieżdżonych elementów
string title = book.Element("title").Value;
string author = book.Element("author").Value;
Console.WriteLine($"[{id}] {title} - {author}");
}
// utworzenie nowego dokumentu XML w kodzie i zapis do pliku
XDocument newDoc = new XDocument(
new XElement("library",
new XElement("book",
new XAttribute("id", "3"),
new XElement("title", "Lalka"),
new XElement("author", "Bolesław Prus"))));
newDoc.Save("new.xml");

Skrót do zapamiętania: XML to tekstowy, hierarchiczny, samoopisujący format danych z definiowanymi przez użytkownika znacznikami; służy do przechowywania i wymiany danych między systemami. Ma jeden korzeń i zagnieżdżone elementy. Bywa well-formed (poprawny składniowo) i valid (zgodny ze schematem DTD/XSD).

  • XML = tekstowy, rozszerzalny, hierarchiczny format danych.
  • Znaczniki definiuje użytkownik -> samoopisujący; rozróżnia wielkość liter.
  • Jeden element korzenia, w nim zagnieżdżone elementy (z atrybutami i wartościami).
  • Budowa: prolog (wersja, kodowanie), elementy, atrybuty, komentarze, CDATA, elementy samozamykające.
  • Well-formed = poprawny składniowo; valid = zgodny ze schematem (DTD/XSD).
  • Element trzyma dane, atrybut opisuje element (metadane).
  • Zastosowanie: przechowywanie i wymiana danych niezależnie od platformy; w .NET pliki konfiguracyjne (.config, .csproj).
  • Porównanie: HTML ma stałe znaczniki (prezentacja), XML własne (dane); JSON lżejszy i dziś częstszy w wymianie danych.
  • W C#: podstawowa obsługa przez XDocument i XElement - Load do wczytania, Element/Elements i Attribute do odczytu, Save do zapisu.

Czym jest serializacja obiektów w języku C#?

Section titled “Czym jest serializacja obiektów w języku C#?”

Serializacja to proces zamiany obiektu (jego stanu, czyli wartości pól) na postać nadającą się do zapisu lub przesłania, np. strumień bajtów, tekst XML albo JSON. Deserializacja to proces odwrotny: odtworzenie obiektu z takiej zapisanej postaci. Dzięki temu obiekty można zapisać do pliku, przesłać siecią lub przechować w pamięci podręcznej, a później je odtworzyć. Typowe zastosowania to zapis stanu do pliku, wymiana danych przez API i sieć oraz pamięć podręczna.

Rodzaje serializacji. Wyróżnia się trzy główne formaty:

  • JSON - lekki, tekstowy, dziś najczęstszy (zwłaszcza w web i API). W .NET obsługuje go System.Text.Json (klasa JsonSerializer).
  • XML - tekstowy, bardziej rozwlekły, ale czytelny i ze schematami. Obsługuje go XmlSerializer; serializuje publiczne właściwości i wymaga bezparametrowego konstruktora.
  • Binarny - kompaktowy strumień bajtów. Dawniej BinaryFormatter, dziś odradzany i wycofywany ze względów bezpieczeństwa (deserializacja niezaufanych danych pozwalała na ataki).

Poniżej wspólna klasa używana w przykładach:

class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Person p = new Person { Name = "Ala", Age = 20 };

Serializacja JSON. Klasa JsonSerializer zamienia obiekt na tekst i z powrotem, a opcjonalny obiekt JsonSerializerOptions steruje wyglądem wyniku:

using System.IO;
using System.Text.Json;
// podstawowa serializacja do tekstu JSON: {"Name":"Ala","Age":20}
string json = JsonSerializer.Serialize(p);
// deserializacja - odtworzenie obiektu z tekstu JSON
Person restored = JsonSerializer.Deserialize<Person>(json);
// opcje sterujące tym, JAK wygląda wynik
JsonSerializerOptions options = new JsonSerializerOptions
{
// wcięcia zamiast jednej linii
WriteIndented = true,
// name zamiast Name
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
// ignoruj wielkość liter przy odczycie
PropertyNameCaseInsensitive = true
};
// zapis do pliku: serializacja z opcjami i zapis tekstu
File.WriteAllText("person.json", JsonSerializer.Serialize(p, options));
// odczyt z pliku: wczytanie tekstu i deserializacja
Person fromFile = JsonSerializer.Deserialize<Person>(File.ReadAllText("person.json"));

Serializacja XML. XmlSerializer pracuje na strumieniach, więc zapis i odczyt z pliku robi się przez StreamWriter/StreamReader:

using System.IO;
using System.Xml.Serialization;
XmlSerializer serializer = new XmlSerializer(typeof(Person));
// zapis obiektu do pliku XML
using (StreamWriter writer = new StreamWriter("person.xml"))
{
serializer.Serialize(writer, p);
}
// odczyt obiektu z pliku XML (rzutowanie, bo Deserialize zwraca object)
using (StreamReader reader = new StreamReader("person.xml"))
{
Person fromXml = (Person)serializer.Deserialize(reader);
}

Sterowanie atrybutami. Atrybuty nakładane na klasę i właściwości pozwalają pominąć pole, zmienić nazwę albo wybrać formę zapisu. Zestaw zależy od serializera:

// atrybuty dla JSON (System.Text.Json)
class Person
{
// inna nazwa właściwości w JSON
[JsonPropertyName("full_name")]
public string Name { get; set; }
// kolejność w wyniku
[JsonPropertyOrder(1)]
public int Age { get; set; }
// pominięcie (np. dane wrażliwe)
[JsonIgnore]
public string Password { get; set; }
}
// atrybuty dla XML (System.Xml.Serialization)
// nazwa elementu korzenia
[XmlRoot("person")]
class PersonXml
{
// zapis jako element o podanej nazwie
[XmlElement("name")]
public string Name { get; set; }
// zapis jako atrybut zamiast elementu
[XmlAttribute]
public int Age { get; set; }
// pominięcie pola
[XmlIgnore]
public string Password { get; set; }
// kolekcja: nazwa opakowania i nazwa pojedynczego elementu
[XmlArray("tags")]
[XmlArrayItem("tag")]
public string[] Tags { get; set; }
}
// atrybuty dla serializacji binarnej (dawniej)
// klasa serializowalna
[Serializable]
class PersonBin
{
public string Name;
// pole nie zostanie zapisane
[NonSerialized]
public string Password;
}

Skrót do zapamiętania: Serializacja to zamiana obiektu na zapisywalną/przesyłalną postać (bajty, XML, JSON), deserializacja to proces odwrotny. W C#: JsonSerializer, XmlSerializer, atrybut [Serializable].

  • Serializacja: obiekt -> bajty/XML/JSON (do zapisu lub przesłania).
  • Deserializacja: postać zapisana -> obiekt.
  • Rodzaje: JSON (dziś najczęstszy), XML, binarny (BinaryFormatter - odradzany, bo niebezpieczny).
  • Narzędzia: JsonSerializer (System.Text.Json), XmlSerializer (System.Xml.Serialization).
  • XmlSerializer: serializuje publiczne właściwości, wymaga bezparametrowego konstruktora.
  • Sterowanie atrybutami: [JsonIgnore]/[XmlIgnore] (pominięcie), [JsonPropertyName]/[XmlElement]/[XmlAttribute] (nazwa, forma).
  • Zastosowanie: zapis do pliku, przesył siecią, API, cache.

Przedstawić sposób obsługi błędów jakie mogą pojawić się w aplikacjach ASP.NET

Section titled “Przedstawić sposób obsługi błędów jakie mogą pojawić się w aplikacjach ASP.NET”

W aplikacjach ASP.NET błędy obsługuje się na kilku poziomach. Najniższy to lokalna obsługa w kodzie blokami try-catch-finally, gdy spodziewamy się konkretnego błędu i chcemy go obsłużyć od razu. Wyższy poziom to obsługa na poziomie strony przez zdarzenie Page_Error, a najwyższy to obsługa globalna na poziomie całej aplikacji przez zdarzenie Application_Error w pliku Global.asax.

Dodatkowo w pliku konfiguracyjnym web.config sekcja customErrors pozwala zdefiniować przyjazne strony błędów wyświetlane użytkownikowi zamiast technicznych komunikatów (np. osobna strona dla błędu 404). Metoda Server.GetLastError() pozwala pobrać ostatni wyjątek, a Server.ClearError() go wyczyścić. W ASP.NET Core obsługę błędów realizuje się przez middleware (np. UseExceptionHandler).

Obsługa lokalna w kodzie blokiem try-catch-finally:

try
{
// kod, który może zgłosić błąd
int x = int.Parse(Request.Form["age"]);
}
catch (FormatException ex)
{
// obsługa konkretnego błędu
labelInfo.Text = "Niepoprawny wiek: " + ex.Message;
}
finally
{
// wykonuje się zawsze (np. sprzątanie)
}

Obsługa globalna w pliku Global.asax przez zdarzenie Application_Error:

protected void Application_Error(object sender, EventArgs e)
{
// pobranie ostatniego wyjątku z aplikacji
Exception ex = Server.GetLastError();
// tu np. zapis błędu do logu
// wyczyszczenie błędu, by nie pokazać użytkownikowi szczegółów technicznych
Server.ClearError();
// przekierowanie na przyjazną stronę błędu
Response.Redirect("~/Error.aspx");
}

Przyjazne strony błędów w pliku web.config (sekcja customErrors):

<configuration>
<system.web>
<!-- On = pokazuj własne strony; defaultRedirect = strona ogólna -->
<customErrors mode="On" defaultRedirect="~/Error.aspx">
<!-- osobna strona dla konkretnego kodu błędu -->
<error statusCode="404" redirect="~/NotFound.aspx" />
</customErrors>
</system.web>
</configuration>

W ASP.NET Core odpowiednikiem obsługi globalnej jest middleware konfigurowane w potoku aplikacji:

// przekierowanie wyjątków na wskazaną ścieżkę obsługi błędów
app.UseExceptionHandler("/Error");

Skrót do zapamiętania: Trzy poziomy: lokalnie try-catch, na poziomie strony Page_Error, globalnie Application_Error (Global.asax). Plus przyjazne strony błędów w web.config (customErrors) i Server.GetLastError().

  • Lokalnie: try-catch-finally w kodzie.
  • Na poziomie strony: zdarzenie Page_Error.
  • Globalnie: Application_Error w Global.asax.
  • web.config -> customErrors: przyjazne strony błędów (np. 404).
  • Server.GetLastError() pobiera ostatni wyjątek; w ASP.NET Core - middleware.