1. Giriş
Əvvəlki mühazirədə C# və .NET Runtime-ın yaddaş idarəetməsinin çox hissəsini necə öz üzərinə götürdüyündən danışdıq. Lakin bəzən performansın optimizasiyası və ya unmanaged kodla qarşılıqlı əlaqə (məsələn, C++-da yazılmış sistem API-ləri) üçün yaddaşa daha aşağı səviyyəli nəzarət etmək lazım gəlir. C# bu məqsədlər üçün bir sıra imkanlar təklif edir ki, onlar güclüdür, amma ehtiyatlı istifadə tələb edir.
C#-da "aşağı səviyyəli yaddaş idarəetməsi" dedikdə adətən nəzərdə tuturuq:
- Göstəricilər ilə birbaşa iş: C/C++-dakı kimi yaddaşa ünvanla çıxış.
- GC heap-dən kənar yaddaş ayırma: Garbage collector tərəfindən izlənməyən yaddaşdan istifadə.
- Managed/unmanaged sərhədində resursların idarə edilməsi: native kitabxanalarla effektiv qarşılıqlı əlaqə.
C#-da adi kodun birbaşa yaddaş ünvanları (göstəricilər) ilə işləməsinə icazə verilmir ki, bu da təhlükəsizlik və stabillik üçün edilir. Amma aşağı səviyyəli əməliyyatlar üçün göstəricilər unsafe kontekstində istifadə oluna bilər.
unsafe konteksti nədir?
unsafe sözü ilə işarələnmiş blok və ya metod sizə göstərici sintaksisini istifadə etməyə və CLR tərəfindən təhlükəsizliyi yoxlanılmayan əməliyyatlar yerinə yetirməyə imkan verir. unsafe kontekstindəki kod native koddan heç də daha az təhlükəsiz deyil. unsafe koddan istifadə etmək üçün layihə /unsafe seçimi ilə kompilyasiya olunmalıdır.
Nümunə: unsafe metod və blok elan etmək
public unsafe class UnsafeExamples
{
// Unsafe metod
public static unsafe void ManipulatePointer(int* ptr)
{
Console.WriteLine($"Qiymət göstəricidən: {*ptr}");
*ptr = 200; // Ünvan üzrə qiyməti dəyişdiririk
}
public static void DemoUnsafeBlock()
{
int value = 100;
unsafe // adi metod daxilində unsafe blok
{
int* ptr = &value; // Dəyişənin ünvanını alırıq
ManipulatePointer(ptr); // Göstəricini unsafe metoda ötürürük
Console.WriteLine($"Yeni qiymət: {value}"); // Çap: Yeni qiymət: 200
}
}
}
Göstərici tipləri
C#-da qiymət tiplərinə göstəricilər elan etmək olar (int*, bool*, MyStruct*) və həmçinin void-a (void* universal göstərici üçün). Reference tiplərə birbaşa göstərici elan etmək olmaz, amma əgər reference tipin sahəsi qiymət tipidirsə, ona göstərici almaq mümkündür.
Nümunə: Müxtəlif göstərici tipləri
unsafe
{
double d = 123.45;
double* dPtr = &d; // double üçün göstərici
int[] numbers = { 1, 2, 3 };
fixed (int* arrPtr = numbers) // 'fixed' obyektin GC tərəfindən köçürülməsini əngəlləyir
{
Console.WriteLine($"Massivin ilk elementi: {arrPtr[0]}");
Console.WriteLine($"Massivin ikinci elementi: {*(arrPtr + 1)}");
}
}
Göstərici operatorları
- & (ünvan): Dəyişənin ünvanını alır.
- * (dereference): Ünvan üzrə qiyməti alır.
- -> (üzvə çıxış): Göstərici vasitəsilə struktur/klasın üzvünə çıxış (yalnız qiymət tipləri üçün).
- [] (index): Göstərici vasitəsilə massiv elementlərinə çıxış (C/C++-dakı kimi).
2. Pinlənmiş və ayrılmış yaddaş blokları
Göstəricilərlə işləyərkən vacibdir ki, göstəricinin işarə etdiyi obyekt sizin işiniz zamanı GC tərəfindən köçürülməsin. Bunun üçün fixed və stackalloc açar sözlərindən istifadə olunur.
fixed operatoru
fixed operatoru reference tipli dəyişəni yaddaşda "pin" edir, yəni onun GC tərəfindən köçürülməsini fixed-blok bitənə qədər əngəlləyir. Bu, managed obyektlərin ünvanlarını unmanaged koda ötürərkən və ya onlarla birbaşa işləyərkən kritikdir.
Nümunə: Massivlərlə fixed istifadəsi
public unsafe class FixedExample
{
public static void ProcessFixedArray()
{
byte[] buffer = new byte[10];
// Buferi doldururuq
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = (byte)(i + 1);
}
unsafe
{
// Massivi yaddaşda pin edirik, ilk elementə göstərici alırıq
fixed (byte* p = buffer)
{
// İndi p ilə təhlükəsiz işləmək olar, massivin GC tərəfindən köçürülməyəcəyini bilirik
Console.WriteLine($"Birinci bayt üzrə qiymət: {*p}");
Console.WriteLine($"İkinci bayt üzrə qiymət: {*(p + 1)}");
// 'p' göstəricisini native funksiyaya ötürmək olar
} // 'fixed' blokundan çıxanda massiv GC tərəfindən yenidən köçürülə bilər
}
}
}
fixed həmçinin struktur sahələri və string-lərlə də istifadə edilə bilər.
Stack-da yaddaş ayırma: stackalloc
stackalloc stack-da yaddaş bloku ayırmağa imkan verir. Bu çox sürətlidir, amma yaddaş yalnız cari metodun müddətində mövcuddur. Ayırılan yaddaş GC tərəfindən idarə olunmur.
- Üstünlüklər: Çox sürətli ayırma, GC-overhead yoxdur, yaddaşın müəyyən şəkildə sərbəst buraxılması.
- Çatışmazlıqlar: Məhdud ölçü (stack nisbətən kiçikdir), çox böyük ayırmalarda stack overflow (StackOverflowException) riski.
- İstifadə: Kiçik, qısa ömürlü buferlər üçün idealdır.
Nümunə: stackalloc istifadəsi
public unsafe class StackAllocExample
{
public static void ProcessStackAlloc()
{
unsafe
{
// Stack-da 10 int ayırırıq
int* numbers = stackalloc int[10];
for (int i = 0; i < 10; i++)
{
numbers[i] = i * 10;
}
Console.WriteLine($"Birinci elementin qiyməti: {numbers[0]}");
Console.WriteLine($"Beşinci elementin qiyməti: {numbers[4]}");
} // Bu metoddan çıxanda yaddaş avtomatik azad olunur.
}
}
stackalloc Span<T> ilə istifadə oluna bilər və bu, belə yaddaşa işləməni daha təhlükəsiz edir, çünki Span<T> idarə olunan bir strukturdur və ardıcıl yaddaş bloklarına təhlükəsiz çıxış təmin edir. Span haqqında daha ətraflı növbəti mühazirədə olacaq.
Nümunə: stackalloc və Span<T>
using System;
public class StackAllocWithSpan
{
public static void DemoSpanStackAlloc()
{
// Stack-da ayırma, amma təhlükəsiz Span
vasitəsilə işləyirik Span
buffer = stackalloc int[10]; for (int i = 0; i < buffer.Length; i++) { buffer[i] = i * 2; } Console.WriteLine($"Span-ın ilk elementi: {buffer[0]}"); Console.WriteLine($"Span-ın son elementi: {buffer[9]}"); // Span
-ı onu qəbul edən metodlara ötürmək olar PrintSpan(buffer); } public static void PrintSpan(Span
s) { foreach (var item in s) { Console.Write($"{item} "); } Console.WriteLine(); } }
Bu hibrit yanaşmadır: yaddaşı stack-da (aşağı səviyyəli) ayırırsınız, amma Span<T> vasitəsilə təhlükəsiz (yüksək səviyyəli) çıxış əldə edirsiniz.
3. Unmanaged kodla qarşılıqlı əlaqə (P/Invoke)
Platform Invoke (P/Invoke) C# kodunun unmanaged kitabxanalardakı funksiyaları çağırmasına imkan verən mexanizmdir (məsələn, Windows API DLL-ləri və ya Linux .so faylları). Bu aşağı səviyyəli yaddaş idarəetməsinin əsas aspektidir, çünki tez-tez managed və unmanaged dünyalar arasında göstəricilər ötürürsünüz.
Xarici funksiyaların elan edilməsi
Native funksiyalara uyğun gələn static extern metodları elan etmək üçün DllImport atributundan istifadə edirsiniz.
Nümunə 3.1: Windows API funksiyasının çağırılması
using System.Runtime.InteropServices;
public class PInvokeExample
{
// user32.dll-dən native MessageBox funksiyasını import edirik
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);
public static void ShowMessageBox()
{
// Native funksiyanı çağırırıq
MessageBox(IntPtr.Zero, "C#-dan salam!", "Pəncərə başlığı", 0);
}
}
Marşalinq (marshaling) məlumatları
Native funksiyalar çağırılarkən .NET Runtime marshaling həyata keçirir — managed və unmanaged formatlar arasında tip çevirmələri. Məsələn, C#-dakı string C++-da char* və ya wchar_t* kimi marshalanır.
Daha mürəkkəb marshaling üçün MarshalAs atributu və Marshal sinfindən istifadə etmək olar.
Nümunə: Struktur marshalingi
using System.Runtime.InteropServices;
// Bu struktur native struktura marshalanacaq
[StructLayout(LayoutKind.Sequential)] // Sahələrin ardıcıllığını göstəricisi
public struct NativePoint
{
public int X;
public int Y;
}
public class StructMarshalExample
{
[DllImport("your_native_lib.dll")] // Misal: native kitabxanadakı funksiya
public static extern void ProcessPoint(NativePoint point);
[DllImport("your_native_lib.dll")]
public static extern void FillPoint(out NativePoint point); // Struktur üçün pointer qəbul edir
public static void DemoStructMarshal()
{
NativePoint myPoint = new NativePoint { X = 10, Y = 20 };
ProcessPoint(myPoint); // Struktur dəyər üzrə marshalanacaq (kopya)
NativePoint resultPoint;
FillPoint(out resultPoint); // Native funksiya strukturu dolduracaq
Console.WriteLine($"Native-dən gələn Point: ({resultPoint.X}, {resultPoint.Y})");
}
}
4. GCHandle və obyektlərin pinlənməsi
GCHandle obyektə managed heap-də "handle" almağa və lazım gəldikdə onu müvəqqəti pinləməyə imkan verən strukturdur, yəni obyektin köçürülməsinin və ya toplanmasının qarşısını alır. Bu, managed obyektin sabit ünvanını unmanaged koda ötürmək lazım olduqda faydalıdır.
Nümunə: GCHandle ilə obyektin pinlənməsi
using System.Runtime.InteropServices;
public class GCHandleExample
{
public static void PinObject()
{
byte[] data = new byte[100];
GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned); // Massivi yaddaşda pin edirik
try
{
IntPtr pointer = handle.AddrOfPinnedObject(); // Pin edilmiş obyektin ünvanını alırıq
Console.WriteLine($"Pin edilmiş massiv ünvanı: {pointer:X}");
// İndi 'pointer' native funksiyaya təhlükəsiz ötürülə bilər.
// Native funksiya bu yaddaşla birbaşa işləyə bilər.
Marshal.WriteByte(pointer, 0, 255); // Göstərici vasitəsilə ilk baytı dəyişirik
Console.WriteLine($"Massivin ilk baytı: {data[0]}"); // Çap: 255
}
finally
{
if (handle.IsAllocated)
{
handle.Free(); // Handle-i azad edirik, GC yenidən obyektə nəzarət edə bilər
}
}
}
}
P/Invoke və GCHandle-in detallı izahı bu kursun çərçivəsindən çıxır, amma onlar Windows kitabxanalarını birbaşa çağırmaq istədiyiniz zaman çox faydalı ola bilər. Ən azı, indi nəyi haradan öyrənmək lazım olduğunu bilirsiniz.
GO TO FULL VERSION