Güvenli olmayan kod, işaretçi türleri ve işlev işaretçileri

Yazdığınız C# kodunun çoğu "verifili güvenli kod" dir. Doğruıda güvenli kod .NET araçları 'nın kodun güvenli olduğunu doğrulayabileceği anlamına gelir. Genel olarak, güvenli kod işaretçiler kullanılarak belleğe doğrudan erişemez. Ayrıca ham bellek ayırmaz. Bunun yerine yönetilen nesneler oluşturur.

C# unsafe , içinde doğrulanamayan kod yazabileceğiniz bir bağlamı destekler. Bir unsafe bağlamda, kod işaretçiler kullanabilir, bellek ayırır ve serbest bellek blokları kullanabilir ve işlev işaretçileri kullanarak yöntemleri çağırabilir. C# ' deki güvenli olmayan kod, tehlikeli değildir; yalnızca güvenliği doğrulanamayan bir koddur.

Güvenli olmayan kod aşağıdaki özelliklere sahiptir:

  • Yöntemler, türler ve kod blokları güvenli olmayan şekilde tanımlanabilir.
  • Bazı durumlarda, güvenli olmayan kod, dizi sınırları denetimlerini kaldırarak bir uygulamanın performansını artırabilir.
  • İşaretçi gerektiren yerel işlevleri çağırdığınızda güvenli olmayan kod gereklidir.
  • Güvenli olmayan kod kullanmak güvenlik ve kararlılık riskleri sağlar.
  • Güvenli olmayan bloklar içeren kodun AllowUnsafeBlocks derleyici seçeneğiyle derlenmesi gerekir.

İşaretçi türleri

Güvenli olmayan bir bağlamda, bir tür, bir değer türüne ek olarak bir işaretçi türü veya bir başvuru türü olabilir. Bir işaretçi türü bildirimi, aşağıdaki biçimlerden birini alır:

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

*Bir işaretçi türündeki öğesinden önce belirtilen tür, başvurulan tür olarak adlandırılır. Yalnızca yönetilmeyen bir tür , başvurulan bir tür olabilir.

İşaretçi türleri nesneden aktarılmaz ve işaretçi türleri ve arasında dönüştürme yok object . Ayrıca, kutulama ve kutudan çıkarma işaretçileri desteklemez. Ancak, farklı işaretçi türleri ve işaretçi türleri ve tamsayı türleri arasında dönüştürme yapabilirsiniz.

Aynı bildirimde birden çok işaretçi bildirdiğinizde, yıldız işaretini ( * ) yalnızca temel alınan türle birlikte yazarsınız. Her işaretçi adının öneki olarak kullanılmaz. Örnek:

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

Bir işaretçi, bir işaretçiye işaret eden bir nesne başvurusu atık olarak toplanabileceğinden, bir başvuruya veya başvuru içeren bir yapıya işaret edebilir. Çöp toplayıcı, bir nesnenin herhangi bir işaretçi türü tarafından işaret edilip edilmeyeceğini izlememez.

Türündeki işaretçi değişkeninin değeri, MyType* türünde bir değişkenin adresidir MyType . Aşağıda, işaretçi türü bildirimi örnekleri verilmiştir:

  • int* p: p tamsayı olan bir işaretçidir.
  • int** p: bir p tamsayı işaretçisinin işaretçisi.
  • int*[] p: p tamsayılara yönelik işaretçilerin tek boyutlu bir dizisidir.
  • char* p: bir p char işaretçisi.
  • void* p: p bilinmeyen bir tür işaretçisi.

İşaretçi yöneltme işleci, * işaretçi değişkeni tarafından işaret edilen konumdaki içeriğe erişmek için kullanılabilir. Örneğin, aşağıdaki bildirimi ele alalım:

int* myVariable;

İfade, *myVariable içinde bulunan int adreste bulunan değişkeni gösterir myVariable .

fixed Deyimdekimakalelerde birçok işaretçiyle ilgili birkaç örnek vardır. Aşağıdaki örnek, unsafe anahtar sözcüğünü ve ifadesini kullanır fixed ve iç işaretçinin nasıl artırılacağını gösterir. Bu kodu çalıştırmak için bir konsolun Ana işlevine yapıştırabilirsiniz. Bu örneklerin AllowUnsafeBlocks derleyici seçenek kümesiyle derlenmesi gerekir.

// Normal pointer to an object.
int[] a = new int[5] { 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
*/

Yöneltme işlecini türündeki bir işaretçiye uygulayamazsınız void* . Ancak, boş bir işaretçiyi başka herhangi bir türü dönüştürmek veya bunun tersini yapmak için bir yayın kullanabilirsiniz.

Bir işaretçi olabilir null . Yönlendirme işlecini bir null işaretçiye uygulamak, uygulama tarafından tanımlanan bir davranışa neden olur.

Yöntemler arasında işaretçiler geçirmek tanımsız davranışlara neden olabilir. Bir in , out veya ref parametresi ya da işlev sonucu olarak yerel bir değişkene bir işaretçi döndüren bir yöntem düşünün. İşaretçi sabit bir blokta ayarlandıysa, işaret ettiği değişken artık sabit olamaz.

Aşağıdaki tabloda, güvenli olmayan bir bağlamda işaretçiler üzerinde işlem yapabilecek işleçler ve deyimler listelenmektedir:

İşleç/Deyim Kullanın
* İşaretçi yöneltmesi gerçekleştirir.
-> Bir yapının bir üyesine bir işaretçi yoluyla erişir.
[] Bir işaretçiyi dizine ekler.
& Bir değişkenin adresini alır.
++ ve -- İşaretçileri artırır ve azaltır.
+ ve - İşaretçi aritmetiği gerçekleştirir.
==, != , < , > , <= ve >= İşaretçileri karşılaştırır.
stackalloc Yığında bellek ayırır.
fixed Ekstre Adresinin bulunamaması için bir değişkeni geçici olarak sabitler.

İşaretçiyle ilgili işleçler hakkında daha fazla bilgi için bkz. işaretçi ile ilgili işleçler.

Herhangi bir işaretçi türü örtük olarak bir türe dönüştürülebilir void* . Herhangi bir işaretçi türüne değer atanabilir null . Herhangi bir işaretçi türü, açıkça atama ifadesi kullanan başka bir işaretçi türüne dönüştürülebilir. Ayrıca, herhangi bir integral türünü bir işaretçi türüne veya herhangi bir işaretçi türüne bir integral türüne dönüştürebilirsiniz. Bu dönüşümler açık bir tür dönüştürme gerektirir.

Aşağıdaki örnek bir öğesine dönüştürür int* byte* . İşaretçinin, değişkenin en düşük adresli baytını işaret ettiğini unutmayın. Sonucu büyük ölçüde arttırdığınızda int (4 bayt), değişkenin kalan baytlarını görüntüleyebilirsiniz.

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

Sabit boyutlu arabellekler

C# ' de, bir veri yapısında sabit boyutlu bir diziye sahip bir arabellek oluşturmak için fixed ifadesini kullanabilirsiniz. Sabit boyutlu arabellekler, diğer dillerdeki veya platformlardaki veri kaynaklarıyla birlikte çalışan Yöntemler yazdığınızda faydalıdır. Sabit dizi, normal yapı üyeleri için izin verilen herhangi bir özniteliği veya değiştiricilerini alabilir. Tek kısıtlama, dizi türünün,,,,,,,, bool byte char short int long sbyte ushort uint , ulong ,, float veya double olması olabilir.

private fixed char name[30];

Güvenli kodda, dizi içeren bir C# yapısı dizi öğelerini içermez. Struct, bunun yerine öğelerine bir başvuru içerir. Bir yapıda sabit boyutlu bir diziyi, güvenli olmayan bir kod bloğunda kullanıldığında bir yapıya katıştırabilirsiniz.

Aşağıdaki boyut, struct bir başvuru olduğundan dizideki öğelerin sayısına bağlı değildir pathName :

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

struct, Güvenli olmayan kodda gömülü bir dizi içerebilir. Aşağıdaki örnekte, fixedBuffer dizisinin sabit bir boyutu vardır. fixedİlk öğe için bir işaretçi oluşturmak üzere bir ifade kullanırsınız. Bu işaretçi aracılığıyla dizinin öğelerine erişirsiniz. fixedİfade, fixedBuffer örnek alanını bellekte belirli bir konuma sabitsabitler.

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

128 öğe char dizisinin boyutu 256 bayttır. Sabit boyutlu char arabellekleri, kodlamadan bağımsız olarak her zaman karakter başına 2 bayt sürer. Bu dizi boyutu, karakter arabelleklerinin API yöntemlerine veya ya da yapı birimleri için sıralanmasına karşın aynı olur CharSet = CharSet.Auto CharSet = CharSet.Ansi . Daha fazla bilgi için bkz. CharSet.

Yukarıdaki örnekte fixed , C# 7,3 ile başlayarak kullanılabilir olan sabitleme olmadan alanlara erişme gösterilmektedir.

Diğer bir yaygın sabit boyutlu dizi bool dizidir. Bir bool dizideki öğeler her zaman boyutu 1 bayttır. bool diziler, bit dizileri veya arabellekleri oluşturmak için uygun değildir.

Sabit boyutlu arabellekler ile derlenir ve System.Runtime.CompilerServices.UnsafeValueTypeAttribute Bu, ortak dil çalışma zamanına (CLR) bir türün, bulunabilecek bir yönetilmeyen dizi içerdiğini söyler. stackalloc kullanılarak ayrılan bellek, CLR'de arabellek taşması algılama özelliklerini de otomatik olarak sağlar. Önceki örnek, sabit boyutlu arabelleğin içinde nasıl var olduğunu unsafe struct gösterir.

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

derleyicisi tarafından oluşturulan C# Buffer için öznitelik şu şekildedir:

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

Sabit boyutlu arabellekler, aşağıdaki yollarla normal dizilerden farklıdır:

  • Yalnızca bir bağlamda unsafe kullanılabilir.
  • Yalnızca yapıların örnek alanları olabilir.
  • Bunlar her zaman vektör veya tek boyutlu dizilerdir.
  • Bildirim, gibi uzunluğu fixed char id[8] içermeli. 'i fixed char id[] kullanasiniz.

Bayt dizisine kopyalamak için işaretçileri kullanma

Aşağıdaki örnek, baytları bir diziden diğerine kopyalamak için işaretçileri kullanır.

Bu örnekte, yönteminde işaretçileri kullanmana olanak sağlayan güvenli olmayan anahtar sözcüğü Copy kullanır. Sabit deyimi, kaynak ve hedef dizilere işaretçiler bildiren kullanılır. deyimi, kaynak ve hedef dizilerinin konumunu belleğe fixed sabitler, böylece çöp toplama tarafından taşınmazlar. Blok tamamlandığında dizilerin bellek blokları fixed sabitlenmiş değil. Bu Copy örnekteki yöntem anahtar unsafe sözcüğünü kullandığı için AllowUnsafeBlocks derleyici seçeneğiyle derlenmiş olması gerekir.

Bu örnek, ikinci bir unmanaged işaretçisi yerine dizinleri kullanarak her iki dizinin öğelerine erişer. ve işaretçilerinin pSource pTarget bildirimi dizileri sabitler. Bu özellik C# 7.3'den başlayarak kullanılabilir.

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

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

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

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

İşlev işaretçileri

C# güvenli delegate işlev işaretçisi nesneleri tanımlamak için türleri sağlar. Temsilci çağırma, türünden türetilen bir türün örneğini oluşturmak ve System.Delegate yöntemine sanal yöntem çağrısı Invoke yapmaktır. Bu sanal çağrı callvirt IL yönergesi kullanır. Performans açısından kritik kod yollarında calli IL yönergesi daha verimlidir.

Söz dizimi kullanarak bir işlev işaretçisi delegate* tanımlayabilirsiniz. Derleyici, nesnesinin örneğini oluşturma ve calli çağırma yerine yönergeyi kullanarak delegate işlevini Invoke çağıracak. Aşağıdaki kod, aynı türde iki nesne birleştirmek için delegate bir veya kullanan iki yöntem delegate* bildirer. İlk yöntem bir temsilci System.Func<T1,T2,TResult> türü kullanır. İkinci yöntem, aynı delegate* parametrelere ve dönüş türüne sahip bir bildirim kullanır:

public static T Combine<T>(Func<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

public static T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

Aşağıdaki kod, statik bir yerel işlevi nasıl bildirebilirsiniz ve bu yerel işlevin işaretçisini UnsafeCombine kullanarak yöntemini nasıl çağırabilirsiniz:

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

Yukarıdaki kod, işlev işaretçisi olarak erişilen işlevde birkaç kuralı gösterir:

  • İşlev işaretçileri yalnızca bir bağlamda unsafe bildirebilirsiniz.
  • Bir (veya delegate* dönüş) alan yöntemler delegate* yalnızca bir bağlamda unsafe çağrılır.
  • Bir & işlevin adresini almak için işlecine yalnızca işlevlerde izin static verilir. (Bu kural hem üye işlevleri hem de yerel işlevler için geçerlidir).

Söz dizimi, türleri bildirerek ve delegate işaretçileri kullanarak paraleldir. üzerinde * soneki delegate bildiriminin bir işlev işaretçisi olduğunu gösterir. bir & işlev işaretçisine bir yöntem grubu atarken işlemi yöntemin adresini alır gösterir.

ve anahtar sözcüklerini kullanarak için delegate* çağırma kuralı managed unmanaged belirtebilirsiniz. Ayrıca işlev unmanaged işaretçileri için çağırma kuralı belirtebilirsiniz. Aşağıdaki bildirimlerde her biri için örnekler verilmiştir. İlk bildirim, managed varsayılan olan çağırma kuralı kullanır. Sonraki üç kural bir çağırma unmanaged kuralı kullanır. Her biri ECMA 335 çağırma kurallarından birini belirtir: Cdecl , Stdcall , veya Fastcall Thiscall . Son bildirimlerde, CLR'ye platform için varsayılan çağırma kuralı seçmesini unmanaged bildirerek çağırma kuralı kullanılır. CLR, çalışma zamanında çağırma kuralı seçer.

public static T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
    combinator(left, right);

C# 9.0 için İşlev işaretçisi teklifinde işlev işaretçileri hakkında daha fazla bilgi öğrenebilirsiniz.

C# dili belirtimi

Daha fazla bilgi için C# dil belirtimlerinin Güvenli olmayan kod bölümüne bakın.