Unsafe code

Na wstępie należy powiedzieć, że używanie wskaźników w języku C# wymaga użycia tzn. unsafe context. Polecenia takie wykonywane są poza kontrolą Garbage Collector.

C# pozwala na używanie wskaźników kiedy bloku kodu jest oznaczony jako unsafe. Kod niezabezpieczony lub też kod niekontrolowany to blok kodu używający wskaźników.


Wskaźniki

Wskaźnik jest zmienną, której wartość to adres innej zmiennej, np. bezpośredni adres w pamięci. Podobnie do innych zmiennych lub stałych, wskaźnik należy zadeklarować zanim zaczniemy przechowywać w nim adres danej zmiennej.
Poniżej przykładowa deklaracja wskaźników:

namespace Wskazniki
{
    class Program
    {
        // oznaczamy blok kodu jako unsafe
        // dodatkowo we właściwościach projektu, w zakładce Build 
        // należy zaznaczyć: "Allow unsafe code"
        unsafe static void Main(string[] args)
        {
            int* ip;
            double* dp;
            float* fp;
            char* chp;
        }
    }
}

Poniżej przykład użycia wskaźników wewnątrz bloku unsafe:

using System;
namespace Wskazniki
{
    class Program
    {
        // oznaczamy blok kodu jako unsafe
        // dodatkowo we właściwościach projektu, w zakładce Build 
        // należy zaznaczyć: "Allow unsafe code"
        unsafe static void Main(string[] args)
        {
            int* ip;
            //double* dp;
            //float* fp;
            //char* chp;
            // Deklaracja zmiennej całkowitej
            int number = 20;
            // przypisanie do wskaźnika adresu pamięci zmiennej
            // Wyrażenie &nazwa_zmiennej zwaraca nam adres pamięci tej zmiennej
            ip = &number;
            Console.WriteLine("Wartość liczby: {0}", number);
            Console.WriteLine("Adres w pamięci: {0}", (int)ip);
            Console.ReadKey();
            // Wynik działania programu
            // Wartosc liczby: 20
            // Adres w pamieci: 119991236
        }
    }
}

Pobieranie wartości przy użyciu wskaźników

Dane przechowywane w lokalizacji wskazywanej przez zmienną wskaźnikową możesz pobrać używając metody ToString().
Poniższy przykład pokazuje taką możliwość:

using System;
namespace Wskazniki
{
    class Program
    {
        // oznaczamy blok kodu jako unsafe
        // dodatkowo we właściwościach projektu, w zakładce Build 
        // należy zaznaczyć: "Allow unsafe code"
        unsafe static void Main(string[] args)
        {
            int* ip;
            //double* dp;
            //float* fp;
            //char* chp;
            // Deklaracja zmiennej całkowitej
            int number = 20;
            // przypisanie do wskaźnika adresu pamięci zmiennej
            // Wyrażenie &nazwa_zmiennej zwaraca nam adres pamięci tej zmiennej
            ip = &number;
            Console.WriteLine("Wartość liczby: {0}", number);
            // Pobieranie danych bezpośrednio ze wskaźnika
            Console.WriteLine("Wartość liczby: {0}", ip->ToString());
            Console.WriteLine("Adres w pamięci: {0}", (int)ip);
            Console.ReadKey();
            // Wynik działania programu
            // Wartosc liczby: 20
            // Wartosc liczby: 20
            // Adres w pamieci: 119991236
        }
    }
}

Przekazywanie parametrów jako parametry metody

Można również przekazać wskaźnik do metody jako jej parametr.
Poniższy przykład pokazuje taką możliwość:

using System;
namespace WskaznikiParametrMetody
{
    class Program
    {
        public unsafe void Swap(int* a, int* b)
        {
            int temp = *a;
            *a = *b;
            *b = temp;
        }
        unsafe static void Main(string[] args)
        {
            Program pr = new Program();
            int a = 10;
            int b = 20;
            int* ap = &a;
            int* bp = &b;
            Console.WriteLine("Przed zmianą: a = {0}, b = {1}", a, b);
            pr.Swap(ap, bp);
            Console.WriteLine("Po zmianie: a = {0}, b = {1}", a, b);
            Console.ReadKey();
        }
    }
}

Dostęp do elementów tablicy przy użyciu wkaźników

W języku C#, nazwa tablicy oraz wskaźnik do takiego samego typu danych jak dane w tablicy, nie jest tym samym typem. Dla przykładu, int* p oraz int[] p, nie są tego samego typu. Możesz zwiększać wartość wskaźnika p ponieważ nie jest on stałą wartością w pamięci podczas gdy adres tablicy w pamięci jest wartością stałą i nie może być zwiększony.

Dlatego, jeżeli chcesz uzyskać dostęp do danych tablicy przy użyciu wskaźników, należy ustalić wskaźnik w pamięci używając do tego słowa kluczowego fixed.
Poniżej przykład ułatwiający zrozumienie problemu:

using System;
namespace WskaznikiArray
{
    class Program
    {
        // Pamiętajcie o zakładce Build we właściwościach projektu
        // i ustawieniu: "Allow unsafe code"
        unsafe static void Main(string[] args)
        {
            // deklaracja tablicy
            int[] tablica = { 100, 200, 300 };
            // ustalamy wskaźnik naszej tablicy
            fixed(int* ptr = tablica)
            
            // adres tablicy przechowujemy we wskaźniku
            for (int i = 0; i < 3; i++)
            {
                Console.WriteLine("Adres tablicy[{0}] = {1}", i, (int)(ptr + i));
                Console.WriteLine("Wartość tablicy[{0}] = {1}", i, *(ptr + i));
                Console.WriteLine("Wartość/kolejny sposób: {0}", (ptr + i)->ToString());
            }
            Console.ReadKey();
            // Wynik działania programu
            // Adres tablicy[0] = 33420824
            // Wartosc tablicy[0] = 100
            // Wartosc/kolejny sposób: 100
            // Adres tablicy[1] = 33420828
            // Wartosc tablicy[1] = 200
            // Wartosc/kolejny sposób: 200
            // Adres tablicy[2] = 33420832
            // Wartosc tablicy[2] = 300
            // Wartosc/kolejny sposób: 300
        }
    }
}