środa, 30 czerwca 2010

Architektura trójwarstwowa w ASP.NET

Architektura trójwarstwowa (three-tier architecture) pozwala na rozdzielenie systemu informatycznego na składowe zwane warstwami (tiers). Co zyskujemy dzięki takiemu podziałowi? Przede wszystkim łatwość wymiany modułów, bez potrzeby aktualizowania pozostałych.

Aplikacja trójwarstwowa składa się z następujących warstw:
  •  warstwy prezentacji - jest to najwyższa z warstw w tym modelu i to z nią prowadzi interakcję użytkownik aplikacji. Warstwa ta posiada najczęściej walidację wprowadzanych przez użytkownika danych oraz referencję do warstwy biznesowej. Nie wie natomiast nic na temat zasad działania systemu, wykorzystywanej bazy danych itp.
  • warstwa logiki biznesowej - warstwa ta przetwarza zapytania użytkownika. Można sobie ją wyobrazić jako "mózg" całej aplikacji. Warstwa ta jest "pośrednikiem" między warstwą prezentacji a warstwą dostępu do danych. Warstwa ta nie wie w jaki sposób "wydobywać" informacje z bazy danych.
  • warstwa danych - jest to jedyna warstwa aplikacji, która wie w jaki sposób pobierać, zapisywać i uaktualniać informacje w bazie danych. Warstwa ta nie wie kto pobiera informacje lub komu je przekazuje.
Warstwy można przedstawić w postaci diagramu:


Jak widać, między warstwami nie ma zależności - tak więc można dowolnie modyfikować jedną bez zmian w innych. Strzałkami zaznaczono kolejność przejścia zapytania od jednej warstwy do pozostałych. Czerwonymi strzałkami zaznaczono drogę zapytania od klienta do danych, a zieloną zwrócone dane.

Microsoft udostępnia wiele sposobów uzyskania takiej architektury. Najprostszymi z nich są LINQ oraz Entity Framework. Za pomocą tych narzędzi jesteśmy w stanie w prosty i szybki sposób wygenerować automatycznie schematy tabel i metod zawartych w bazie danych do postaci obiektów i metod pracujących na nich.

Starszym rozwiązaniem, lecz nadal stosowanym jest korzystanie z DataSet-ów. Postaram się przedstawić w jaki sposób korzystać właśnie z tego rozwiązania w budowaniu własnej aplikacji wielowarstwowej.

Na potrzeby omawianego zagadnienia stworzymy prostą bazę danych:



Do tabel wprowadzamy po kilka rekordów.

Teraz przejdziemy do tworzenia naszych warstw. Zaczniemy od najniższej - odpowiedzialnej za interakcję z bazą danych. Tak więc na początek tworzymy pustą solucję (Blank Solution) w której będziemy umieszczać kolejne komponenty.

Do naszej solucji na początek dodajemy projekt Class Library o przykładowej nazwie DataAccessLayer. W stworzonym projekcie tworzy się nam plik Class1.cs i dodawane są potrzebne referencje. Dla nas plik Class1.cs jest zbędny - usuwamy go więc. Następnie dodajmy do projektu DataSet i korzystając z Server Explorer "przeciągamy ze stworzonej przez nas bazy danych tabele do projektu:


Jak można zauważyć, referencja tabeli person i car z MS SQL została odwzorowana bez żadnych dodatkowych zabiegów z naszej strony.

Jak można zobaczyć w pliku CarsSet.Designer.cs, zostały stworzone klasy odpowiedzialne za tabele Car i Person, oraz klasy xTableAdapter, które pozwalają na łatwe manipulowanie danymi. Domyślnie otrzymujemy metody SELECT, UPDATE, INSERT, DELETE. Warto jednak przyjrzeć się im i dokonać modyfikacji. Dla przykładu biorąc komendę DELETE dla tabeli Car, zapytanie do bazy danych ma postać:

DELETE FROM Car
WHERE (IdCar = @Original_IdCar) AND (Marque = @Original_Marque) AND (Model = @Original_Model) AND (ProductionYear = @Original_ProductionYear) AND
(IdPerson = @Original_IdPerson)

Raczej nigdy nie skorzystamy z takiej postaci zapytania a w większości przypadków będziemy korzystali z usuwania po kluczu głównym - czyli IdCar. Tak więc modyfikujemy zapytanie do postaci:

DELETE FROM Car
WHERE (IdCar = @Original_IdCar)

Przejdźmy teraz do dodawania własnych zapytań. Klikamy prawym klawiszem myszy na DataSecie i wybieramy operację Add -> Query. Otwiera się edytor pozwalający na dowolną operację na danych w bazie. W moim przypadku chcę dodać zapytanie zwracające wyniki. Tak więc wybieram Use SQL statements a następnie SELECT which returns a single value (zapytanie zwracające pojedyncza wartość). Domyślnie otrzymujemy zapytanie:

SELECT COUNT(*) FROM Car

czyli zliczające ilość rekordów w tabeli Car. Na końcu wprowadzamy jeszcze tylko nazwę zapytania (np. RowCount) i klikamy na Finish. Następnie możemy dodać metodę zwracającą informację o samochodzie na podstawie jego Id. Tak jak poprzednio nadajemy nazwę metodzie i kończymy akcję kreatora.

Po dodaniu wymaganych zapytań, przechodzimy do edycji klas. Istnieją dwa podejścia do tego zadania. Pierwsze polega na wyedytowaniu stworzonych przez designer gotowych plików klas. Drugi to utworzenie pliku z nagłówkami klas i słówkiem kluczowym partial. Drugie rozwiązanie jest o tyle lepsze, że ułatwia czytanie kodu (kod wygenerowany przez designer może mieć tysiące linii).

Tworzymy więc w projekcie Code File a w nim kolejno tworzymy potrzebne namespaces oraz klasy i metody:

using System;

namespace DataAccessLayer
{
    public partial class CarSet
    {
        public partial class CarDataTable
        {
            public CarDataTable GetAllCars()
            {
                CarSetTableAdapters.CarTableAdapter adapter = new CarSetTableAdapters.CarTableAdapter();
                return adapter.GetData();
            }
        }

        public partial class CarRow
        {
            public CarDataTable GetCarInformation()
            {
                CarSetTableAdapters.CarTableAdapter adapter = new CarSetTableAdapters.CarTableAdapter();
                return adapter.CarById(this.IdCar);
            }
        }

        public partial class PersonDataTable
        {

        }

        public partial class PersonRow
        {
            public PersonDataTable GetPersonInformation()
            {
                CarSetTableAdapters.PersonTableAdapter adaper = new CarSetTableAdapters.PersonTableAdapter();
                return adaper.PersonById(this.IdPerson);
            }
        }
    }
}

namespace DataAccessLayer.CarSetTableAdapters
{
    public partial class CarTableAdapter
    {
        public CarSet.CarDataTable GetCars(int startRecord, int maxRecords, string orderColumn = null)
        {
            Adapter.SelectCommand = CommandCollection[0];
            CarSet carSet = new CarSet();
            if (!string.IsNullOrWhiteSpace(orderColumn))
            {
                Adapter.SelectCommand.CommandText += "ORDER BY " + orderColumn;
            }

            Adapter.Fill(startRecord, maxRecords, new System.Data.DataTable[] {carSet.Car});

            return carSet.Car;
        }
    }

    public partial class PersonTableAdapter
    {

    }
}

Tak więc dodając kolejne metody tworzymy warstwę danych. Teraz przejdziemy do warstwy logiki biznesowej.

Logika biznesowa jak wiemy z wcześniejszego akapitu odpowiada za warstwę komunikującą się z prezentacją i dostępem do danych. Metody implementowane w warstwie logiki biznesowej w większości zwracają DataSety z danymi.
Tworzymy więc nowy projekt w naszej solucji typu Class Library o nazwie np. BusinessLogicLayer. Dodajemy referencję do warstwy dostępu danych i w tak przygotowanym projekcie zaczynamy tworzyć klasy i metody odpowiedzialne za przetwarzanie danych.

W większości przypadków w warstwie logiki biznesowej tworzymy klasy mapujące te zawarte w warstwie dostępu do danych. W naszym przypadku będą to więc dwie klasy:
CarBL oraz PersonBL. Dodajemy odpowiednie klauzule using:

using DataAccessLayer;
using DataAccessLayer.CarSetTableAdapters;

Następnie tworzymy metody:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DataAccessLayer;
using DataAccessLayer.CarSetTableAdapters;

namespace BusinessLogicLayer
{
    public class CarBL
    {
        private CarTableAdapter adapter = new CarTableAdapter();

        public CarSet.CarDataTable GetCars()
        {
            return adapter.GetData();
        }

        public CarSet.CarDataTable GetCustomerById(int i)
        {
            return adapter.CarById(i);
        }

        public int? GetRowsCount()
        {
            return adapter.RowCount();
        }
    }
}


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DataAccessLayer;
using DataAccessLayer.CarSetTableAdapters;

namespace BusinessLogicLayer
{
    public class PersonBL
    {
        private PersonTableAdapter adapter = new PersonTableAdapter();

        public CarSet.PersonDataTable GetAllPersons()
        {
            return adapter.GetData();
        }

        public CarSet.PersonDataTable GetPersonById(int i)
        {
            return adapter.PersonById(i);
        }

        public int? GetRowsCount()
        {
            return adapter.RowCount();
        }
    }
}


Oczywiście należy jeszcze dodać metody odpowiadające z usuwanie, dodawanie oraz modyfikowanie danych w bazie danych.

Oprócz tego klasy i metody możemy oznaczyć znacznikami znajdującymi się w przestrzeni System.ComponentModel, dzięki czemu w łatwy sposób będziemy mogli bindować kontrolki do warstwy biznesowej:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DataAccessLayer;
using DataAccessLayer.CarSetTableAdapters;
using System.ComponentModel;

namespace BusinessLogicLayer
{
    [DataObject]
    public class PersonBL
    {
        private PersonTableAdapter adapter = new PersonTableAdapter();

        [DataObjectMethod(DataObjectMethodType.Select, true)] //Komenta typu select; true - domyślna
        public CarSet.PersonDataTable GetAllPersons()
        {
            return adapter.GetData();
        }

        [DataObjectMethod(DataObjectMethodType.Select, false)]
        public CarSet.PersonDataTable GetPersonById(int i)
        {
            return adapter.PersonById(i);
        }

        [DataObjectMethod(DataObjectMethodType.Select, false)]
        public int? GetRowsCount()
        {
            return adapter.RowCount();
        }
    }
}



Czas teraz na ostatni etap tworzenia projektu - warstwa prezentacji. Do prezentacji danych Microsoft dostarcza wielu technologi: Silverlight, WindowsForms, ASP.NET, WPF itp. W tym projekcie zastosuję ASP.NET gdyż to aplikacje internetowe cieszą się największym zainteresowaniem w dzisiejszych czasach.

Tworzymy więc nowy projekt typu ASP.NET WebSite i dodajemy w nim referencję do warstwy biznesowej.

Następnie tworzymy pustą stronę i dodajemy do niej kontrolkę GridView. Należy teraz skonfigurować kontrolkę, aby czerpała dane z warstwy biznesowej. Wybieramy Choose Data Source a następnie New Data Source. W kreatorze który się utworzy wybieramy Object i nadajemy mu nazwę. Teraz z listy Choose your business object wybieramy interesującą nas klasę (PersonBL lub CarBL). Kolejno podpinamy metody dla SELECT, UPDATE, INSERT oraz DELETE. Po uruchomieniu aplikacji otrzymujemy tabelkę wypełnioną odpowiednimi danymi:


Tak więc całość po drobnych modyfikacjach, dodaniu kolejnych metod spowoduje, że rozwój projektu będzie prosty i szybki. Dzięki podziałowi można stworzyć kilka modułów z różną funkcjonalnością, lub nawet umożliwić ich wymianę w czasie działania programu. 

Opisany powyżej proces można przyśpieszyć poprzez zastosowanie nowszych technologi ORMów jak LINQu czy też Entity Framework. Jeżeli mówimy o aplikacjach internetowych można skorzystać z wzorca MVC (Model, View, Controler) który koncepcją nie odbiega od powyżej przedstawionego rozwiązania. 

Projekt można pobrać stąd.

Źródło:
http://en.wikipedia.org/wiki/Three-tier_%28computing%29#Three-tier_architecture
http://pl.wikipedia.org/wiki/Architektura_wielowarstwowa
http://pl.wikipedia.org/wiki/Architektura_tr%C3%B3jwarstwowa
http://www.codeproject.com/KB/architecture/Application_Architecture.aspx
http://www.codeproject.com/KB/aspnet/ASP_NET_MVC_WITH_EF.aspx
http://www.youtube.com/watch#!v=UJdRkr3tB5o&feature=related

sobota, 19 czerwca 2010

Identity w SQL Server

Często zdarza się, że w tabeli klucz główny to pole typu int z autoinkrementacją (identity w SQL Server). Wstawiając nowy wiersz do tabeli nie musimy się martwić o problem znajdowania ostatnio dodawanego identyfikatora i modyfikowaniu go. Z drugiej strony należy też go w miarę potrzeby wyciągnąć jeżeli jest potrzebny przy dodawaniu danych do kilku tabel. Przykładowo weźmy dwie tabele: Osoba i Adres. W tabeli osoba mamy klucz obcy wskazujący na adres znajdujący się w tabeli Adres. Podczas tworzenia nowego użytkownika, najczęściej najpierw tworzymy rekord w tabeli adres a później mając już Id Adresu wstawiamy dane do tabeli Osoba.

W przypadku tabeli z autoinkrementacją, aby pobrać ostatnio tworzony numer autoinkrementacji można skorzystać z kilku dostępnych sposobów w SQL Serwerze:

1. @@Identity

Czyli stosując zapis SELECT @@Identity otrzymamy ostatnio wygenerowany identyfikator niezależnie od tabeli w której został utworzony.

2. Scope_Identity()

Czyli stosując zapytanie SELECT SCOPE_IDENTITY() otrzymujemy identyfikator, który w jawny sposób utworzyliśmy. Nie otrzymamy takich wartości które dla przykładu zostały utworzone za pomocą triggera czy też funkcji.

3. Ident_Current

Stosując zapytanie SELECT Ident_Current('Nazwa_tabeli') otrzymamy identyfikator utworzony jako ostatni dla danej tabeli. Jest to chyba najczęściej wykorzystywana funkcja w przypadku SQL Servera z tabelami w których wykorzystywana jest autoinkrementacja.


Innym sposobem jest stworzenie własnego systemu autoinkrementacji. Dla przykładu można stworzyć procedurę która tworzy wiersz w tabeli pobierając potrzebne parametry od użytkownika. W środku procedury można obsłużyć np. pobranie maksymalnego obecnie ID i dodać np 1 do niego.

Sposobów jest oczywiście wiele i każdy ma swoje wady i zalety.

czwartek, 17 czerwca 2010

Praca ze wskaźnikami w C#

Platforma .NET oferuje możliwość wykorzystania kodu niezarządzanego. Co to znaczy? Jeżeli ktoś kiedyś programował w C lub C++ zagadnienie to nie jest mu obce. Otóż w C++ tworzenie tablicy, której możemy nadać rozmiar w czasie działania programu odbywa się tak:

#include <iostream>
#include <time.h>
#include <stdlib.h>

using namespace std;

int main()
{
    srand(time(NULL));
    //tworzenie tabicy statycznej (umieszczona na stosie)
    int tabs[1000];
    for(int i = 0; i < 1000; ++i)
    {
        tabs[i] = i;
    }

    //tworzenie tablicy dynamiczenj:
    int * tabd = new int[1000];
    //przypisujemy wartości tablicy
    for(int *i = tabd; i < tabd + 1000; ++i)
    {
        *i = rand() % 1000;
    }
    //wypisujemy na ekran zawartość tablicy
    for(int *i = tabd; i < tabd + 1000; ++i)
    {
        cout<<*i<<"  ";
    }
    //zwalniamy zarezerwowany obszar
    delete[] tabd;
}


C# przyzwyczaił nas do prostej konwencji w stylu:

            int[] tab = new int[1000];

Proste i bez komplikowania sobie życia. Czy jednak ta tablica zostanie umieszczona na stosie i jest tak samo wydajna jak ta w C/C++? Otóż nie. W wersji C# tablica zostanie umieszczona na stercie. Co to oznacza dla nas w praktyce? Stratę wydajności. Pytanie więc brzmi: jak dużo tracimy przy takiej konwencji w porównaniu do tablic tworzonych bezpośrednio na stosie?

Jak wspominałem C# umożliwia używanie wskaźników. Postanowiłem więc sprawdzić jaka jest różnica czasu w tworzeniu i wykonywaniu operacji przy pomocy wersji takiej do jakiej przywykliśmy i tablicy stworzonej za pomocą operatora stackalloc. Jak można przczytać na MSDN: "The stackalloc keyword is used in an unsafe code context to allocate a block of memory on the stack." - a więc operator ten umożliwia zarezerwowanie pamięci na stosie w trybie unsafe.

Aby możliwe było użycie trybu unsafe, w opcjach projektu należy zaznaczyć właściwość Allow unsafe code:


Po zaznaczeniu tej właściwości możemy już kompilować kod zawierający kod niezarządzany. Aplikacja która posłuży to testu jest bardzo prosta, mamy dwie metody. Pierwsza tworzy tablica w zwykłym stylu (bez użycia stackalloc) a druga tworzy je na stosie. Następnie wykonywane są operacje na tych tablicach. Metody wywołujemy tyle razy ile użytkownik zażąda. Kod aplikacji:

        private void button1_Click(object sender, EventArgs e)
        {
            Stopwatch clock = new Stopwatch();
            clock.Start();
            for (int i = 0; i < int.Parse(textBox1.Text); i++)
            {
                manageCode();
            }
            clock.Stop();
            label1.Text = clock.ElapsedMilliseconds.ToString();

            clock.Reset();
            clock.Start();
            for (int i = 0; i < int.Parse(textBox1.Text); i++)
            {
                unmanageCode();
            }
            clock.Stop();
            label2.Text = clock.ElapsedMilliseconds.ToString();
        }

        private void manageCode()
        {
            int[] tab1 = new int[1500];
            int[] tab2 = new int[1500];
            int[] tab3 = new int[1500];

            for (int k = 0; k < tab1.Length; k++)
            {
                tab1[k] = 10 + 10;
            }

            for (int k = 0; k < tab1.Length; k++)
            {
                tab2[k] = 10 + 10;
            }

            for (int k = 0; k < tab1.Length; k++)
            {
                for (int z = 0; z < tab1.Length; z++)
                {
                    tab3[k] += tab1[k] + tab2[z];
                }
            }
        }

        private unsafe void unmanageCode()
        {
            int* tab1 = stackalloc int[1500];
            int* tab2 = stackalloc int[1500];
            int* tab3 = stackalloc int[1500];

            for (int* k = tab1; k < (tab1 + 1500); k++)
            {
                *k = 10 + 10;
            }

            for (int* k = tab2; k < (tab2 + 1500); k++)
            {
                *k = 10 + 10;
            }

            for (int* k = tab3; k < (tab3 + 1500); k++)
            {
                for (int z = 0; z < 1500; z++)
                {
                    *k += *(tab1 + z) + *(tab2 + z);
                }
            }
        }
    }

Sama formatka aplikacji jest bardzo prosta: dwa labele, textbox oraz button:


Wyniki testów dla 500 iteracji kodu zarządzanego i niezarządzanego przedstawiają się następująco:


Prawie 5 sekund różnicy. Czy to dużo czy mało? Jeżeli nasza aplikacja mocno wykorzystuje tablice i często je tworzy, warto zastanowić się nad modyfikacją ich do postaci rezerwowanej na stosie.

Przedstawione tutaj przykłady korzystają z tablic jednowymiarowych. Co w takim razie z tablicami wielowymiarowymi?

W C++ tablicę statyczną i dynamiczną wielowymiarową tworzymy w następujący sposób:

#include <iostream>
#include <time.h>
#include <stdlib.h>

using namespace std;

int main()
{
    srand(time(NULL));
    //tworzenie tabicy dwuwymiarowej statycznej (umieszczona na stosie)
    int tab[100][100];
    for(int i = 0; i < 100; ++i)
    {
        for(int j = 0; j < 100; ++j)
        {
            tab[i][j] = rand() % 1000;
        }
    }

    //tworzenie tablicy dwuwymiarowej dynamicznej (na stercie)
    int n = 1000;
    //przydzielamy pamięć tablicy wskaźników do wskaźników typu int
    int **tabd = new int*[n];
    //dla każdego wskaźnika w tablicy przydzielamy pamięć na tablicę o n elementach
    for(int i = 0; i < n; ++i)
    {
        tabd[i] = new int[n];
    }

    //kasowanie w odwrotnej kolejności niż tworzenie - najpierw najbardziej wewnętrzny wymiar
    for(int i = 0; i < n; ++i)
    {
        delete[] tabd[i];
    }
    //na końcu zewnętrzna tablica wskaźników
    delete[] tabd;


    //Jagged arrays (o dowolnych wymiarach wierszy)
    //tablica będzie przypominała schodki
    int **tabj = new int*[n];
    for(int i = 1; i < n + 1; ++i)
    {
        tabj[i - 1] = new int[i];
    }

    //Kasowanie tablicy jak poprzedni
    for(int i = 1; i < n + 1; ++i)
    {
        delete[] tabj[i - 1];
    }
    delete[] tabj;
}

Operacje równorzędne dla C# mogłyby wyglądać następująco:

            //tablica dwuwymiarowa
            int[,] tab = new int[100, 100];

            //jagged array
            int[][] tabj = new int[100][];
            for (int i = 0; i < tabj.Length; i++)
            {
                tabj[i] = new int[100];
            }

Mniej kodu a jak tym razem wygląda sprawa wydajności?

Aby sprawdzić szybkość wykonywania wersji niezarządzanej i zarządzanej skorzystam z operacji mnożenia macierzy. Implementowana metoda to mnożenie Cauchy'ego. Metoda ta wykonuje sporo operacji, tak więc nada się w sam raz na benchmark.

Ciekawą sprawą tego zagadnienia jest fakt, że w C# nie działa następujący kod:

            int n = 500;
            int** tab1 = stackalloc int*[n];
            for (int i = 0; i < n; i++)
            {
                tab1[i] = stackalloc int[n];
            }

Stackalloc nie obsługuje tablic wielowymiarowych. Łatwo jednak osiągnąć wielowymiarowość. Należy od razu zadeklarować tablicę potrzebnych rozmiarów a później odnosić się do elementów mnożąc odpowiednio ilość elementów w wierszu razy żądany wiersz dodając do tego pozycję żądanego elementu w wierszu:

        private void button1_Click(object sender, EventArgs e)
        {
            Stopwatch clock = new Stopwatch();
            clock.Start();
            for (int i = 0; i < int.Parse(textBox1.Text); i++)
            {
                manageCode();
            }
            clock.Stop();
            label1.Text = clock.ElapsedMilliseconds.ToString();

            clock.Reset();
            clock.Start();
            for (int i = 0; i < int.Parse(textBox1.Text); i++)
            {
                unmanageCode();
            }
            clock.Stop();
            label2.Text = clock.ElapsedMilliseconds.ToString();
        }

        private void manageCode()
        {
            Random rand = new Random();
            int n = 100;
            int[,] a = new int[n, n];
            int[,] b = new int[n, n];
            int[,] c = new int[n, n];

            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    a[i, j] = rand.Next(0, 1000);
                    b[i, j] = rand.Next(0, 1000);
                }
            }

            int tmp;
            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    tmp = 0;
                    for (int r = 0; r < n; r++)
                    {
                        tmp += a[i, r] * b[r, j];
                    }
                    c[i, j] = tmp;
                }
            }
        }

        private unsafe void unmanageCode()
        {
            Random random = new Random();
            int n = 100;
            int * a = stackalloc int[n * n];
            int * b = stackalloc int[n * n];
            int * c = stackalloc int[n * n];

            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    *(a + i * n + j) = random.Next(0, 1000);
                    *(b + i * n + j) = random.Next(0, 1000);
                }
            }

            int tmp;
            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    tmp = 0;
                    for (int r = 0; r < n; r++)
                    {
                        tmp += *(a + i * n + r) * *(b + r * n + j);
                    }
                    *(c + i * n + j) = tmp;
                }
            }
        }

Jedyny mankament jaki występuje w powyższym sposobie to ilość pamięci stosu. Standardowo stos w aplikacji .NET zajmuje 1 MB tak więc maksymalny rozmiar tablicy intów jaki stworzymy to trochę ponad 260 tys. elementów (powyżej tej wartości otrzymamy wyjątek stackoverflow). Można jeszcze pokusić się o zwiększenie rozmiaru stosu jest to jednak niezalecana operacja.

Wróćmy do testu i zobaczmy na wynik:


Tak więc znów mamy do czynienia z dwukrotnie większą wydajnością niż w przypadku standardowych tablic.

Ważną kwestią która z pewnością teraz nurtuje to zwalnianie pamięci. Otóż w przypadku C++ mieliśmy instrukcję delete[] tab; W kodzie unsafe C# nigdzie nie zwalniałem pamięci zarezerwowanej przez stackalloc. Otóż w tej kwestii wyręcza nas znów CLR i samo zajmie się zwolnieniem pamięci w momencie opuszczenia bloku instrukcji (w tym przypadku funkcji).

Podsumowując - stackalloc przydaje się w przypadku częstego tworzenia tablic. Pozwala przyśpieszyć kod nawet 3 krotnie (po dodaniu optymalizacji kodu). Tak więc niekiedy warto zaryzykować i pobawić się wskaźnikami aby uzyskać lepszą wydajność;)