안전하지 않은 코드, 포인터 형식 및 함수 포인터

작성하는 대부분의 C# 코드는 확인 가능한 안전한 코드입니다. 사용할 수 없는 안전한 코드는 .NET 도구가 코드가 안전한지 확인할 수 있음을 의미합니다. 일반적으로 안전한 코드는 포인터를 사용하여 메모리에 직접 액세스하지 않습니다. 또한 원시 메모리를 할당하지 않습니다. 대신 관리되는 개체를 만듭니다.

C# 언어 참조는 가장 최근에 릴리스된 C# 언어 버전을 문서화합니다. 또한 예정된 언어 릴리스의 공개 미리 보기 기능에 대한 초기 설명서도 포함되어 있습니다.

설명서는 언어의 마지막 세 버전 또는 현재 공개 미리 보기에서 처음 도입된 기능을 식별합니다.

팁 (조언)

C#에서 기능이 처음 도입된 시기를 찾으려면 C# 언어 버전 기록에 대한 문서를 참조하세요.

C#은 unsafe확인할 수 없는 코드를 작성할 수 있는 컨텍스트도 지원합니다. 안전하지 않은 코드가 반드시 위험한 것은 아닙니다. .NET 도구에서 안전을 확인할 수 없는 코드일 뿐입니다. 안전하지 않은 코드를 사용하여 포인터가 필요한 네이티브 함수를 호출하고 경우에 따라 배열 경계 검사를 방지하는 직접 메모리 액세스를 통해 성능을 향상시킵니다. 안전하지 않은 코드는 보안 및 안정성 위험도 발생합니다. 컨텍스트가 포함된 unsafe 코드를 컴파일하려면 AllowUnsafeBlocks 컴파일러 옵션을 추가합니다.

C#은 안전하지 않은 코드로 간주되는 두 가지 모델, 즉 C# 15 및 .NET 11에서 미리 보기로 제공되는 원래 모델과 업데이트된 메모리 안전 모델을 정의합니다. 두 모델의 차이점에 대한 자세한 내용은 안전하지 않은 코드에 대한 두 가지 모델을 참조하세요.

C#의 안전하지 않은 코드에 대한 모범 사례에 대한 자세한 내용은 안전하지 않은 코드 모범 사례를 참조하세요.

안전하지 않은 코드에 대한 두 가지 모델

C#은 안전하지 않은 코드에 대한 두 가지 모델을 정의합니다. 실제로 모델은 컨텍스트가 필요한 unsafe 작업과 멤버의 unsafe 한정자가 호출자에게 미치는 영향을 결정합니다.

  • 원래 안전하지 않은 모델: 컨텍스트는 unsafe포인터 기능의 존재를 다룹니다. 포인터 형식을 선언하거나, 변수의 주소를 사용하거나, 포인터를 역참조하거나, 식을 포인터로 변환 stackalloc 하거나, 컨텍스트 내에서만 임의의 형식에 unsafe 적용 sizeof 합니다. stackalloc(안전 코드에 Span<T>ReadOnlySpan<T> 할당되거나 허용된 식입니다.) unsafe 형식, 멤버 또는 블록의 한정자는 해당 컨텍스트를 설정하지만 호출자에게는 의무를 두지 않습니다. C# 1.0은 이 모델을 도입했으며 기본값으로 유지됩니다.
  • 업데이트된 메모리 안전 모델: 컨텍스트는 unsafe런타임이 관리하지 않는 메모리에 액세스하는 작업을 다룹니다. 포인터의 존재는 안전하지 않습니다. 포인터의 역참조입니다. unsafe 멤버의 한정자는 호출자에게 안전을 감사할 의무를 전파하는 계약이 됩니다. 이 모델은 C# 15 및 .NET 11에서 미리 보기로 제공됩니다.

다음 표에서는 각 모델에 컨텍스트가 필요한 unsafe 작업을 비교합니다.

Operation 원래 모델 업데이트된 모델
포인터 형식을 선언하거나 를 사용하여 주소를 가져옵니다. & 필요함 unsafe 안전한 코드에서 허용됨
fixed 선언문 필요함 unsafe 안전한 코드에서 허용됨
stackalloc 식을 포인터로 변환 필요함 unsafe 안전한 코드에서 허용됨
sizeof 관리되지 않는 형식의 연산자입니다. 필요함 unsafe 안전한 코드에서 허용됨
포인터 간접 참조(*p), 멤버 액세스(p->m) 또는 요소 액세스(p[i]) 필요함 unsafe 필요함 unsafe
함수 포인터 호출 필요함 unsafe 필요함 unsafe
고정 크기 버퍼의 요소 액세스 필요함 unsafe 필요함 unsafe
표시된 멤버 호출 unsafe 호출자 요구 사항 없음 필요함 unsafe

업데이트된 모델을 시도하려면 .NET 11 SDK(미리 보기)를 사용하고 컴파일러 옵션을 preview로 설정합니다LangVersion. 포인터 이완은 C# 15 컴파일러 및 preview 언어 버전으로 컴파일할 때마다 적용됩니다. 발신자 의무 및 어셈블리 옵트인을 포함한 전체 적용은 아직 개발 중입니다. 자세한 내용은 업데이트된 메모리 안전 모델(미리 보기)을 참조하세요.

원래 안전하지 않은 모델

원래 모델에서 키워드는 unsafe 형식, 멤버 또는 블록에 안전하지 않은 컨텍스트를 설정하고 해당 컨텍스트는 다음 섹션에 설명된 포인터 기능을 잠금 해제합니다. unsafe 한정자는 표시된 코드가 수행할 수 있는 작업만 변경합니다. 호출자에게는 요구 사항이 없습니다. 이러한 예제를 컴파일하려면 AllowUnsafeBlocks 컴파일러 옵션을 설정합니다.

포인터 형식

안전하지 않은 컨텍스트에서 형식은 값 형식 또는 참조 형식 외에 포인터 형식일 수 있습니다. 포인터 형식 선언은 다음 형식 중 하나를 사용합니다.

type* identifier;
void* identifier; //allowed but not recommended

포인터 형식의 * 앞에 지정하는 형식이 참조 형식입니다.

포인터 형식은 개체에서 상속되지 않으며 포인터 형식과 object. 또한 boxing과 unboxing은 포인터를 지원하지 않습니다. 그러나 다른 포인터 형식과 포인터 형식과 정수 형식 간에 변환할 수 있습니다.

동일한 선언에서 여러 포인터를 선언하는 경우 기본 형식과 함께 별표(*)를 작성합니다. 각 포인터 이름의 접두사로 사용되지 않습니다. 예를 들어:

int* p1, p2, p3;   // Ok
int *p1, *p2, *p3;   // Invalid in C#

가비지 수집기는 개체가 어떤 포인터 형식에 의해 가리켜지고 있는지 여부를 추적하지 않습니다. 참조가 관리되는 힙의 개체(람다 식 또는 익명 대리자로 캡처된 지역 변수 포함)인 경우 포인터가 사용되는 한 개체를 고정 해야 합니다.

MyType* 형식의 포인터 변수 값은 MyType형식 변수의 주소입니다. 다음은 포인터 형식 선언의 예입니다.

  • int* p: p 정수에 대한 포인터입니다.
  • int** p: p 정수에 대한 포인터입니다.
  • int*[] p: p 정수에 대한 포인터의 1차원 배열입니다.
  • char* p: p 문자에 대한 포인터입니다.
  • void* p: p 알 수 없는 형식에 대한 포인터입니다.

포인터 간접 참조 연산 * 자를 사용하여 포인터 변수가 가리키는 위치의 내용에 액세스할 수 있습니다. 예를 들어 다음 선언을 고려합니다.

int* myVariable;

*myVariableint포함된 주소에 있는 myVariable 변수를 나타냅니다.

fixed 문서의문에 포인터의 여러 예가 있습니다. 다음 예제에서는 unsafe 키워드와 fixed 문을 사용하고 내부 포인터를 증가하는 방법을 보여 있습니다. 이 코드를 콘솔 애플리케이션의 Main 함수에 붙여넣어 실행할 수 있습니다. 이러한 예제는 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
*/

void*형식의 포인터에는 간접 참조 연산자를 적용할 수 없습니다. 그러나 캐스트를 사용하여 void 포인터를 다른 포인터 형식으로 변환하거나 그 반대로 변환할 수 있습니다.

포인터는 null수 있습니다. 간접 참조 연산자를 null 포인터에 적용하면 구현 정의 동작이 발생합니다.

메서드 간에 포인터를 전달하면 정의되지 않은 동작이 발생할 수 있습니다. in, out또는 ref 매개 변수 또는 함수 결과를 통해 지역 변수에 대한 포인터를 반환하는 메서드를 고려합니다. 포인터가 고정 블록에 설정된 경우 포인터가 가리키는 변수가 더 이상 고정되지 않을 수 있습니다.

다음 표에서는 안전하지 않은 컨텍스트의 포인터에서 작동할 수 있는 연산자와 문을 나열합니다.

연산자/명령문 사용하다
* 포인터 간접 참조를 수행합니다.
-> 포인터를 통해 구조체의 멤버에 액세스합니다.
[] 포인터를 인덱싱합니다.
& 변수의 주소를 가져옵니다.
++-- 포인터를 증가시키고 감소시킵니다.
+- 포인터 산술 연산을 수행합니다.
==, !=, <, >, <=>= 포인터를 비교합니다.
stackalloc 스택에 메모리를 할당합니다.
fixed 진술 주소를 찾을 수 있도록 변수를 일시적으로 수정합니다.

포인터 관련 연산자에 대한 자세한 내용은 포인터 관련 연산자참조하세요.

모든 포인터 형식은 암시적으로 void* 형식으로 변환할 수 있습니다. 모든 포인터 형식에 null값을 할당할 수 있습니다. 캐스트 식을 사용하여 포인터 형식을 다른 포인터 형식으로 명시적으로 변환할 수 있습니다. 모든 정수 계열 형식을 포인터 형식으로 변환하거나 포인터 형식을 정수 형식으로 변환할 수도 있습니다. 이러한 변환에는 명시적 캐스트가 필요합니다.

다음 예제에서는 int*byte*변환합니다. 포인터는 변수의 주소가 가장 낮은 바이트를 가리킵니다. 결과를 연속적으로 증가시키는 경우 int 크기(4바이트)까지 변수의 나머지 바이트를 표시할 수 있습니다.

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
    */
}

고정 크기 버퍼

배열은 참조 형식이므로 안전한 코드에서 배열인 구조체 필드는 요소 자체가 아니라 배열의 요소에 대한 참조만 저장합니다. 다음 struct 크기는 참조이므로 배열 pathName 의 요소 수에 따라 달라지지 않습니다.

public struct PathArray
{
    public char[] pathName;
    private int reserved;
}

구조체 자체 내에 배열의 내용을 저장하려면 키워드를 fixed 사용하여 고정 크기 버퍼를 선언합니다. 키워드에는 fixed 컨텍스트가 unsafe 필요합니다. 고정 크기 버퍼는 다른 언어 또는 플랫폼의 데이터 원본과 상호 운용되는 메서드를 작성할 때 유용합니다. 고정 크기 버퍼는 일반 구조체 멤버에 허용되는 모든 특성 또는 한정자를 사용할 수 있습니다. 유일한 제한 사항은 배열 형식이 , ,, , shortcharint, long, sbyteushort, uintfloatulong또는 double다음과 여야 한다는 것입니다.boolbyte

private fixed char name[30];

다음 예제에서는 fixedBuffer 배열의 크기가 고정되어 있습니다. 문을 사용하여 fixed 첫 번째 요소에 대한 포인터를 가져와서 해당 포인터를 통해 배열의 요소에 액세스합니다. 이 문은 fixed 인스턴스 필드를 메모리의 fixedBuffer 특정 위치에 고정합니다.

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]);
    }
}

배열 char 128개 요소의 크기는 256바이트입니다. 고정 크기 char 버퍼는 인코딩에 관계없이 문자당 항상 2바이트를 사용합니다. 이 배열 크기는 문자 버퍼가 CharSet = CharSet.Auto 또는 CharSet = CharSet.Ansi있는 API 메서드 또는 구조체로 마샬링되는 경우에도 동일합니다. 자세한 내용은 CharSet참조하세요.

앞의 예제에서는 고정하지 않고 fixed 필드에 액세스하는 방법을 보여 줍니다. 또 다른 일반적인 고정 크기 배열은 bool 배열입니다. bool 배열의 요소는 항상 1 바이트 크기입니다. bool 배열은 비트 배열 또는 버퍼를 만드는 데 적합하지 않습니다.

고정 크기 버퍼는 System.Runtime.CompilerServices.UnsafeValueTypeAttribute컴파일되며, CLR(공용 언어 런타임)에 잠재적으로 오버플로될 수 있는 관리되지 않는 배열이 형식에 포함되어 있음을 지시합니다. stackalloc를 사용하여 할당된 메모리는 CLR에서 버퍼 오버런 검색 기능을 자동으로 사용하도록 설정합니다. 앞의 예제에서는 고정 크기 버퍼가 있는 unsafe struct방법을 보여줍니다.

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

Buffer에 대한 컴파일러 생성 C#은 다음과 같이 정의됩니다.

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;
}

고정 크기 버퍼는 다음과 같은 방법으로 일반 배열과 다릅니다.

  • 컨텍스트에서 unsafe 만 사용할 수 있습니다.
  • 구조체의 인스턴스 필드만 사용할 수 있습니다.
  • 항상 벡터 또는 1차원 배열입니다.
  • 선언에는 길이(예: fixed char id[8].)가 포함되어야 합니다. fixed char id[]사용할 수 없습니다.

함수 포인터

C#은 안전한 함수 포인터 개체를 정의하는 delegate 형식을 제공합니다. 대리자를 호출하려면 System.Delegate 파생된 형식을 인스턴스화하고 해당 Invoke 메서드에 가상 메서드를 호출해야 합니다. 이 가상 호출은 callvirt IL 명령을 사용합니다. 성능에 중요한 코드 경로에서 calli IL 명령을 사용하는 것이 더 효율적입니다.

구문을 사용하여 함수 포인터를 정의할 delegate* 수 있습니다. 컴파일러는 개체를 인스턴스화하고 호출하는 대신 명령을 사용하여 calli 함수를 delegate 호출 Invoke합니다. 다음 코드는 delegate 또는 delegate* 사용하여 동일한 형식의 두 개체를 결합하는 두 가지 메서드를 선언합니다. 첫 번째 메서드는 System.Func<T1,T2,TResult> 대리자 형식을 사용합니다. 두 번째 메서드는 매개 변수와 반환 형식이 동일한 delegate* 선언을 사용합니다.

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);

다음 코드는 정적 로컬 함수를 선언하고 해당 로컬 함수에 UnsafeCombine 대한 포인터를 사용하여 메서드를 호출하는 방법을 보여줍니다.

int product = 0;
unsafe
{
    static int localMultiply(int x, int y) => x * y;
    product = UnsafeCombine(&localMultiply, 3, 4);
}

앞의 코드는 함수 포인터로 액세스되는 함수에 대한 몇 가지 규칙을 보여 줍니다.

  • 컨텍스트에서만 함수 포인터를 선언할 unsafe 수 있습니다.
  • 컨텍스트에서 a를 사용 delegate* 하거나 반환하는 메서드만 호출할 delegate*unsafe 수 있습니다.
  • 함수의 주소를 가져오는 & 연산자는 static 함수에서만 허용됩니다. 이 규칙은 멤버 함수와 로컬 함수 모두에 적용됩니다.

구문에는 delegate 형식 선언 및 포인터 사용과 유사합니다. *delegate 접미사는 선언이 함수 포인터임을 나타냅니다. 함수 포인터에 메서드 그룹을 할당할 때 & 작업은 메서드의 주소를 사용했음을 나타냅니다.

키워드를 사용하여 a delegate* 에 대한 호출 규칙을 지정할 수 있습니다managed.unmanaged 또한 unmanaged 함수 포인터의 경우 호출 규칙을 지정할 수 있습니다. 다음 선언은 각각에 대한 예제를 보여 줍니다. 첫 번째 선언은 기본값인 managed 호출 규칙을 사용합니다. 다음 4개에서는 unmanaged 호출 규칙을 사용합니다. 각각은 ECMA 335 호출 규칙(Cdecl, Stdcall, Fastcall또는 Thiscall중 하나를 지정합니다. 마지막 선언은 unmanaged 호출 규칙을 사용하여 CLR이 플랫폼에 대한 기본 호출 규칙을 선택하도록 지시합니다. CLR은 런타임에 호출 규칙을 선택합니다.

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);

C# 언어 사양의 함수 포인터 섹션에서 함수 포인터 에 대해 자세히 알아볼 수 있습니다.

예: 포인터를 사용하여 바이트 배열 복사

다음 예제에서는 포인터를 사용하여 한 배열에서 다른 배열로 바이트를 복사합니다.

이 예제에서는 메서드에서 unsafe 포인터를 사용할 수 있도록 하는 키워드를 Copy 사용합니다. 이 문은 fixed 원본 및 대상 배열에 대한 포인터를 선언합니다. fixed 문은 원본 및 대상 배열이 가비지 수집으로 인해 메모리에서 이동하지 않도록 그 위치를 고정합니다. 블록은 fixed 블록 범위의 배열에 대한 메모리 블록을 고정합니다. 이 예제의 메서드는 Copy 키워드를 unsafe 사용하므로 AllowUnsafeBlocks 컴파일러 옵션을 사용하여 컴파일해야 합니다.

다음은 관리되지 않는 두 번째 포인터가 아닌 인덱스를 사용하여 두 배열의 요소에 액세스하는 예제입니다. pSourcepTarget 포인터 선언은 배열을 고정합니다.

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
    */
}

업데이트된 메모리 안전 모델(미리 보기)

Important

업데이트된 메모리 안전 모델은 C# 15 및 .NET 11의 미리 보기 기능입니다. 미리 보기 릴리스 중에 피드백에 따라 계속 발전하고 있습니다. 모델을 시도하려면 .NET 11(미리 보기) SDK를 사용하고 컴파일러 옵션을 preview로 설정합니다LangVersion. .NET 11 Preview 5의 컴파일러는 포인터 완화를 구현하지만 호출자 의무, 어셈블리 옵트인 또는 safe 키워드를 아직 적용하지는 않습니다. 전체 디자인은 메모리 안전 기능 사양을 참조하세요.

업데이트된 모델은 원래 모델이 하나로 취급하는 두 가지, 즉 포인터 코드의 존재 와 호출자에 대한 안전 의무 전파 를 구분합니다. 멤버 unsafe 를 표시하면 더 이상 본문에 포인터가 허용되지 않으므로 멤버 호출자가 안전하지 않게 되므로 모든 호출자는 해당 의무를 전파하거나 유효성이 검사되고 안전한 호출 가능한 경계 뒤에 내보내야 합니다. 이러한 분리를 지원하기 위해 모델은 안전하지 않은 컨텍스트의 범위를 좁혀줍니다. 포인터의 존재는 안전하지 않고 런타임에서 관리하지 않는 메모리에 액세스하는 작업만을 관리합니다. 축소를 사용하면 안전한 코드에서 포인터를 유지, 전달 및 반환하는 동시에 unsafe 실제로 메모리 안전을 위반할 수 있는 작업 및 멤버를 표시할 수 있습니다.

호출자 안전하지 않은 멤버

원래 모델에서 멤버의 unsafe 한정자는 멤버의 서명 및 본문에 있는 포인터만 허용합니다. 발신자에게 안전에 대해 알리지 않습니다. 업데이트된 모델은 호출자에게 한정자를 의미합니다. 멤버 unsafe를 표시할 때 컴파일러는 호출자 안전하지 않음 ( requires-unsafe라고도 함)으로 처리합니다. 모든 호출자는 컨텍스트에서 unsafe 호출해야 하며 보안 감사 의무는 해당 호출자로 이동합니다.

unsafe 멤버 서명의 한정자는 더 이상 본문에 안전하지 않은 컨텍스트를 설정하지 않습니다. 두 역할은 분할됩니다.

  • unsafe 서명의 한정자는 호출자에게 의무를 전파합니다.
  • 내부 unsafe 블록은 관리되지 않는 메모리에 액세스하는 작업의 범위를 지정합니다.

다음 미리 보기 모형 ReadInt32 에서는 호출자가 안전하지 않습니다. 서명은 한정자를 전달 unsafe 하고 내부 unsafe 블록은 역참조를 래핑합니다.

// 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;
    }
}

호출자는 자체 unsafe 블록에서 호출을 래핑합니다.

// Preview
unsafe
{
    int value = ReadInt32(buffer);
}

업데이트된 모델은 다음과 같은 몇 가지 관련 규칙도 강화합니다.

  • unsafe 한정자에 알릴 호출자가 없기 때문에 한정자는 형식 선언, 정적 생성자 및 종료자에 대한 오류를 생성합니다.
  • 대리자는 형식 모양이므로 대리자를 사용할 unsafe수 없습니다.
  • 매개 변수가 없는 생성자가 제약 조건을 충족 new() 하지 않는 형식입니다unsafe.

안전하지 않은 컨텍스트가 필요한 작업

가리키는 메모리에 액세스하는 작업에는 컨텍스트가 필요합니다.unsafe

  • 포인터 간접 참조(*p), 포인터 멤버 액세스(p->member) 및 포인터 요소 액세스(p[i]).
  • 함수 포인터 호출입니다.
  • 고정 크기 버퍼에 대한 요소 액세스입니다.

다음 예제에서는 컨텍스트 없이 배열을 unsafe 고정하지만 포인터를 하나 안에 역참조합니다.

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;
        }
    }
}

완화된 작업

가리키기 메모리에 액세스하지 않는 작업에는 더 이상 컨텍스트가 unsafe 필요하지 않습니다.

  • 포인터 형식을 선언하고 연산자를 사용하여 변수의 주소를 가져옵니다 & .
  • fixed 변수를 고정하는 문입니다.
  • 식을 포인터로 변환합니다 stackalloc .
  • sizeof 관리되지 않는 모든 형식에 적용된 연산자입니다.

다음 예제에서는 컨텍스트 없이 unsafe 포인터를 만들고 고정합니다.

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;
    }
}

이러한 완화는 어셈블리가 업데이트된 preview 메모리 안전 규칙을 옵트인하는지 여부에 관계없이 언어 버전으로 컴파일할 때마다 적용됩니다.

퇴원 발신자 안전하지 않은 의무

호출자 안전하지 않은 작업을 호출하는 멤버에는 의무를 전파하거나 해지하는 두 가지 옵션이 있습니다.

  • 전파: 고유한 멤버 unsafe를 표시합니다. 의무는 발신자에게 전달됩니다. 의무를 직접 완전히 검증할 수 없는 경우 전파를 사용합니다.
  • 방전: 회원의 서명을 안전하게 둡니다. 일반적으로 런타임 가드를 사용하여 멤버 내의 의무에 대한 유효성을 검사한 다음 내부 unsafe 블록에서 안전하지 않은 작업을 수행합니다. 내부 unsafe 블록을 포함하지만 자체 서명을 unsafe 표시하지 않는 멤버는 안전하지 않은 경계입니다. 안전하지 않은 코드를 안전한 호출 가능 화면으로 바꿉니다.

다음 미리 보기 모형은 가드를 사용하여 입력의 유효성을 검사하고, 관리되는 배열을 고정하고, 포인터를 읽습니다. 호출자는 컨텍스트가 unsafe 필요하지 않습니다. 메서드가 의무를 이행하기 때문입니다.

// 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;
        }
    }
}

null 검사 및 배열 길이는 버퍼를 지나 읽기가 실행되도록 하는 입력을 배제하므로 블록 내부의 unsafe 역참조가 소리입니다. 이 메서드는 잔여 의무를 남기지 않으므로 안전한 호출 가능한 서명을 노출합니다.

안전 설명서

호출자 안전하지 않은 멤버는 호출자가 보장해야 하는 내용을 문서화해야 합니다. 업데이트된 모델은 다음과 같은 두 가지 보완적인 주석 스타일을 권장합니다.

  • /// <safety> 서명 위의 설명서 블록은 공식 계약( 호출자가 충족해야 하는 조건)을 나타냅니다. 분석기는 누락된 호출자 안전하지 않은 멤버에 플래그를 지정할 수 있습니다.
  • // SAFETY: 블록 내부의 unsafe 주석은 신체를 읽는 개발자와 감사자를 위해 작업이 그 자리에서 들리는 이유를 기록합니다.

다음 미리 보기 모형은 호출자 안전하지 않은 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];
    }
}

블록은 /// <safety> 계약을 알려줍니다. 계약은 모든 호출자 및 검토자가 볼 수 있는 설명서에 속합니다.

안전하지 않은 필드

선언된 형식이 unsafe 바깥쪽 형식이 유지 관리하는 계약을 표현하지 않고 다른 코드가 종속된 경우 필드에 한정자를 사용합니다. 안전하지 않은 것은 형식 시스템에서 보는 것과 형식이 약속하는 것 사이의 간격에 존재합니다. 한정자는 필드에 대한 모든 쓰기를 블록으로 강제 적용 unsafe 하여 쓰기를 한 곳에서 검토할 수 있게 합니다.

가장 명확한 사례는 네이티브 포인터가 있는 필드입니다. 포인터는 주소 System.Span<T> 가 있는 바이트 수를 선언하지 않으므로 포함하는 형식은 해당 정보 자체를 유지 관리합니다.

// 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 필드는 계약을 기본 제공 가드 unsafe 와 쌍으로 연결합니다. 고정 이름을 지정하고 readonly 생성 후 중단시킬 수 있는 쓰기를 방지합니다. 속성 또는 이벤트를 unsafe 표시해도 해당 지원 필드 호출자가 안전하지 않은 것은 아닙니다. 구조체 [StructLayout(LayoutKind.Explicit)]에서 모든 필드를 safe 표시하거나 unsafe.

safe 키워드

업데이트된 모델은 컴파일러에서 명시적으로 선택해야 하는 경우 선언이 소리임을 나타내는 상황별 키워드를 추가 safe 합니다.

멤버는 네이 extern 티브 코드를 호출하므로 컴파일러는 해당 보안을 분류할 수 없습니다. 업데이트된 모델에서 부분 메서드를 비롯한 모든 extern 선언을 LibraryImport 표시합니다unsafe.safe

// Preview
[LibraryImport("libc")]
internal static safe partial int getpid();

[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)]
internal static unsafe partial nint strlen(byte* str);

getpid 매개 변수를 사용하지 않고 기본 형식을 반환하므로 작성자는 호출이 안전하다는 것을 증명하고 호출자는 의식 없이 사용합니다. strlen 는 네이티브 코드가 역참조하는 원시 포인터를 사용하므로 선언은 unsafe 호출자에게 의무를 전파합니다. 두 한정자를 모두 생략하는 것은 오류이므로 안전 결정을 내려야 합니다. 명시적 레이아웃이 있는 구조체의 필드는 동일한 규칙을 사용합니다.

옵트인 및 어셈블리 간 동작

업데이트된 모델에는 두 개의 독립적인 프로젝트 수준 스위치가 있습니다.

  • 새 옵트인 속성은 업데이트된 규칙을 켭니다. 속성이 꺼져 있으면 원래 규칙이 적용됩니다. 이 항목이 켜 unsafe 지면 멤버에서 호출자에게 전파되고 컴파일러는 어셈블리에서 선택한 항목을 특성과 함께 MemorySafetyRulesAttribute 기록합니다.
  • 기존 AllowUnsafeBlocks 속성은 호출 사이트의 내부 블록을 포함하여 키워드의 unsafe 모든 모양을 게이트합니다. 기본값은 기본 false값이므로 기본값의 프로젝트는 안전하지 않은 API를 호출할 수 없습니다.

두 속성은 다음과 같이 결합됩니다.

옵트인 속성 AllowUnsafeBlocks Result
켜기 Off(기본값) 가장 안전한 구성입니다. 프로젝트는 업데이트된 모델을 사용하며 안전하지 않은 코드를 허용하지 않습니다.
켜기 켜기 프로젝트는 업데이트된 모델을 사용하고 안전하지 않은 코드를 허용합니다.
끄기 끄기 원래 모델이 적용되고 프로젝트에서 포인터 형식을 사용할 수 없습니다.
끄기 켜기 원래 모델이 적용되고 프로젝트에서 포인터 형식을 사용할 수 있습니다.

한 어셈블리가 업데이트된 규칙을 다른 어셈블리에 적용하는지 여부는 어느 쪽에서 옵트인하는지에 따라 달라집니다.

  • 업데이트된 모델 호출자, 업데이트된 모델 호출 수신자: 호출 수신자의 unsafe 표식은 메타데이터를 통해 이동합니다. 호출자는 블록에서 호출자 안전하지 않은 멤버에 대한 각 호출을 래핑합니다 unsafe .
  • 업데이트된 모델 호출자, 원래 모델 호출 수신자: 호환 모드는 서명의 포인터 형식을 가진 호출 수신자 멤버를 호출자 안전하지 않은 것으로 처리하므로 호출 사이트에는 바깥쪽 unsafe 블록이 필요합니다. 이 모드는 포인터 기반 API가 자동으로 요구 사항을 잃지 unsafe 않도록 합니다.
  • 원래 모델 호출자, 업데이트된 모델 호출 수신자: 원래 포인터 규칙이 계속 적용됩니다. 서명에 포인터 형식이 없는 호출자 안전하지 않은 멤버는 원래 모델 호출자가 새 마커를 읽을 수 없으므로 안전 코드에서 호출할 수 있습니다.

C# 언어 사양

자세한 내용은 C# 언어 사양안전하지 않은 코드 장을 참조하세요.

업데이트된 메모리 안전 모델의 디자인은 메모리 안전 기능 사양을 참조하세요.

참고하십시오