Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Większość pisanego kodu w języku C# jest weryfikowalnym bezpiecznym kodem. Bezpieczny kod oznacza, że .NET narzędzia mogą sprawdzić, czy kod jest bezpieczny. Ogólnie rzecz biorąc, bezpieczny kod nie uzyskuje bezpośredniego dostępu do pamięci przy użyciu wskaźników. Nie przydziela również surowej pamięci. Zamiast tego tworzy zarządzane obiekty.
Dokumentacja języka C# zawiera ostatnio wydaną wersję języka C#. Zawiera również początkową dokumentację funkcji w publicznej wersji zapoznawczej nadchodzącej wersji językowej.
Dokumentacja identyfikuje dowolną funkcję po raz pierwszy wprowadzoną w ostatnich trzech wersjach języka lub w bieżącej publicznej wersji zapoznawczej.
Wskazówka
Aby dowiedzieć się, kiedy funkcja została po raz pierwszy wprowadzona w języku C#, zapoznaj się z artykułem dotyczącym historii wersji języka C#.
Język C# obsługuje unsafe również kontekst, w którym można napisać nieweryfikowalny kod. Niebezpieczny kod nie musi być niebezpieczny; to tylko kod, którego bezpieczeństwo nie może być zweryfikowane przez narzędzia .NET. Niebezpieczny kod służy do wywoływania funkcji natywnych, które wymagają wskaźników, a w niektórych przypadkach w celu zwiększenia wydajności dzięki bezpośredniemu dostępowi do pamięci, który pozwala uniknąć sprawdzania granic tablicy. Niebezpieczny kod wprowadza również zagrożenia bezpieczeństwa i stabilności. Aby skompilować kod zawierający unsafe kontekst, dodaj opcję kompilatora AllowUnsafeBlocks .
Język C# definiuje dwa modele, które są liczone jako niebezpieczny kod: oryginalny model i zaktualizowany model bezpieczeństwa pamięci, który jest w wersji zapoznawczej w języku C# 15 i .NET 11. Aby uzyskać informacje na temat różnic między dwoma modelami, zobacz Dwa modele niebezpiecznego kodu.
Aby uzyskać informacje o najlepszych rozwiązaniach dotyczących niebezpiecznego kodu w języku C#, zobacz Niebezpieczne najlepsze rozwiązania dotyczące kodu.
Dwa modele niebezpiecznego kodu
Język C# definiuje dwa modele niebezpiecznego kodu. Model w efekcie określa, które operacje wymagają unsafe kontekstu i jak unsafe modyfikator elementu członkowskiego wpływa na wywołujących.
-
Oryginalny niebezpieczny model:
unsafekontekst obejmuje istnienie funkcji wskaźnika. Zadeklarujesz typ wskaźnika, bierzesz adres zmiennej, wyłuszasz wskaźnik, konwertujeszstackallocwyrażenie na wskaźnik lub stosujesz dosizeofdowolnego typu tylko wewnątrzunsafekontekstu. (Wyrażeniestackallocprzypisane do lubSpan<T>ReadOnlySpan<T>jest dozwolone w bezpiecznym kodzie). Modyfikatorunsafetypu, elementu członkowskiego lub bloku ustanawia ten kontekst, ale nie nakłada obowiązku dla rozmówców. Język C# 1.0 wprowadził ten model i pozostaje domyślny. -
Zaktualizowany model bezpieczeństwa pamięci: kontekst
unsafeobejmuje operacje, które uzyskują dostęp do pamięci, którymi nie zarządza środowisko uruchomieniowe. Istnienie wskaźnika nie jest niebezpieczne; wyłuszczenie wskaźnika jest. Modyfikatorunsafena elemencie członkowskim staje się umową, która propaguje obowiązek inspekcji bezpieczeństwa rozmówcy. Ten model jest w wersji zapoznawczej w języku C# 15 i .NET 11.
W poniższej tabeli porównaliśmy operacje wymagające unsafe kontekstu w każdym modelu.
| Operation | Oryginalny model | Zaktualizowany model |
|---|---|---|
Zadeklaruj typ wskaźnika lub zadeklaruj adres za pomocą polecenia & |
Wymaga unsafe |
Dozwolone w bezpiecznym kodzie |
Instrukcja fixed |
Wymaga unsafe |
Dozwolone w bezpiecznym kodzie |
Konwertowanie stackalloc wyrażenia na wskaźnik |
Wymaga unsafe |
Dozwolone w bezpiecznym kodzie |
Operator sizeof w dowolnym niezarządzanych typach |
Wymaga unsafe |
Dozwolone w bezpiecznym kodzie |
Pośredni wskaźnik (*p), dostęp do składowych (p->m) lub dostęp do elementów (p[i]) |
Wymaga unsafe |
Wymaga unsafe |
| Wywołanie wskaźnika funkcji | Wymaga unsafe |
Wymaga unsafe |
| Dostęp do elementu w buforze o stałym rozmiarze | Wymaga unsafe |
Wymaga unsafe |
Wywoływanie elementu członkowskiego oznaczonego unsafe |
Brak wymagania wywołującego | Wymaga unsafe |
Aby wypróbować zaktualizowany model, użyj zestawu SDK .NET 11 (w wersji zapoznawczej) i ustaw opcję kompilatora LangVersion na preview. Złagodzenie wskaźnika ma zastosowanie za każdym razem, gdy kompilujesz kompilator języka C# 15 i preview wersję języka. Pełne egzekwowanie przepisów, w tym obowiązki rozmówców i zgoda zgromadzenia, jest nadal w toku. Aby uzyskać więcej informacji, zobacz Zaktualizowany model bezpieczeństwa pamięci (wersja zapoznawcza).
Oryginalny niebezpieczny model
W oryginalnym modelu unsafe słowo kluczowe ustanawia niebezpieczny kontekst dla typu, elementu członkowskiego lub bloku, a kontekst odblokowuje funkcje wskaźnika opisane w poniższych sekcjach. Modyfikator unsafe zmienia tylko to, co może zrobić oznaczony kod; nie wymaga to od osób wywołujących. Aby skompilować dowolny z tych przykładów, ustaw opcję kompilatora AllowUnsafeBlocks .
Typy wskaźników
W niebezpiecznym kontekście typ może być typem wskaźnika, oprócz typu wartości lub typu odwołania. Deklaracja typu wskaźnika przyjmuje jedną z następujących form:
type* identifier;
void* identifier; //allowed but not recommended
Typ określony przed * typem wskaźnika jest typem odwołania.
Typy wskaźników nie dziedziczą z obiektu i nie istnieją konwersje między typami wskaźników i object. Ponadto boxing i unboxing nie obsługują wskaźników. Można jednak konwertować między różnymi typami wskaźników i między typami wskaźników i typami całkowitymi.
W przypadku deklarowania wielu wskaźników w tej samej deklaracji zapisz gwiazdkę (*) razem tylko z typem bazowym. Nie jest on używany jako prefiks do każdej nazwy wskaźnika. Na przykład:
int* p1, p2, p3; // Ok
int *p1, *p2, *p3; // Invalid in C#
Moduł odśmiecający śmieci nie śledzi, czy obiekt jest wskazywany przez jakiekolwiek typy wskaźników. Jeśli odwołanie jest obiektem w zarządzanym stercie (w tym zmiennych lokalnych przechwyconych przez wyrażenia lambda lub delegatów anonimowych), należy przypiąć obiekt tak długo, jak wskaźnik jest używany.
Wartość zmiennej wskaźnika typu MyType* jest adresem zmiennej typu MyType. Poniżej przedstawiono przykłady deklaracji typów wskaźników:
-
int* p:pjest wskaźnikiem do liczby całkowitej. -
int** p:pjest wskaźnikiem do wskaźnika liczby całkowitej. -
int*[] p:pjest jednowymiarową tablicą wskaźników do liczb całkowitych. -
char* p:pjest wskaźnikiem do char. -
void* p:pjest wskaźnikiem do nieznanego typu.
Możesz użyć operatora * pośredniego wskaźnika, aby uzyskać dostęp do zawartości w lokalizacji wskazanej przez zmienną wskaźnika. Rozważmy na przykład następującą deklarację:
int* myVariable;
Wyrażenie *myVariable oznacza zmienną int znalezioną pod adresem zawartym w myVariable.
Istnieje kilka przykładów wskaźników w artykułach dotyczących stwierdzenia fixed. W poniższym przykładzie użyto słowa kluczowego unsafe i wyrażenia fixed oraz pokazano, jak zwiększyć wskaźnik wewnętrzny. Możesz wkleić ten kod do funkcji Main aplikacji konsolowej, aby go uruchomić. Te przykłady należy skompilować przy użyciu zestawu opcji kompilatora 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
*/
Nie można zastosować operatora pośredniego do wskaźnika typu void*. Można jednak użyć rzutowania, aby przekonwertować wskaźnik typu void na dowolny inny typ wskaźnika i na odwrót.
Wskaźnik może być null. Zastosowanie operatora pośredniego do wskaźnika o wartości null powoduje zachowanie zdefiniowane przez implementację.
Przekazywanie wskaźników między metodami może powodować niezdefiniowane zachowanie. Rozważ metodę zwracającą wskaźnik do zmiennej lokalnej przez parametr in, outlub ref albo jako wynik funkcji. Jeśli wskaźnik został ustawiony w stałym bloku, zmienna, do której wskazuje, nie może być już stała.
W poniższej tabeli wymieniono operatory i instrukcje, które mogą działać na wskaźnikach w niebezpiecznym kontekście:
| Operator/instrukcja | Użyj |
|---|---|
* |
Wykonuje dereferencję wskaźnika. |
-> |
Uzyskuje dostęp do elementu członkowskiego struktury za pomocą wskaźnika. |
[] |
Indeksuje wskaźnik. |
& |
Uzyskuje adres zmiennej. |
++ i -- |
Wskaźniki zostają inkrementowane i dekrementowane. |
+ i - |
Wykonuje arytmetykę wskaźników. |
==, !=, <, >, <=i >= |
Porównuje wskaźniki. |
stackalloc |
Przydziela pamięć na stosie. |
fixed oświadczenie |
Tymczasowo naprawia zmienną, aby można było znaleźć jej adres. |
Aby uzyskać więcej informacji na temat operatorów wskaźnikowych, zobacz sekcję Operatory wskaźnikowe.
Dowolny typ wskaźnika można niejawnie przekonwertować na typ void*. Do każdego typu wskaźnika można przypisać wartość null. Można jawnie przekonwertować dowolny typ wskaźnika na dowolny inny typ wskaźnika przy użyciu wyrażenia rzutowania. Można również przekonwertować dowolny typ całkowity na typ wskaźnika lub dowolny typ wskaźnika na typ całkowity. Te konwersje wymagają jawnego rzutowania.
Poniższy przykład konwertuje int* na byte*. Zwróć uwagę, że wskaźnik wskazuje najniższy adresowany bajt zmiennej. Po kolejnych przyrostach wyniku do rozmiaru int (4 bajty) można wyświetlić pozostałe bajty zmiennej.
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
*/
}
bufory o stałym rozmiarze
Tablice są typami referencyjnymi, więc w bezpiecznym kodzie pole struktury, które jest tablicą, przechowuje tylko odwołanie do elementów tablicy, a nie samych elementów. Rozmiar poniższych struct elementów nie zależy od liczby elementów w tablicy, ponieważ pathName jest to odwołanie:
public struct PathArray
{
public char[] pathName;
private int reserved;
}
Aby przechowywać zawartość tablicy wewnątrz samej struktury, użyj fixed słowa kluczowego , aby zadeklarować bufor o stałym rozmiarze. Słowo fixed kluczowe wymaga unsafe kontekstu. Bufory o stałym rozmiarze są przydatne podczas pisania metod, które współdziałają ze źródłami danych z innych języków lub platform. Bufor o stałym rozmiarze może przyjmować dowolne atrybuty lub modyfikatory, które są dozwolone dla zwykłych składowych struktury. Jedynym ograniczeniem jest to, że typ tablicy musi być bool, , intshortlongsbytecharbyteuintulongushortfloatlub :double
private fixed char name[30];
W poniższym przykładzie tablica fixedBuffer ma stały rozmiar. Aby uzyskać wskaźnik do pierwszego elementu, należy użyć fixed instrukcji , a następnie uzyskać dostęp do elementów tablicy za pośrednictwem tego wskaźnika. Instrukcja fixed przypina fixedBuffer pole wystąpienia do określonej lokalizacji w pamięci:
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]);
}
}
Rozmiar tablicy 128 elementów char wynosi 256 bajtów. Bufory znaków o stałym rozmiarze zawsze zajmują 2 bajty na znak, niezależnie od kodowania. Ten rozmiar tablicy pozostaje taki sam, nawet gdy bufory znaków są przekazywane do metod interfejsu API lub struktur z CharSet = CharSet.Auto lub CharSet = CharSet.Ansi. Aby uzyskać więcej informacji, zobacz CharSet.
W poprzednim przykładzie pokazano uzyskiwanie dostępu do pól fixed bez przypinania. Inną typową tablicą o stałym rozmiarze jest tablica bool. Elementy w tablicy bool mają zawsze rozmiar 1 bajt.
bool tablice nie są odpowiednie do tworzenia tablic bitowych ani buforów.
Bufory o stałym rozmiarze są kompilowane za pomocą System.Runtime.CompilerServices.UnsafeValueTypeAttribute, który informuje środowisko uruchomieniowe języka wspólnego (CLR), że typ zawiera niezarządzaną tablicę, która może ulec przepełnieniu. Pamięć przydzielona przy użyciu obiektu stackalloc automatycznie włącza również funkcje wykrywania przepełnień buforu w środowisku CLR. W poprzednim przykładzie pokazano, jak w obiekcie może istnieć bufor o stałym rozmiarze unsafe struct.
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
Wygenerowany przez kompilator język C# dla Buffer jest przypisywany w następujący sposób:
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;
}
Bufory o stałym rozmiarze różnią się od zwykłych tablic w następujący sposób:
- Można ich używać tylko w
unsafekontekście. - Mogą to być tylko pola wystąpień struktur.
- Są to zawsze wektory lub tablice jednowymiarowe.
- Deklaracja musi zawierać długość, taką jak
fixed char id[8]. Nie można użyćfixed char id[].
Wskaźniki funkcji
Język C# udostępnia typy delegate do definiowania bezpiecznych obiektów wskaźnika funkcji. Wywoływanie delegata polega na utworzeniu instancji typu pochodzącego z System.Delegate i wykonaniu wywołania metody wirtualnej do jej metody Invoke. To wywołanie wirtualne używa instrukcji callvirt IL. W ścieżkach kodu krytycznego dla wydajności użycie instrukcji calli IL jest bardziej wydajne.
Wskaźnik funkcji można zdefiniować przy użyciu delegate* składni . Kompilator wywołuje funkcję przy użyciu calli instrukcji delegate zamiast tworzenia wystąpienia obiektu i wywoływania metody Invoke. Poniższy kod deklaruje dwie metody używające delegate lub delegate* do łączenia dwóch obiektów tego samego typu. Pierwsza metoda używa typu delegata System.Func<T1,T2,TResult>. Druga metoda używa deklaracji delegate* z tymi samymi parametrami i zwracanym typem:
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);
Poniższy kod pokazuje, jak zadeklarować statyczną funkcję lokalną i wywołać UnsafeCombine metodę przy użyciu wskaźnika do tej funkcji lokalnej:
int product = 0;
unsafe
{
static int localMultiply(int x, int y) => x * y;
product = UnsafeCombine(&localMultiply, 3, 4);
}
Poprzedni kod ilustruje kilka reguł dotyczących dostępu do funkcji za pomocą wskaźnika funkcji.
- Wskaźniki funkcji można zadeklarować tylko w
unsafekontekście. - Można wywoływać tylko metody, które przyjmują metodę
delegate*(lub zwracają wartośćdelegate*) wunsafekontekście. - Operator
&uzyskiwania adresu funkcji jest dozwolony tylko w funkcjachstatic. Ta reguła ma zastosowanie zarówno do funkcji składowych, jak i funkcji lokalnych.
Składnia zawiera podobieństwa do deklarowania typów delegate i używania wskaźników. Sufiks *delegate wskazuje, że deklaracja jest wskaźnikiem funkcji .
& podczas przypisywania grupy metod do wskaźnika funkcji wskazuje, że operacja przyjmuje adres metody.
Możesz określić konwencję wywoływania elementu delegate* za pomocą słów kluczowych managed i unmanaged. Ponadto w przypadku wskaźników funkcji unmanaged można określić konwencję wywoływania. W poniższych deklaracjach przedstawiono przykłady każdego z nich. Pierwsza deklaracja używa konwencji wywoływania managed, która jest domyślna. Następne cztery używają konwencji wywołań unmanaged. Każdy określa jedną z konwencji wywołujących ECMA 335: Cdecl, Stdcall, Fastcalllub Thiscall. Ostatnia deklaracja używa konwencji wywoływania unmanaged, poinstruując CLR, aby wybrać domyślną konwencję wywoływania dla platformy. CLR wybiera konwencję wywoływania podczas wykonywania.
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);
Więcej informacji na temat wskaźników funkcji można dowiedzieć się w sekcji Wskaźniki funkcji specyfikacji języka C#.
Przykład: kopiowanie tablicy bajtów za pomocą wskaźników
W poniższym przykładzie użyto wskaźników do skopiowania bajtów z jednej tablicy do innej.
W tym przykładzie użyto słowa kluczowego unsafe , które umożliwia używanie wskaźników w metodzie Copy . Instrukcja fixed deklaruje wskaźniki do tablic źródłowych i docelowych. Instrukcja fixedwyprowadza lokalizację tablic źródłowych i docelowych w pamięci, aby odzyskiwanie pamięci nie przenosiło tablic. Blok fixed przypina bloki pamięci dla tablic w zakresie bloku. Ponieważ metoda w tym przykładzie Copy używa słowa kluczowego unsafe , należy ją skompilować przy użyciu opcji kompilatora AllowUnsafeBlocks .
W tym przykładzie uzyskuje dostęp do elementów obu tablic przy użyciu indeksów, a nie drugiego niezarządzanego wskaźnika. Deklaracja wskaźników pSource i pTarget przypina tablice.
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
*/
}
Zaktualizowany model bezpieczeństwa pamięci (wersja zapoznawcza)
Ważna
Zaktualizowany model bezpieczeństwa pamięci to funkcja w wersji zapoznawczej w języku C# 15 i .NET 11. Nadal ewoluuje na podstawie opinii w wersjach zapoznawczych. Aby wypróbować model, użyj zestawu SDK .NET 11 (wersja zapoznawcza) i ustaw opcję kompilatora LangVersion na preview. Kompilator w wersji .NET 11 (wersja zapoznawcza 5) implementuje złagodzenie wskaźnika, ale nie wymusza jeszcze zobowiązań wywołujących, zgody zestawu ani słowa kluczowegosafe. Aby uzyskać pełny projekt, zobacz specyfikację funkcji bezpieczeństwa pamięci.
Zaktualizowany model oddziela dwie elementy, które oryginalny model traktuje jako jeden: istnienie kodu wskaźnika i propagację zobowiązań bezpieczeństwa dla rozmówców. Oznaczanie elementu członkowskiego unsafe nie zezwala już tylko na wskaźniki w jego treści; sprawia, że element wywołujący element członkowski jest niebezpieczny, więc każdy obiekt wywołujący musi albo propagować ten obowiązek lub zwalniać go za zweryfikowaną, bezpieczną granicą. Aby zapewnić obsługę tego rozdzielenia, model zawęża również niebezpieczny kontekst: istnienie wskaźnika nie jest niebezpieczne, tylko operacje, które uzyskują dostęp do pamięci, którymi nie zarządza środowisko uruchomieniowe. Zawężenie umożliwia utrzymywanie, przekazywanie i zwracanie wskaźników w bezpiecznym kodzie, a jednocześnie unsafe oznacza operacje i elementy członkowskie, które mogą rzeczywiście naruszać bezpieczeństwo pamięci.
Element wywołujący niebezpieczne elementy członkowskie
W oryginalnym modelu unsafe modyfikator na elemencie członkowskim zezwala tylko na wskaźniki w podpisie i treści elementu członkowskiego. Nie informuje rozmówców o bezpieczeństwie. Zaktualizowany model daje modyfikatorowi znaczenie dla wywołujących. Gdy oznaczysz element członkowski unsafe, kompilator traktuje go jako niebezpieczny obiekt wywołujący (nazywany również wymaga-niebezpieczny): każdy obiekt wywołujący musi wywołać go z kontekstu, a obowiązek inspekcji bezpieczeństwa przenosi się do tego obiektu wywołującego unsafe .
Modyfikator unsafe podpisu elementu członkowskiego nie ustanawia już niebezpiecznego kontekstu dla treści. Dwie role są podzielone:
- Modyfikator
unsafepodpisu propaguje obowiązek rozmówców. - Wewnętrzny
unsafeblok określa zakresy operacji, które uzyskują dostęp do niezarządzanej pamięci.
W poniższej wersji zapoznawczej makiety ReadInt32 jest wywołanie niebezpieczne. Podpis przenosi unsafe modyfikator, a wewnętrzny unsafe blok owija wyłuszczenie:
// 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;
}
}
Obiekt wywołujący opakowuje wywołanie we własnym unsafe bloku:
// Preview
unsafe
{
int value = ReadInt32(buffer);
}
Zaktualizowany model zaostrza również kilka powiązanych reguł:
- Modyfikator
unsafegeneruje błąd deklaracji typu, konstruktora statycznego i finalizatora, ponieważ modyfikator nie ma wywołującego informacji. - Delegaci nie mogą być
unsafeelementami , ponieważ delegat ma kształt typu. - Typ, którego konstruktor bez parametrów
unsafenie spełnianew()wymagań ograniczenia.
Operacje wymagające niebezpiecznego kontekstu
Operacje, które uzyskują dostęp do pamięci wskazującej na pamięć, wymagają unsafe kontekstu:
- Pośredni wskaźnik (
*p), dostęp do składowych wskaźnika (p->member) i dostęp do elementu wskaźnika (p[i]). - Wywołanie wskaźnika funkcji.
- Dostęp do elementu w buforze o stałym rozmiarze.
W poniższym przykładzie tablica jest przypięta bez kontekstu, unsafe ale wyłuska wskaźnik wewnątrz jednej:
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;
}
}
}
Zrelaksowane operacje
Operacje, które nie mają dostępu do pamięci wskazywanej, nie wymagają unsafe już kontekstu:
- Deklarowanie typu wskaźnika i pobieranie adresu zmiennej z operatorem
&. - Instrukcja
fixed, która przypina zmienną. - Konwertowanie
stackallocwyrażenia na wskaźnik. - Operator
sizeofzastosowany do dowolnego typu niezarządzanego.
Poniższy przykład tworzy i przypina wskaźniki bez unsafe kontekstu:
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;
}
}
Te złagodzenie mają zastosowanie za każdym razem, gdy kompilujesz preview wersję języka, bez względu na to, czy zestaw zdecyduje się na zaktualizowane zasady bezpieczeństwa pamięci.
Zwalnianie rozmówców niebezpiecznych zobowiązań
Element członkowski wywołujący niebezpieczną operację ma dwie opcje: propagowanie obowiązku lub jego zwolnienie.
-
Propagacja: oznacz własny element członkowski
unsafe. Obowiązek przechodzi do rozmówców. Użyj propagacji, gdy nie możesz w pełni zweryfikować zobowiązania samodzielnie. -
Zwolnienie: pozostaw bezpieczny podpis członka. Zweryfikuj zobowiązanie wewnątrz elementu członkowskiego, zwykle z osłonami środowiska uruchomieniowego, a następnie wykonaj niebezpieczną operację w bloku wewnętrznym
unsafe. Element członkowski, który zawiera blok wewnętrznyunsafe, ale nie oznacza własnego podpisuunsafe, jest niebezpieczną granicą: zamienia niebezpieczny kod w bezpieczną powierzchnię.
Poniższy podgląd makiety weryfikuje dane wejściowe za pomocą funkcji guard, przypina zarządzaną tablicę i odczytuje wskaźnik. Osoby wywołujące nie potrzebują unsafe kontekstu, ponieważ metoda zwalnia obowiązek:
// 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;
}
}
}
Sprawdzanie wartości null i długość tablicy wykluczają dane wejściowe, które pozwoliłyby na uruchomienie odczytu poza buforem, więc wyłuszczania wewnątrz unsafe bloku jest dźwięk. Metoda nie pozostawia pozostałego obowiązku, więc uwidacznia podpis z możliwością bezpiecznego wywoływania.
Dokumentacja dotycząca bezpieczeństwa
Element członkowski wywołujący-niebezpieczny powinien udokumentować to, co musi zagwarantować obiekt wywołujący. Zaktualizowany model zachęca do dwóch uzupełniających stylów komentarzy:
-
/// <safety>Blok dokumentacji powyżej podpisu określa formalny kontrakt: warunki, które musi spełnić obiekt wywołujący. Analizator może oznaczyć element członkowski wywołujący niebezpieczny, którego brakuje. -
// SAFETY:Komentarz wewnątrzunsafebloku rejestruje, dlaczego operacja jest brzmieć na tym miejscu, dla deweloperów i audytorów, którzy czytają treść.
Poniższy podgląd makiety pokazuje oba style w metodzie niebezpiecznej ReadByte wywołującej:
// 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 /// <safety> informuje o kontrakcie. Umowa należy do dokumentacji, w której każdy obiekt wywołujący i recenzent widzi go.
Niebezpieczne pola
unsafe Użyj modyfikatora dla pola, gdy zadeklarowany typ nie wyraża kontraktów, od których utrzymuje się otaczający typ, a inny kod zależy. Niebezpieczność istnieje w różnicy między tym, co widzi system typów i jakie obiecuje typ. Modyfikator wymusza każdy zapis w polu do unsafe bloku, który zachowuje możliwość przeglądania zapisów w jednym miejscu.
Najczystszym przypadkiem jest pole, które zawiera natywny wskaźnik. Wskaźnik nie deklaruje, ile bajtów adresuje w taki sposób System.Span<T> , więc typ zawierający przechowuje te informacje:
// 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];
}
}
}
readonly unsafe Pole paruje kontrakt z wbudowaną osłoną: unsafe nazywa niezmienną readonly i uniemożliwia zapis, który może przerwać go po budowie. Oznaczanie właściwości lub zdarzenia unsafe nie powoduje, że obiekt wywołujący pole pomocnicze nie jest niebezpieczny. W strukturę z elementem [StructLayout(LayoutKind.Explicit)]oznaczasz każde pole albo safe .unsafe
Bezpieczne słowo kluczowe
Zaktualizowany model dodaje kontekstowe safe słowo kluczowe, które potwierdza deklarację, gdzie kompilator wymaga jawnego wyboru.
Element extern członkowski wywołuje kod natywny, więc kompilator nie może sklasyfikować jego bezpieczeństwa. W ramach zaktualizowanego modelu oznaczasz każdą extern deklarację, w tym metodę częściową LibraryImport lub safeunsafe:
// Preview
[LibraryImport("libc")]
internal static safe partial int getpid();
[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)]
internal static unsafe partial nint strlen(byte* str);
getpid nie przyjmuje parametrów i zwraca element pierwotny, więc autor potwierdza, że wywołanie jest bezpieczne i osoby wywołujące używają go bez ceremonii.
strlen przyjmuje nieprzetworzone wskaźniki, że kod natywny wyłuska, więc deklaracja jest unsafe i propaguje obowiązek wywołań. Pominięcie obu modyfikatorów jest błędem, który wymusza podjęcie decyzji o bezpieczeństwie. Pole w strukturze z jawnym układem używa tej samej reguły.
Zachowanie opt-in i cross-assembly
Zaktualizowany model ma dwa niezależne przełączniki na poziomie projektu:
- Nowa właściwość zgody włącza zaktualizowane reguły. Gdy właściwość jest wyłączona, mają zastosowanie oryginalne reguły. Gdy element członkowski jest włączony,
unsafepropaguje element wywołujący, a kompilator rejestruje wybór w zestawie za pomocą atrybutu MemorySafetyRulesAttribute . - Istniejąca właściwość AllowUnsafeBlocks bramuje każdy wygląd słowa kluczowego
unsafe, w tym bloki wewnętrzne w lokacjach wywołań. Domyślnie jestfalseto wartość , więc projekt w domyślnej wersji nie może wywoływać żadnego niebezpiecznego interfejsu API.
Dwie właściwości łączą się w następujący sposób:
| Właściwość zgody | AllowUnsafeBlocks |
Result |
|---|---|---|
| Włączone | Wyłączone (ustawienie domyślne) | Najbezpieczniejsza konfiguracja. Projekt używa zaktualizowanego modelu i nie zezwala na niebezpieczny kod. |
| Włączone | Włączone | Projekt używa zaktualizowanego modelu i zezwala na niebezpieczny kod. |
| Off | Off | Oryginalny model ma zastosowanie, a projekt nie może używać typów wskaźników. |
| Off | Włączone | Ma zastosowanie oryginalny model, a projekt może używać typów wskaźników. |
To, czy jeden zestaw wymusza zaktualizowane reguły względem innego, zależy od tego, która strona wyrazi zgodę:
-
Obiekt wywołujący zaktualizowany model, obiekt wywoływany przez zaktualizowany model: znaczniki wywoływanego
unsafeprzechodzą przez metadane. Obiekt wywołujący opakowuje każde wywołanie elementu wywołującego niebezpiecznegounsafeelementu członkowskiego w bloku. -
Obiekt wywołujący zaktualizowany model, wywoływany w oryginalnym modelu: tryb zgodności traktuje dowolny element członkowski wywoływany z typem wskaźnika w podpisie jako obiekt wywołujący niebezpieczny, więc lokacja wywołania wymaga otaczającego
unsafebloku. Ten tryb utrzymuje interfejs API oparty na wskaźniku z dyskretnego utraty wymagańunsafe. - Obiekt wywołujący oryginalny model, wywoływany przez zaktualizowany model: oryginalne reguły wskaźnika nadal mają zastosowanie. Element członkowski wywołujący niebezpieczny, który nie ma typu wskaźnika w podpisie, staje się wywoływany z bezpiecznego kodu, ponieważ obiekt wywołujący oryginalny model nie może odczytać nowych znaczników.
Specyfikacja języka C#
Aby uzyskać więcej informacji, zobacz Niebezpieczny kod rozdział specyfikacji języka C# .
Aby zapoznać się z projektem zaktualizowanego modelu bezpieczeństwa pamięci, zobacz specyfikację funkcji bezpieczeństwa pamięci.