TP

C# - Unsafe kód a ukazatele

Co tento článek ukazuje

Častým mylným argumentem proti jazyku C# je, že v tomto jazyce není narozdíl od C++ možné pracovat s ukazateli. V C# lze s ukazateli pracovat díky takzvanému "unsafe kódu", ale pravdou je, že to ve většině případů není potřeba. Tento článek ukazuje jakým způsobem lze v C# přímo přistupovat do paměti a na závěr i jeden příklad, který ukazuje kdy je vhodné ukazatele použít.

Práce s ukazateli v C#

Klíčové slovo unsafe

Následující příklad ukazuje, jak lze napsat funkci, která přes ukazatele zapisuje hodnoty do pole typu int[]. Tato funkce bere jako parametr ukazatel na pole a předpokládá, že velikost pole je nejméně 500 položek. Toto je velmi nebezpečné chování, protože pole může být klidně menší a v tom případě by funkce mohla přepsat jiná data v paměti (a nemuselo by to způsobyt vyjímku). Klíčové slovo unsafe, které je uvedeno v deklaraci metody označuje tuto metodu za "nebezpečnou" a umožňuje v ní pracovat s ukazateli (Toto klíčové slovo lze dle potřeby uvést i k celé třídě nebo pouze jako blok v metodě).

// Naplnuje pole bytu primym pristupem do pameti
// ptr - ukazatel na pole int[]
private unsafe static void UnsafeMethod(int* ptr)
{
  for(int i=0; i<500; i++) {
    *ptr=i; ptr++;
  }
}

Z předcházejícího příkladu je zřejmé, že stejně jako v C/C++ i v C# platí ukazatelová aritmetika a je tedy možné ukazatel na nějaký datový typ posouvat po jednotlivých položkách v poli pomocí operátoru ++ (a samozřejmě i dalších).

Pokud se pokusíte tento kód zkompilovat, compiler ohlásí chybu "Unsafe code may only appear if compiling with /unsafe", což znamená, že je potřeba přidat compileru (csc.exe) jako parametr /unsafe a nebo ve VS.Net ve vlastnostech projektu nastavit Allow Unsafe Code Blocks.

Klíčové slovo fixed

Nyní když máme metodu, která naplňuje pole čísly, je ještě potřeba ukázat jak jí zavolat. K tomu je potřeba použít klíčové slovo fixed, pomocí kterého lze označit blok kde pracujeme s ukazatelem na pole. Tímto klíčovým slovem .NET Frameworku zakážeme přesunutí objektu, takže ukazatel zůstane po celou dobu platný (jeho použití je opět možné pouze v unsafe bloku):

unsafe static void Main(string[] args)
{
  // vytvori pole a preda ukazatel
  int[] buffer=new int[500];
  fixed(int* p=buffer)
  {
    UnsafeMethod(p);
  }
}

Operátor sizeof

Další důležitou součástí C# při práci s ukazateli je (stejně jako v C++) operátor sizeof, pomocí kterého je možné zjistit velikost jednotlivých hodnotových datových typů (value types) v paměti.

Nebezpečný kód

Již podle klíčového slova unsafe lze poznat, že práce s ukazateli není v C# myšlena jako věc, bez které se člověk při každodení práci neobejde. V následující části naleznete jednu ukázku, která rozhodně neukazuje jak lze rozumně používat ukazatele, proto jí prosím berte s rezervou. Jedná se spíše o demnostraci toho, že ukazatele jsou i v C# poměrně všemocným nástrojem. Následující kód by se tedy rozhodně neměl objevit v žádné reálné aplikaci (a prosím čtenáře se slabším žaludkem, ať následující odstavec přeskočí).

Tato ukázka vychází z toho, že .NET si před řetězec ukládá do 4 bytů (tedy datový typ int) délku řetězce. Tuto délku zjišťuje tak, že získá ukazatel na začátek řetězce, přetypuje ho na ukazatel typu int* a posune se na předcházející položku (tedy o 4 byty před řetězec) a přečte délku.

string str="Hello world";
fixed(char* ptr=str)
{
  // ziskame ukazatel na retezec
  int* p=(int*)ptr; 
  // presuneme se na delku (-4 byty)
  p--; 
  Console.WriteLine("Delka: {0}",*p);
}

Práce s obrázky

Jedním s mála případů, kdy je velmi vhodné použít ukazatele je situace, kdy potřebujete přistupovat přímo k datům v bitmapě. Třída Bitmap sice obsahuje metody pro zjištění a nastavení barvy pixelu, ale tato metoda je řádově pomalejší. O práci s obrázky jsem již dříve psal (viz [1]), ale v tomto článku bude celý kód lépe popsaný a vysvětlený. Více o práci s obrázky můžete najít v seriálu Christiana Grausse [2]. V následující (zjednodušené) ukázce vyplníme bitmapu náhodnými body a složitější příklad (viz screenshot) naleznete v přiložených zdrojových kódech.

Objekt Bitmap obsahuje metodu LockBits, pomocí které lze získat přístup k datům v bitmapě. Tato metoda vrací strukturu, která mimo jiné obsahuje ukazatel na začátek dat bitmapy. Jako parametr této metodě můžete specifikovat formát v jakém mají být data k dispozici. V této ukázce budeme pracovat s formátem Format32bppArgb, který pro každý pixel ukládá barvu jako 4 byty (int), kde 1 byte je průhlednost a další tři byty tvoří barvu ve formátu RGB. Pokud máte barvu v tomto formátu, je možné zkonvertovat ji na strukturu Color pomocí Color.FromArgb a zpět metodou Color.ToArgb.

// Vytvori novou bitmapu
Bitmap ret=new Bitmap(320,200); 
// Ziska pristup k datum bitmapy
BitmapData data=ret.LockBits( 
  new Rectangle(0,0,bmp.Width,bmp.Height), 
  ImageLockMode.WriteOnly,PixelFormat.Format32bppArgb); 

V následující ukázce naleznete kód, který postupně prochází všechny řádky a sloupce v bitmapě a každému pixelu nastaví náhodnou barvu. Ukazatel na začátek bitmapy je položka data.Scan0 ze struktury BitmapData. Tato struktura dále obsahuje položku Stride, která určuje o kolik bytů je třeba posunout ukazatel při přechodu u řádku, protože řádka může v paměti zabírat o něco více než je šířka bitmapy. V této ukázce je tedy nejpodstatnější výpočet pozice na které začíná y-tá řádka bitmapy (po pixelech na řádce se již ukazatel posouvá pomocí operátoru ++).

// generator nahodnych cisel
Random rnd=new Random();

// postupne prochazime po radcich
for(int y=0; y<data.Height; y++) 
{ 
  // vypocte ukazatel na zacatek y-teho radku
  int* retPos=(int*)((int)data.Scan0+(y*data.Stride)); 

  int x=0;
  while(x<data.Width) 
  { 
    // vyplni pixel nahodnou barvou
    *retPos=Color.FromArgb(rnd.Next(256),
      rnd.Next(256),rnd.Next(256)).ToArgb(); 

   // posun na dalsi pixel
   retPos++; x++;
  } 
} 

Na závěr je ještě potřeba uvolnit data, získaná pomocí LockBits a bitmapa je vytvořena.

// Uvolnit data
ret.UnlockBits(data); 

Soubory na stažení a odkazy

Published: Sunday, 5 June 2005, 9:36 PM
Author: Tomas Petricek
Typos: Send me a pull request!
Tags: