Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Sebagian besar kode C# yang Anda tulis adalah kode aman yang dapat diverifikasi. Kode aman berarti bahwa alat .NET dapat memverifikasi bahwa kode aman. Secara umum, kode aman tidak secara langsung mengakses memori dengan menggunakan pointer. Ini juga tidak mengalokasikan memori mentah. Ini justru membuat objek terkelola.
Referensi bahasa C# mendanai versi bahasa C# yang terbaru dirilis. Ini juga berisi dokumentasi awal untuk fitur dalam pratinjau publik untuk rilis bahasa yang akan datang.
Dokumentasi mengidentifikasi fitur apa pun yang pertama kali diperkenalkan dalam tiga versi terakhir bahasa atau dalam pratinjau publik saat ini.
Tip
Untuk menemukan kapan fitur pertama kali diperkenalkan di C#, lihat artikel tentang riwayat versi bahasa C#.
C# juga mendukung unsafe konteks, di mana Anda dapat menulis kode yang tidak dapat diverifikasi . Kode tidak aman tidak selalu berbahaya; itu hanya kode yang keamanannya tidak dapat diverifikasi oleh alat .NET. Anda menggunakan kode tidak aman untuk memanggil fungsi asli yang memerlukan penunjuk, dan dalam beberapa kasus untuk meningkatkan performa melalui akses memori langsung yang menghindari pemeriksaan terikat array. Kode tidak aman juga memperkenalkan risiko keamanan dan stabilitas. Untuk mengkompilasi kode yang berisi unsafe konteks, tambahkan opsi pengkompilasi AllowUnsafeBlocks .
C# mendefinisikan dua model untuk apa yang dihitung sebagai kode yang tidak aman: model asli dan model keamanan memori yang diperbarui yang ada dalam pratinjau di C# 15 dan .NET 11. Untuk informasi tentang perbedaan kedua model, lihat Dua model untuk kode yang tidak aman.
Untuk informasi tentang praktik terbaik untuk kode tidak aman di C#, lihat Praktik terbaik kode tidak aman.
Dua model untuk kode tidak aman
C# mendefinisikan dua model untuk kode yang tidak aman. Model yang berlaku menentukan operasi mana yang memerlukan unsafe konteks dan bagaimana unsafe pengubah pada anggota memengaruhi pemanggil.
-
Model asli yang tidak aman: Konteks
unsafemencakup keberadaan fitur pointer. Anda mendeklarasikan jenis penunjuk, mengambil alamat variabel, mendereferensikan penunjuk, mengonversistackallocekspresi ke penunjuk, atau menerapkansizeofke jenis arbitrer hanya di dalamunsafekonteks. (EkspresistackallocyangSpan<T>ditetapkan ke atauReadOnlySpan<T>diizinkan dalam kode aman.) Pengubahunsafepada jenis, anggota, atau blok menetapkan konteks tersebut tetapi tidak menempatkan kewajiban pada pemanggil. C# 1.0 memperkenalkan model ini, dan tetap menjadi default. -
Model keamanan memori yang diperbarui: Konteks
unsafemencakup operasi yang mengakses memori yang tidak dikelola runtime. Keberadaan pointer tidak aman; dereferensi pointer adalah. Pengubahunsafepada anggota menjadi kontrak yang menyebarluaskan kewajiban untuk mengaudit keamanan kepada pemanggil. Model ini dalam pratinjau di C# 15 dan .NET 11.
Tabel berikut membandingkan operasi mana yang unsafe memerlukan konteks di setiap model.
| Pengoperasian | Model asli | Model yang diperbarui |
|---|---|---|
Mendeklarasikan jenis pointer atau mengambil alamat dengan & |
Memerlukan unsafe |
Diizinkan dalam kode aman |
Pernyataan fixed |
Memerlukan unsafe |
Diizinkan dalam kode aman |
stackalloc Mengonversi ekspresi menjadi penunjuk |
Memerlukan unsafe |
Diizinkan dalam kode aman |
Operator sizeof pada jenis yang tidak dikelola |
Memerlukan unsafe |
Diizinkan dalam kode aman |
Tidak langsung penunjuk (*p), akses anggota (p->m), atau akses elemen (p[i]) |
Memerlukan unsafe |
Memerlukan unsafe |
| Pemanggilan penunjuk fungsi | Memerlukan unsafe |
Memerlukan unsafe |
| Akses elemen pada buffer ukuran tetap | Memerlukan unsafe |
Memerlukan unsafe |
Memanggil anggota yang ditandai unsafe |
Tidak ada persyaratan penelepon | Memerlukan unsafe |
Untuk mencoba model yang diperbarui, gunakan .NET 11 SDK (dalam pratinjau) dan atur LangVersion opsi pengkompilasi ke preview. Relaksasi pointer berlaku setiap kali Anda mengkompilasi dengan pengkompilasi C# 15 dan preview versi bahasa. Penegakan penuh, termasuk kewajiban pemanggil dan keikutsertaan perakitan, masih dalam pengembangan. Untuk informasi selengkapnya, lihat Model keamanan memori yang diperbarui (pratinjau).
Model asli yang tidak aman
Dalam model asli, unsafe kata kunci menetapkan konteks yang tidak aman pada jenis, anggota, atau blok, dan konteks tersebut membuka kunci fitur pointer yang dijelaskan di bagian berikut. Pengubah unsafe hanya mengubah apa yang dapat dilakukan kode yang ditandai; itu tidak menempatkan persyaratan pada pemanggil. Untuk mengkompilasi salah satu contoh ini, atur opsi pengkompilasi AllowUnsafeBlocks .
Jenis penunjuk
Dalam konteks tidak aman, jenis bisa menjadi jenis penunjuk, selain jenis nilai atau jenis referensi. Deklarasi jenis pointer mengambil salah satu formulir berikut:
type* identifier;
void* identifier; //allowed but not recommended
Jenis yang * Anda tentukan sebelum dalam jenis penunjuk adalah 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.
Saat Anda mendeklarasikan beberapa pointer dalam deklarasi yang sama, tulis tanda bintang (*) bersama dengan jenis yang mendasar 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 diambil oleh ekspresi lambda atau delegasi anonim), Anda harus menyematkan objek selama penunjuk digunakan.
Nilai variabel pointer jenis MyType* adalah alamat variabel jenis MyType. Berikut ini adalah contoh deklarasi jenis pointer:
-
int* p:padalah penunjuk ke bilangan bulat. -
int** p:padalah pointer ke pointer ke bilangan bulat. -
int*[] p:padalah array satu dimensi yang terdiri dari penunjuk ke bilangan bulat. -
char* p:padalah pointer ke karakter. -
void* p:padalah penunjuk ke jenis yang tidak diketahui.
Anda dapat menggunakan operator * tidak langsung penunjuk untuk mengakses konten di lokasi yang diarahkan oleh variabel penunjuk. 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. Anda dapat secara eksplisit mengonversi jenis penunjuk apa pun ke jenis penunjuk lain menggunakan ekspresi transmisi. 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: {number}");
/* Output:
The 4 bytes of the integer: 00 04 00 00
The value of the integer: 1024
*/
}
Buffer ukuran tetap
Array adalah jenis referensi, jadi dalam kode aman, bidang struct yang merupakan array hanya menyimpan referensi ke elemen array, bukan elemen itu sendiri. Ukuran berikut struct ini tidak bergantung pada jumlah elemen dalam array, karena pathName merupakan referensi:
public struct PathArray
{
public char[] pathName;
private int reserved;
}
Untuk menyimpan konten array di dalam struktur itu sendiri, gunakan fixed kata kunci untuk mendeklarasikan buffer ukuran tetap. Kata fixed kunci memerlukan unsafe konteks. Buffer ukuran tetap berguna ketika Anda menulis metode yang berinteroperasi dengan sumber data dari bahasa atau platform lain. Buffer ukuran tetap dapat mengambil atribut atau pengubah apa pun yang diizinkan untuk anggota struct reguler. Satu-satunya batasan adalah bahwa jenis array harus bool, , byte, char, short, int, longsbyte, , ushort, uint, ulong, float, , atau double:
private fixed char name[30];
Dalam contoh berikut, array fixedBuffer memiliki ukuran tetap. Anda menggunakan fixed pernyataan untuk mendapatkan penunjuk ke elemen pertama, lalu mengakses elemen array melalui penunjuk tersebut. Pernyataan menyematkan fixedfixedBuffer bidang instans 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 dengan 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:
- Anda hanya dapat menggunakannya dalam
unsafekonteks. - Mereka hanya dapat menjadi bidang instans struktur.
- Mereka selalu vektor, atau array satu dimensi.
- Deklarasi harus mencakup panjangnya, seperti
fixed char id[8]. Anda tidak dapat menggunakanfixed char id[].
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 dengan menggunakan delegate* sintaks. Pengkompilasi memanggil fungsi dengan menggunakan calli instruksi daripada membuat delegate instans objek 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 cara Anda mendeklarasikan fungsi lokal statis dan memanggil metode dengan menggunakan penunjuk ke fungsi lokal tersebut UnsafeCombine :
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:
- Anda hanya dapat mendeklarasikan penunjuk fungsi dalam
unsafekonteks. - Anda hanya dapat memanggil metode yang mengambil
delegate*(atau mengembalikandelegate*) dalamunsafekonteks. - 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 dengan 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 bagian Penunjuk fungsi dari spesifikasi bahasa C#.
Contoh: Gunakan pointer untuk menyalin array byte
Contoh berikut menggunakan pointer untuk menyalin byte dari satu array ke array lainnya.
Contoh ini menggunakan unsafe kata kunci, yang memungkinkan Anda menggunakan penunjuk dalam Copy metode . Pernyataan mendeklarasikan fixed penunjuk ke array sumber dan tujuan. Pernyataan fixedmenyematkan lokasi array sumber dan array tujuan dalam memori sehingga proses pengumpulan sampah tidak memindahkan array. Blok fixed menyematkan blok memori untuk array dalam cakupan blok.
Copy Karena metode dalam contoh ini menggunakan unsafe kata kunci, Anda harus mengkompilasinya dengan menggunakan opsi pengkompilasi AllowUnsafeBlocks.
Contoh ini mengakses elemen kedua array dengan menggunakan indeks daripada pointer kedua yang tidak dikelola. 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
*/
}
Model keamanan memori yang diperbarui (pratinjau)
Important
Model keamanan memori yang diperbarui adalah fitur pratinjau di C# 15 dan .NET 11. Ini terus berkembang berdasarkan umpan balik selama rilis pratinjau. Untuk mencoba model, gunakan SDK .NET 11 (pratinjau) dan atur LangVersion opsi pengkompilasi ke preview. Pengkompilasi di .NET 11 Pratinjau 5 mengimplementasikan relaksasi pointer tetapi belum memberlakukan kewajiban pemanggil, keikutsertaan perakitan, atau safe kata kunci. Untuk desain lengkapnya, lihat spesifikasi fitur keamanan memori.
Model yang diperbarui memisahkan dua hal yang diperlakukan model asli sebagai satu: keberadaan kode penunjuk dan penyebaran kewajiban keamanan kepada penelepon. Menandai anggota unsafe tidak lagi hanya mengizinkan penunjuk dalam tubuhnya; itu membuat penelepon anggota tidak aman, sehingga setiap penelepon harus menyebarluaskan kewajiban itu atau melepasnya di belakang batas yang divalidasi dan dapat dipanggil dengan aman. Untuk mendukung pemisahan itu, model juga mempersempit konteks yang tidak aman: keberadaan pointer tidak aman, hanya operasi yang mengakses memori yang tidak dikelola runtime. Penyempitan memungkinkan Anda menahan, meneruskan, dan mengembalikan pointer dalam kode aman, sekaligus unsafe menandai operasi dan anggota yang benar-benar dapat melanggar keamanan memori.
Anggota penelepon tidak aman
Dalam model asli, pengubah unsafe pada anggota hanya mengizinkan penunjuk dalam tanda tangan dan isi anggota. Ini tidak memberi tahu penelepon tentang keselamatan. Model yang diperbarui memberikan arti pengubah untuk penelepon. Ketika Anda menandai anggota unsafe, pengkompilasi memperlakukannya sebagai penelepon-tidak aman (juga disebut memerlukan-tidak aman): setiap penelepon harus memanggilnya dari unsafe konteks, dan kewajiban untuk mengaudit keselamatan berpindah ke penelepon tersebut.
Pengubah unsafe pada tanda tangan anggota tidak lagi menetapkan konteks yang tidak aman untuk isi. Dua peran tersebut dibagi:
- Pengubah
unsafepada tanda tangan menyebarkan kewajiban kepada penelepon. - Blok dalam
unsafemencakup operasi yang mengakses memori yang tidak dikelola.
Dalam mockup pratinjau berikut, ReadInt32 adalah penelepon tidak aman. Tanda tangan membawa pengubah unsafe , dan blok dalam unsafe membungkus dereferensi:
// Preview: illustrates the updated model, which the current compiler doesn't fully enforce yet.
public static unsafe int ReadInt32(byte* source)
{
unsafe
{
return *(int*)source;
}
}
Penelepon membungkus panggilan di bloknya sendiri unsafe :
// Preview
unsafe
{
int value = ReadInt32(buffer);
}
Model yang diperbarui juga mengencangkan beberapa aturan terkait:
- Pengubah
unsafemenghasilkan kesalahan pada deklarasi jenis, konstruktor statis, dan finalizer, karena pengubah tidak memiliki pemanggil untuk memberi tahu. - Delegasi tidak boleh
unsafe, karena delegasi berbentuk tipe. - Jenis yang konstruktor tanpa parameternya
unsafetidak memenuhinew()batasan.
Operasi yang memerlukan konteks yang tidak aman
Operasi yang mengakses memori yang ditujukan ke memerlukan unsafe konteks:
- Tidak langsung penunjuk (
*p), akses anggota penunjuk (p->member), dan akses elemen penunjuk (p[i]). - Pemanggilan penunjuk fungsi.
- Akses elemen pada buffer ukuran tetap.
Contoh berikut menyematkan array tanpa unsafe konteks tetapi mendereferensikan penunjuk di dalamnya:
public static int ReadValue(int[] numbers)
{
fixed (int* first = numbers)
{
// Dereferencing a pointer accesses unmanaged memory, so it still
// requires an unsafe context.
unsafe
{
return *first;
}
}
}
Operasi yang dilonggarkan
Operasi yang tidak mengakses memori yang ditujukan ke tidak lagi memerlukan unsafe konteks:
- Mendeklarasikan jenis penunjuk dan mengambil alamat variabel dengan
&operator. - Pernyataan
fixedyang menyematkan variabel. - Mengonversi
stackallocekspresi menjadi penunjuk. - Operator
sizeofditerapkan ke jenis yang tidak dikelola.
Contoh berikut membuat dan menyematkan pointer tanpa unsafe konteks:
public static void CreatePointer()
{
int value = 42;
// Creating a pointer doesn't require an unsafe context.
int* pointer = &value;
int** pointerToPointer = &pointer;
}
public static void PinArray(int[] numbers)
{
// The fixed statement no longer requires an unsafe context.
fixed (int* first = numbers)
{
int* current = first;
}
}
Relaksasi ini berlaku setiap kali Anda mengkompilasi dengan preview versi bahasa, apakah perakitan ikut serta dalam aturan keamanan memori yang diperbarui atau tidak.
Melepas kewajiban penelepon-tidak aman
Anggota yang memanggil operasi penelepon tidak aman memiliki dua pilihan: menyebarluaskan kewajiban, atau melepasnya.
-
Sebarkan: Tandai anggota
unsafeAnda sendiri . Kewajiban diteruskan kepada penelepon Anda. Gunakan penyebaran ketika Anda tidak dapat sepenuhnya memvalidasi kewajiban sendiri. -
Pelepasan: Biarkan tanda tangan anggota Anda aman. Validasi kewajiban di dalam anggota, biasanya dengan penjaga runtime, lalu lakukan operasi yang tidak aman di blok dalam
unsafe. Anggota yang berisi blok dalamunsafetetapi tidak menandai tanda tangannyaunsafesendiri adalah batas yang tidak aman: itu mengubah kode yang tidak aman menjadi permukaan yang dapat dipanggil dengan aman.
Mockup pratinjau berikut memvalidasi inputnya dengan penjaga, menyematkan array terkelola, dan membaca penunjuk. Penelepon tidak memerlukan unsafe konteks, karena metode melenyapkan kewajiban:
// Preview
public static int SumBytes(byte[] source)
{
ArgumentNullException.ThrowIfNull(source);
fixed (byte* first = source)
{
unsafe
{
// SAFETY: the null check and source.Length bound every read to the pinned array.
int total = 0;
for (int i = 0; i < source.Length; i++)
{
total += first[i];
}
return total;
}
}
}
Pemeriksaan null dan panjang array mengesampingkan input yang akan membiarkan baca berjalan melewati buffer, sehingga dereferensi di dalam unsafe blok adalah suara. Metode ini tidak meninggalkan kewajiban residual, sehingga mengekspos tanda tangan yang dapat dipanggil dengan aman.
Dokumentasi keamanan
Anggota penelepon-tidak aman harus mendokumen apa yang harus dijamin oleh pemanggil. Model yang diperbarui mendorong dua gaya komentar pelengkap:
-
/// <safety>Blok dokumentasi di atas tanda tangan menyatakan kontrak formal: kondisi yang harus dipenuhi pemanggil. Penganalisis dapat menandai anggota penelepon yang tidak aman yang tidak memilikinya. - Komentar
// SAFETY:di dalamunsafeblok mencatat mengapa operasi terdengar di tempat itu, untuk pengembang dan auditor yang membaca isinya.
Mockup pratinjau berikut menunjukkan kedua gaya pada metode penelepon-tidak aman ReadByte :
// Preview
/// <summary>Reads a single byte from unmanaged memory.</summary>
/// <safety>
/// The sum of <paramref name="ptr"/> and <paramref name="offset"/> must address a byte
/// the caller is permitted to read.
/// </safety>
public static unsafe byte ReadByte(IntPtr ptr, int offset)
{
byte* address = (byte*)ptr;
unsafe
{
// SAFETY: relies on the caller obligation stated in the <safety> block.
return address[offset];
}
}
Blok memberi /// <safety> tahu Anda kontrak. Kontrak berada dalam dokumentasi di mana setiap pemanggil dan peninjau melihatnya.
Bidang tidak aman
Gunakan pengubah unsafe untuk bidang saat jenis yang dinyatakan tidak mengekspresikan kontrak yang dipertahankan oleh jenis penutup dan kode lainnya bergantung pada. Ketidaknyamanan ada dalam kesenjangan antara apa yang dilihat sistem jenis dan apa yang dijanjikan jenis. Pengubah memaksa setiap penulisan ke bidang ke dalam unsafe blok, yang membuat tulisan dapat ditinjau di satu tempat.
Kasus terjernih adalah bidang yang menyimpan penunjuk asli. Penunjuk tidak menyatakan berapa banyak byte yang ditanganinya sebagaimana dilakukan System.Span<T> , sehingga jenis yang berisi mempertahankan informasi itu sendiri:
// Preview
public class NativeBuffer
{
/// <safety>
/// Null, or points to a buffer of Length bytes.
/// </safety>
private unsafe byte* _pointer;
public int Length { get; }
public byte ReadAt(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Length);
unsafe
{
// SAFETY: the bounds checks confine the read to the buffer that _pointer addresses.
return _pointer[index];
}
}
}
Bidang readonly unsafe memasangkan kontrak dengan penjaga bawaan: unsafe menamai invarian, dan readonly mencegah penulisan yang dapat merusaknya setelah konstruksi. Menandai properti atau peristiwa unsafe tidak membuat pemanggil bidang cadangannya tidak aman. Dalam struktur dengan [StructLayout(LayoutKind.Explicit)], Anda menandai setiap bidang baik safe atau unsafe.
Kata kunci aman
Model yang safe diperbarui menambahkan kata kunci kontekstual yang membuktikan deklarasi adalah suara di mana pengkompilasi mengharuskan Anda untuk membuat pilihan menjadi eksplisit.
Anggota extern memanggil kode asli, sehingga pengkompilasi tidak dapat mengklasifikasikan keamanannya. Di bawah model yang diperbarui, Anda menandai setiap extern deklarasi, termasuk LibraryImport metode parsial, baik safe atau unsafe:
// Preview
[LibraryImport("libc")]
internal static safe partial int getpid();
[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)]
internal static unsafe partial nint strlen(byte* str);
getpid tidak mengambil parameter dan mengembalikan primitif, sehingga penulis membuktikan bahwa panggilan aman dan penelepon menggunakannya tanpa upacara.
strlen mengambil pointer mentah yang dereferensi kode asli, sehingga deklarasinya adalah unsafe dan menyebarluaskan kewajiban kepada penelepon. Menghilangkan kedua pengubah adalah kesalahan, yang memaksa Anda untuk membuat keputusan keamanan. Bidang dalam struktur dengan tata letak eksplisit menggunakan aturan yang sama.
Perilaku opt-in dan cross-assembly
Model yang diperbarui memiliki dua sakelar tingkat proyek independen:
- Properti keikutsertaan baru mengaktifkan aturan yang diperbarui. Saat properti nonaktif, aturan asli berlaku. Ketika aktif,
unsafepada anggota disebarkan ke penelepon, dan pengkompilasi merekam pilihan di rakitan dengan MemorySafetyRulesAttribute atribut . - Gerbang properti AllowUnsafeBlocks yang ada menampilkan setiap tampilan
unsafekata kunci, termasuk blok dalam di situs panggilan. Ini default kefalse, sehingga proyek pada default tidak dapat memanggil API yang tidak aman.
Dua properti menggabungkan sebagai berikut:
| Properti keikutsertaan | AllowUnsafeBlocks |
Result |
|---|---|---|
| Aktif | Nonaktif (default) | Konfigurasi paling aman. Proyek ini menggunakan model yang diperbarui dan tidak mengizinkan kode yang tidak aman. |
| Aktif | Aktif | Proyek ini menggunakan model yang diperbarui dan memungkinkan kode yang tidak aman. |
| Nonaktif | Nonaktif | Model asli berlaku, dan proyek tidak dapat menggunakan jenis penunjuk. |
| Nonaktif | Aktif | Model asli berlaku, dan proyek dapat menggunakan jenis penunjuk. |
Apakah satu assembly memberlakukan aturan yang diperbarui terhadap aturan lain tergantung pada sisi mana yang ikut serta:
-
Penelepon model yang diperbarui, penelepon model yang diperbarui: Penanda callee
unsafemelakukan perjalanan melalui metadata. Penelepon membungkus setiap panggilan ke anggota penelepon-tidak aman diunsafeblok. -
Pemanggil model yang diperbarui, penerima panggilan model asli: Mode kompatibilitas memperlakukan setiap anggota penerima panggilan dengan jenis penunjuk dalam tanda tangannya sebagai penelepon-tidak aman, sehingga situs panggilan memerlukan blok penutup
unsafe. Mode ini membuat API berbasis pointer tidak kehilangan persyaratannyaunsafesecara diam-diam. - Penelepon model asli, penelepon model yang diperbarui: Aturan penunjuk asli masih berlaku. Anggota penelepon-tidak aman yang tidak memiliki jenis penunjuk dalam tanda tangannya menjadi dapat dipanggil dari kode aman, karena pemanggil model asli tidak dapat membaca penanda baru.
Spesifikasi bahasa C#
Untuk informasi selengkapnya, lihat bab kode Tidak Aman dari spesifikasi bahasa C#.
Untuk desain model keamanan memori yang diperbarui, lihat spesifikasi fitur keamanan memori.