Kode tidak aman, jenis penunjuk, dan penunjuk fungsi
Sebagian besar kode C# yang Anda tulis adalah "kode aman yang dapat diverifikasi." Kode aman yang dapat diverifikasi berarti alat .NET dapat memverifikasi bahwa kode aman. Secara umum, kode aman tidak secara langsung mengakses memori menggunakan pointer. Ini juga tidak mengalokasikan memori mentah. Ini justru membuat objek terkelola.
C# mendukung konteks unsafe
, di mana Anda dapat menulis kode yang tidak dapat diverifikasi. Dalam konteks unsafe
, kode dapat menggunakan pointer, mengalokasikan dan membebaskan blok memori, dan memanggil metode menggunakan penunjuk fungsi. Kode tidak aman di C# belum tentu berbahaya; itu hanya kode yang keamanannya tidak dapat diverifikasi.
Kode tidak aman memiliki properti berikut:
- Metode, jenis, dan blok kode dapat didefinisikan sebagai tidak aman.
- Dalam beberapa kasus, kode yang tidak aman dapat meningkatkan performa aplikasi dengan menghapus pemeriksaan batas array.
- Kode tidak aman diperlukan saat Anda memanggil fungsi asli yang memerlukan penunjuk.
- Menggunakan kode yang tidak aman menimbulkan risiko keamanan dan stabilitas.
- Kode yang berisi blok tidak aman harus dikompilasi dengan opsi AllowUnsafeBlocks compiler.
Jenis penunjuk
Dalam konteks tidak aman, sebuah tipe dapat menjadi tipe penunjuk, selain tipe nilai, atau tipe referensi. Deklarasi jenis pointer mengambil salah satu formulir berikut:
type* identifier;
void* identifier; //allowed but not recommended
Jenis yang ditentukan sebelum *
dalam jenis penunjuk disebut jenis referensi .
Jenis penunjuk tidak mewarisi dari objek dan tidak ada konversi antara jenis penunjuk dan object
. Selain itu, boxing dan unboxing tidak mendukung pointer. Namun, Anda dapat mengonversi antara jenis penunjuk yang berbeda dan antara jenis penunjuk dan jenis integral.
Ketika Anda mendeklarasikan beberapa pointer dalam deklarasi yang sama, Anda menulis tanda bintang (*
) bersama dengan tipe dasar saja. Ini tidak digunakan sebagai awalan untuk setiap nama penunjuk. Misalnya:
int* p1, p2, p3; // Ok
int *p1, *p2, *p3; // Invalid in C#
Pengumpul sampah tidak melacak apakah objek sedang diarahkan oleh jenis pointer apa pun. Jika referensi adalah objek dalam tumpukan terkelola (termasuk variabel lokal yang ditangkap oleh ekspresi lambda atau delegasi anonim), objek harus disematkan selama pointer digunakan.
Nilai variabel pointer jenis MyType*
adalah alamat variabel jenis MyType
. Berikut ini adalah contoh deklarasi jenis pointer:
-
int* p
:p
adalah penunjuk ke bilangan bulat. -
int** p
:p
adalah pointer ke pointer ke bilangan bulat. -
int*[] p
:p
adalah array satu dimensi yang terdiri dari penunjuk ke bilangan bulat. -
char* p
:p
adalah pointer ke karakter. -
void* p
:p
adalah penunjuk ke jenis yang tidak diketahui.
Operator indireksi penunjuk *
dapat digunakan untuk mengakses isi di lokasi yang diarahkan oleh variabel pointer. Misalnya, pertimbangkan deklarasi berikut:
int* myVariable;
Ekspresi *myVariable
menunjukkan variabel int
yang ditemukan di alamat yang terkandung dalam myVariable
.
Ada beberapa contoh pointer dalam artikel tentang pernyataan fixed
. Contoh berikut menggunakan kata kunci unsafe
dan pernyataan fixed
, serta menunjukkan cara meningkatkan pointer internal. Anda dapat menempelkan kode ini ke fungsi Utama aplikasi konsol untuk menjalankannya. Contoh-contoh ini harus dikompilasi dengan pengaturan opsi compiler AllowUnsafeBlocks.
// Normal pointer to an object.
int[] a = [10, 20, 30, 40, 50];
// Must be in unsafe code to use interior pointers.
unsafe
{
// Must pin object on heap so that it doesn't move while using interior pointers.
fixed (int* p = &a[0])
{
// p is pinned as well as object, so create another pointer to show incrementing it.
int* p2 = p;
Console.WriteLine(*p2);
// Incrementing p2 bumps the pointer by four bytes due to its type ...
p2 += 1;
Console.WriteLine(*p2);
p2 += 1;
Console.WriteLine(*p2);
Console.WriteLine("--------");
Console.WriteLine(*p);
// Dereferencing p and incrementing changes the value of a[0] ...
*p += 1;
Console.WriteLine(*p);
*p += 1;
Console.WriteLine(*p);
}
}
Console.WriteLine("--------");
Console.WriteLine(a[0]);
/*
Output:
10
20
30
--------
10
11
12
--------
12
*/
Anda tidak dapat menerapkan operator tidak langsung ke penunjuk jenis void*
. Meskipun demikian, Anda dapat menggunakan cast untuk mengonversi void pointer ke jenis pointer lainnya, dan sebaliknya.
Pointer bisa null
. Menerapkan operator penginderaan ke penunjuk null menyebabkan perilaku yang ditentukan oleh implementasi.
Meneruskan pointer antar metode dapat menyebabkan perilaku yang tidak terdefinisi. Pertimbangkan metode yang mengembalikan penunjuk ke variabel lokal melalui parameter in
, out
, atau ref
atau sebagai hasil fungsi. Jika pointer diatur dalam blok tetap, variabel yang ditunjuk oleh pointer tersebut mungkin tidak lagi bersifat tetap.
Tabel berikut mencantumkan operator dan pernyataan yang dapat beroperasi pada penunjuk dalam konteks yang tidak aman:
Operator/Pernyataan | Pakai |
---|---|
* |
Melakukan pengindeksan pointer. |
-> |
Mengakses anggota struct melalui pointer. |
[] |
Mengindeks penunjuk. |
& |
Mendapatkan alamat variabel. |
++ dan -- |
Kenaikan dan penunjuk penurunan. |
+ dan - |
Melakukan aritmatika pointer. |
== , != , < , > , <= , dan >= |
Membandingkan pointer. |
stackalloc |
Mengalokasikan memori pada tumpukan. |
fixed pernyataan |
Memperbaiki variabel untuk sementara waktu sehingga alamatnya dapat ditemukan. |
Untuk informasi selengkapnya tentang operator yang terkait dengan pointer, lihat Operator terkait pointer.
Jenis penunjuk apa pun dapat dikonversi secara implisit ke jenis void*
. Jenis penunjuk apa pun dapat ditetapkan nilainya null
. Jenis tipe penunjuk apa pun dapat dikonversi secara eksplisit ke tipe penunjuk lain menggunakan ekspresi pembacaan. Anda juga dapat mengonversi jenis integral apa pun ke jenis penunjuk, atau jenis penunjuk apa pun ke jenis integral. Konversi ini memerlukan pemeran eksplisit.
Contoh berikut mengonversi int*
menjadi byte*
. Perhatikan bahwa pointer menunjuk ke byte terendah yang diatasi dari variabel. Ketika Anda secara berturut-turut meningkatkan hasilnya, hingga ukuran int
(4 byte), Anda dapat menampilkan byte variabel yang tersisa.
int number = 1024;
unsafe
{
// Convert to byte:
byte* p = (byte*)&number;
System.Console.Write("The 4 bytes of the integer:");
// Display the 4 bytes of the int variable:
for (int i = 0 ; i < sizeof(int) ; ++i)
{
System.Console.Write(" {0:X2}", *p);
// Increment the pointer:
p++;
}
System.Console.WriteLine();
System.Console.WriteLine("The value of the integer: {0}", number);
/* Output:
The 4 bytes of the integer: 00 04 00 00
The value of the integer: 1024
*/
}
Buffer ukuran tetap
Anda dapat menggunakan kata kunci fixed
untuk membuat buffer dengan array ukuran tetap dalam struktur data. Buffer ukuran tetap berguna ketika Anda menulis metode yang berinteroperasi dengan sumber data dari bahasa atau platform lain. Buffer dengan ukuran tetap dapat mengambil atribut atau pengubah apa pun yang diizinkan bagi anggota struct biasa. Satu-satunya batasan adalah bahwa jenis array harus bool
, byte
, char
, short
, int
, long
, sbyte
, ushort
, uint
, ulong
, float
, atau double
.
private fixed char name[30];
Dalam kode aman, struktur C# yang berisi array tidak berisi elemen array. Struktur berisi referensi ke elemen sebagai gantinya. Anda dapat menyematkan array ukuran tetap dalam struct saat digunakan dalam blok kode tidak aman .
Ukuran struct
berikut tidak bergantung pada jumlah elemen dalam array, karena pathName
adalah referensi:
public struct PathArray
{
public char[] pathName;
private int reserved;
}
Struktur dapat berisi array yang disematkan dalam kode yang tidak aman. Dalam contoh berikut, array fixedBuffer
memiliki ukuran tetap. Anda menggunakan pernyataan fixed
untuk mendapatkan penunjuk ke elemen pertama. Anda mengakses elemen array melalui penunjuk ini. Pernyataan fixed
menyematkan bidang instans fixedBuffer
ke lokasi tertentu dalam memori.
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
internal unsafe class Example
{
public Buffer buffer = default;
}
private static void AccessEmbeddedArray()
{
var example = new Example();
unsafe
{
// Pin the buffer to a fixed location in memory.
fixed (char* charPtr = example.buffer.fixedBuffer)
{
*charPtr = 'A';
}
// Access safely through the index:
char c = example.buffer.fixedBuffer[0];
Console.WriteLine(c);
// Modify through the index:
example.buffer.fixedBuffer[0] = 'B';
Console.WriteLine(example.buffer.fixedBuffer[0]);
}
}
Ukuran array char
elemen 128 adalah 256 byte. Buffer berukuran tetap char selalu menggunakan 2 byte per karakter, terlepas dari pengodean. Ukuran array ini tetap sama bahkan ketika buffer karakter di-marshalkan ke metode API atau struktur dengan CharSet = CharSet.Auto
atau CharSet = CharSet.Ansi
. Untuk informasi selengkapnya, lihat CharSet.
Contoh sebelumnya menunjukkan cara mengakses bidang fixed
tanpa menyematkan. Array ukuran tetap umum lainnya adalah array bool . Elemen dalam array bool
selalu berukuran 1 byte. array bool
kurang sesuai untuk membangun array bit atau buffer.
Buffer ukuran tetap dikompilasi dengan System.Runtime.CompilerServices.UnsafeValueTypeAttribute, yang memberi instruksi kepada common language runtime (CLR) bahwa tipe tersebut mengandung array yang tidak dikelola yang berpotensi meluap. Memori yang dialokasikan menggunakan stackalloc juga secara otomatis memungkinkan fitur deteksi overrun buffer di CLR. Contoh sebelumnya menunjukkan bagaimana buffer ukuran tetap bisa ada di unsafe struct
.
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
C# yang dihasilkan oleh kompilator untuk Buffer
ditetapkan sebagai berikut:
internal struct Buffer
{
[StructLayout(LayoutKind.Sequential, Size = 256)]
[CompilerGenerated]
[UnsafeValueType]
public struct <fixedBuffer>e__FixedBuffer
{
public char FixedElementField;
}
[FixedBuffer(typeof(char), 128)]
public <fixedBuffer>e__FixedBuffer fixedBuffer;
}
Buffer berukuran tetap berbeda dari array biasa dengan cara berikut:
- Hanya dapat digunakan dalam konteks
unsafe
. - Mungkin hanya bidang instans struktur.
- Mereka selalu vektor, atau array satu dimensi.
- Deklarasi harus mencakup panjangnya, contohnya adalah
fixed char id[8]
. Anda tidak dapat menggunakanfixed char id[]
.
Cara menggunakan pointer untuk menyalin larik byte
Contoh berikut menggunakan pointer untuk menyalin byte dari satu array ke array lainnya.
Contoh ini menggunakan kata kunci tidak aman , yang memungkinkan Anda menggunakan pointer dalam metode Copy
. Pernyataan tetap digunakan untuk mendeklarasikan penunjuk ke array sumber dan array tujuan. Pernyataan fixed
menyematkan lokasi array sumber dan array tujuan dalam memori sehingga proses pengumpulan sampah tidak memindahkan array. Blok-blok memori untuk array dilepas ketika blok fixed
selesai. Karena metode Copy
dalam contoh ini menggunakan kata kunci unsafe
, metode tersebut harus dikompilasi dengan opsi AllowUnsafeBlocks compiler.
Contoh ini mengakses elemen dari kedua array menggunakan indeks alih-alih pointer kedua yang tidak terkelola. Deklarasi penunjuk pSource
dan pTarget
memfiksasi posisi array.
static unsafe void Copy(byte[] source, int sourceOffset, byte[] target,
int targetOffset, int count)
{
// If either array is not instantiated, you cannot complete the copy.
if ((source == null) || (target == null))
{
throw new System.ArgumentException("source or target is null");
}
// If either offset, or the number of bytes to copy, is negative, you
// cannot complete the copy.
if ((sourceOffset < 0) || (targetOffset < 0) || (count < 0))
{
throw new System.ArgumentException("offset or bytes to copy is negative");
}
// If the number of bytes from the offset to the end of the array is
// less than the number of bytes you want to copy, you cannot complete
// the copy.
if ((source.Length - sourceOffset < count) ||
(target.Length - targetOffset < count))
{
throw new System.ArgumentException("offset to end of array is less than bytes to be copied");
}
// The following fixed statement pins the location of the source and
// target objects in memory so that they will not be moved by garbage
// collection.
fixed (byte* pSource = source, pTarget = target)
{
// Copy the specified number of bytes from source to target.
for (int i = 0; i < count; i++)
{
pTarget[targetOffset + i] = pSource[sourceOffset + i];
}
}
}
static void UnsafeCopyArrays()
{
// Create two arrays of the same length.
int length = 100;
byte[] byteArray1 = new byte[length];
byte[] byteArray2 = new byte[length];
// Fill byteArray1 with 0 - 99.
for (int i = 0; i < length; ++i)
{
byteArray1[i] = (byte)i;
}
// Display the first 10 elements in byteArray1.
System.Console.WriteLine("The first 10 elements of the original are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray1[i] + " ");
}
System.Console.WriteLine("\n");
// Copy the contents of byteArray1 to byteArray2.
Copy(byteArray1, 0, byteArray2, 0, length);
// Display the first 10 elements in the copy, byteArray2.
System.Console.WriteLine("The first 10 elements of the copy are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray2[i] + " ");
}
System.Console.WriteLine("\n");
// Copy the contents of the last 10 elements of byteArray1 to the
// beginning of byteArray2.
// The offset specifies where the copying begins in the source array.
int offset = length - 10;
Copy(byteArray1, offset, byteArray2, 0, length - offset);
// Display the first 10 elements in the copy, byteArray2.
System.Console.WriteLine("The first 10 elements of the copy are:");
for (int i = 0; i < 10; ++i)
{
System.Console.Write(byteArray2[i] + " ");
}
System.Console.WriteLine("\n");
/* Output:
The first 10 elements of the original are:
0 1 2 3 4 5 6 7 8 9
The first 10 elements of the copy are:
0 1 2 3 4 5 6 7 8 9
The first 10 elements of the copy are:
90 91 92 93 94 95 96 97 98 99
*/
}
Penunjuk fungsi
C# menyediakan jenis delegate
untuk menentukan objek penunjuk fungsi yang aman. Memanggil sebuah delegasi melibatkan pembuatan instance dari tipe yang diturunkan dari System.Delegate dan melakukan panggilan metode virtual ke metode Invoke
. Panggilan virtual ini menggunakan instruksi IL callvirt
. Dalam jalur kode kritis performa, menggunakan instruksi calli
IL lebih efisien.
Anda dapat menentukan penunjuk fungsi menggunakan sintaks delegate*
. Pengkompilasi memanggil fungsi menggunakan instruksi calli
daripada membuat instans objek delegate
dan memanggil Invoke
. Kode berikut mendeklarasikan dua metode yang menggunakan delegate
atau delegate*
untuk menggabungkan dua objek dengan jenis yang sama. Metode pertama menggunakan jenis delegasi System.Func<T1,T2,TResult>. Metode kedua menggunakan deklarasi delegate*
dengan parameter yang sama dan jenis pengembalian:
public static T Combine<T>(Func<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) =>
combinator(left, right);
Kode berikut menunjukkan bagaimana Anda akan mendeklarasikan fungsi lokal statis dan memanggil metode UnsafeCombine
menggunakan penunjuk ke fungsi lokal tersebut:
int product = 0;
unsafe
{
static int localMultiply(int x, int y) => x * y;
product = UnsafeCombine(&localMultiply, 3, 4);
}
Kode sebelumnya menggambarkan beberapa aturan pada fungsi yang diakses sebagai penunjuk fungsi:
- Penunjuk fungsi hanya dapat dideklarasikan dalam konteks
unsafe
. - Metode yang mengambil
delegate*
(atau mengembalikandelegate*
) hanya dapat dipanggil dalam konteksunsafe
. - Operator
&
untuk mendapatkan alamat fungsi hanya diizinkan pada fungsistatic
. (Aturan ini berlaku untuk fungsi anggota dan fungsi lokal).
Sintaks memiliki kesamaan dengan mendeklarasikan jenis delegate
dan menggunakan pointer. Akhiran *
pada delegate
menunjukkan deklarasi adalah penunjuk fungsi .
&
saat menetapkan grup metode ke penunjuk fungsi menunjukkan operasi mengambil alamat metode .
Anda dapat menentukan konvensi panggilan untuk delegate*
menggunakan kata kunci managed
dan unmanaged
. Selain itu, Anda dapat menentukan konvensi panggilan untuk penunjuk fungsi unmanaged
. Deklarasi berikut menunjukkan contoh masing-masing. Deklarasi pertama menggunakan konvensi panggilan managed
, yang merupakan default. Empat berikutnya menggunakan konvensi panggilan unmanaged
. Masing-masing menentukan salah satu konvensi panggilan ECMA 335: Cdecl
, Stdcall
, Fastcall
, atau Thiscall
. Deklarasi terakhir menggunakan konvensi panggilan unmanaged
, menginstruksikan CLR untuk memilih konvensi panggilan default untuk platform. CLR memilih konvensi pemanggilan pada waktu runtime.
public static unsafe T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
combinator(left, right);
public static unsafe T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
combinator(left, right);
Anda dapat mempelajari selengkapnya tentang penunjuk fungsi di spesifikasi fitur penunjuk Fungsi .
Spesifikasi bahasa C#
Untuk informasi selengkapnya, lihat bab kode Tidak Aman dari spesifikasi bahasa C#.