Aracılığıyla paylaş


Bellek<T> ve Span<T> kullanım yönergeleri

.NET, rastgele bir bitişik bellek bölgesini temsil eden bir dizi tür içerir. Span<T> ve ReadOnlySpan<T> yönetilen veya yönetilmeyen belleğe başvuruları sarmalayan hafif bellek arabellekleridir. Bu türler yalnızca yığında depolanabildiğinden, zaman uyumsuz yöntem çağrıları gibi senaryolar için uygun değildir. Bu sorunu gidermek için .NET 2.1, , Memory<T>, ReadOnlyMemory<T>ve IMemoryOwner<T>gibi MemoryPool<T>bazı ek türler ekledi. Span<T> gibi, Memory<T> ve ilgili türleri hem yönetilen hem de yönetilmeyen bellek tarafından yedeklenebilir. Span<T>'nin aksine Memory<T>yönetilen yığında depolanabilir.

Hem Span<T> hem de Memory<T>, işlem hatlarında kullanılabilecek yapılandırılmış verilerin arabellekleri üzerinde sarmalayıcılardır. Bu demek oluyor ki, verilerin bir kısmının veya tümünün işlem hattındaki bileşenlere verimli bir şekilde geçirilebilmesi, bu bileşenlerin verileri işleyebilmesi ve arzu edilirse arabelleği değiştirebilmesi amacıyla tasarlanmıştır. Memory<T> ve ilgili türlerine birden çok bileşen veya birden çok iş parçacığı tarafından erişilebildiği için, sağlam kod oluşturmak için bazı standart kullanım yönergelerini izlemek önemlidir.

Sahipler, tüketiciler ve yaşam süresi yönetimi

Arabellekler API'ler arasında geçirilebilir ve bazen birden fazla iş parçacığından erişilebilir, bu nedenle arabellek ömrünün nasıl yönetildiğinin farkında olun. Üç temel kavram vardır:

  • Sahiplik. Arabellek örneğinin sahibi, artık kullanımda olmadığında arabelleği yok etmek dahil yaşam süresi yönetiminden sorumludur. Tüm arabelleklerin tek bir sahibi vardır. Genellikle sahibi, arabelleği oluşturan veya bir fabrikadan arabelleği alan bileşendir. Sahiplik de aktarılabilir; Bileşen-A , arabellek denetimini Bileşen-B'ye verebilir; bu noktada Bileşen-A artık arabelleği kullanmayabilir ve Component-B artık kullanımda olmadığında arabelleeği yok etmekle sorumlu olur.

  • Tüketim. Bir arabellek örneğinin tüketicisinin, arabellek örneğinden okuyarak ve mümkünse ona yazarak arabellek örneğini kullanmasına izin verilir. Bazı dış eşitleme mekanizmaları sağlanmadığı sürece arabellekler aynı anda yalnızca bir tüketiciye sahip olabilir. Bir arabelleğin etkin tüketicisi, arabelleğin sahibi olmayabilir.

  • Kira Sözleşmesi. Kiralama süresi, belirli bir bileşenin arabelleği tüketmesine izin verilen süreyi belirler.

Aşağıdaki sahte kod örneği bu üç kavramı gösterir. Buffer, sahte kodda Memory<T> veya Span<T> türünde Char bir arabelleği temsil eder. Main yöntemi arabelleğin örneğini oluşturur, WriteInt32ToBuffer arabelleğe bir tamsayının dize gösterimini yazmak için yöntemini çağırır ve ardından arabelleğin değerini görüntülemek için yöntemini çağırırDisplayBufferToConsole.

using System;

class Program
{
    // Write 'value' as a human-readable string to the output buffer.
    void WriteInt32ToBuffer(int value, Buffer buffer);

    // Display the contents of the buffer to the console.
    void DisplayBufferToConsole(Buffer buffer);

    // Application code
    static void Main()
    {
        var buffer = CreateBuffer();
        try
        {
            int value = Int32.Parse(Console.ReadLine());
            WriteInt32ToBuffer(value, buffer);
            DisplayBufferToConsole(buffer);
        }
        finally
        {
            buffer.Destroy();
        }
    }
}

Main yöntemi arabelleği oluşturur ve dolayısıyla onun sahibidir. Bu nedenle, Main artık kullanımda olmadığında arabelleği yok etmekle sorumludur. Sahte kod, arabellekte bir Destroy yöntem çağırarak bunu gösterir. (Memory<T> veya Span<T>'ün aslında bir Destroy yöntemi yoktur. Bu makalenin devamında gerçek kod örneklerini göreceksiniz.)

Arabelleğin iki tüketicisi vardır: WriteInt32ToBuffer ve DisplayBufferToConsole. Bir kerede yalnızca bir tüketici vardır (önce WriteInt32ToBuffer, sonra DisplayBufferToConsole), ve hiçbir tüketici arabelleğe sahip değildir. Bu bağlamdaki "tüketici" ifadesinin, arabelleğin sadece salt okunur bir görünümünü ifade etmediğini unutmayın; tüketiciler, arabelleğin okuma/yazma görünümü verildiğinde, WriteInt32ToBuffer gibi arabellek içeriğini değiştirebilir.

WriteInt32ToBuffer yöntemi, yöntem çağrısının başlangıcı ile yöntemin dönüşü arasındaki sürede arabelleği kullanabilir. Benzer şekilde, DisplayBufferToConsole yürütülürken arabellekte bir kirası vardır ve yöntem geri sarıldığında kira serbest bırakılır. (Kiralama yönetimi için API yoktur; "kiralama" kavramsal bir konudur.)

Bellek<T> ve sahiplik/tüketim modeli

Sahipler, tüketiciler ve yaşam süresi yönetimi bölümünde belirtildiği gibi, bir arabelleğin her zaman bir sahibi vardır. .NET iki sahiplik modeli destekler:

  • Tek sahipli modeli destekleyen bir model. Arabellek, tüm ömrü boyunca tek bir sahipe sahiptir.
  • Sahiplik aktarımını destekleyen bir model. Bir arabelleğin sahipliği özgün sahibinden (oluşturucusundan) başka bir bileşene aktarılabilir ve bu da arabelleğin yaşam süresi yönetiminden sorumlu olur. Bu sahip, sahipliği başka bir bileşene aktarabilir ve bu şekilde devam edebilir.

System.Buffers.IMemoryOwner<T> arayüzünü kullanarak bir arabelleğin sahipliğini açıkça yönetirsiniz. IMemoryOwner<T> her iki sahiplik modellerini de destekler. Tampon belleğe referansı olan IMemoryOwner<T> bileşen tampona sahiptir. Aşağıdaki örnek, bir IMemoryOwner<T> arabelleğin sahipliğini yansıtmak için bir Memory<T> örnek kullanır.

using System;
using System.Buffers;

class Example
{
    static void Main()
    {
        IMemoryOwner<char> owner = MemoryPool<char>.Shared.Rent();

        Console.Write("Enter a number: ");
        try
        {
            string? s = Console.ReadLine();

            if (s is null)
                return;

            var value = Int32.Parse(s);

            var memory = owner.Memory;

            WriteInt32ToBuffer(value, memory);

            DisplayBufferToConsole(owner.Memory.Slice(0, value.ToString().Length));
        }
        catch (FormatException)
        {
            Console.WriteLine("You did not enter a valid number.");
        }
        catch (OverflowException)
        {
            Console.WriteLine($"You entered a number less than {Int32.MinValue:N0} or greater than {Int32.MaxValue:N0}.");
        }
        finally
        {
            owner?.Dispose();
        }
    }

    static void WriteInt32ToBuffer(int value, Memory<char> buffer)
    {
        var strValue = value.ToString();

        var span = buffer.Span;
        for (int ctr = 0; ctr < strValue.Length; ctr++)
            span[ctr] = strValue[ctr];
    }

    static void DisplayBufferToConsole(Memory<char> buffer) =>
        Console.WriteLine($"Contents of the buffer: '{buffer}'");
}

Bu örneği using açıklamasıyla da yazabilirsiniz:

using System;
using System.Buffers;

class Example
{
    static void Main()
    {
        using (IMemoryOwner<char> owner = MemoryPool<char>.Shared.Rent())
        {
            Console.Write("Enter a number: ");
            try
            {
                string? s = Console.ReadLine();

                if (s is null)
                    return;

                var value = Int32.Parse(s);

                var memory = owner.Memory;
                WriteInt32ToBuffer(value, memory);
                DisplayBufferToConsole(memory.Slice(0, value.ToString().Length));
            }
            catch (FormatException)
            {
                Console.WriteLine("You did not enter a valid number.");
            }
            catch (OverflowException)
            {
                Console.WriteLine($"You entered a number less than {Int32.MinValue:N0} or greater than {Int32.MaxValue:N0}.");
            }
        }
    }

    static void WriteInt32ToBuffer(int value, Memory<char> buffer)
    {
        var strValue = value.ToString();

        var span = buffer.Slice(0, strValue.Length).Span;
        strValue.AsSpan().CopyTo(span);
    }

    static void DisplayBufferToConsole(Memory<char> buffer) =>
        Console.WriteLine($"Contents of the buffer: '{buffer}'");
}

Bu kodda:

  • Main yöntemi IMemoryOwner<T> örneğine referansı tuttuğundan, Main yöntemi arabelleğin sahibidir.
  • WriteInt32ToBuffer ve DisplayBufferToConsole yöntemleri genel API olarak kabul Memory<T> eder. Bu nedenle, arabellek tüketicileridir. Bu yöntemler, arabelleği birer birer kullanır.

WriteInt32ToBuffer yöntemi arabelleğe bir değer yazmak için tasarlanmış olsa da, DisplayBufferToConsole yöntemi için bu amaçlanmamıştır. Bunu yansıtmak için ReadOnlyMemory<T> türünde bir bağımsız değişken kabul edebilirdi. Daha fazla bilgi için ReadOnlyMemory<T>hakkında, Kural #2: Arabelleğin salt okunur olması gerekiyorsa ReadOnlySpan<T> veya ReadOnlyMemory<T> kullanın bölümüne bakın.

"Sahipsiz" Bellek<T> örnekleri

Bir Memory<T> örnek IMemoryOwner<T> kullanmadan oluşturabilirsiniz. Bu durumda, arabellek sahipliği açık değil örtüktür ve yalnızca tek sahipli model desteklenir. Bunu şu şekilde yapabilirsiniz:

  • Oluşturuculardan Memory<T> birini doğrudan çağırarak, aşağıdaki örnekte olduğu gibi bir T[]geçirin.
  • Örnek oluşturmak için ReadOnlyMemory<char> uzantısı yöntemini çağırma.
using System;

class Example
{
    static void Main()
    {
        Memory<char> memory = new char[64];

        Console.Write("Enter a number: ");
        string? s = Console.ReadLine();

        if (s is null)
            return;

        var value = Int32.Parse(s);

        WriteInt32ToBuffer(value, memory);
        DisplayBufferToConsole(memory);
    }

    static void WriteInt32ToBuffer(int value, Memory<char> buffer)
    {
        var strValue = value.ToString();
        strValue.AsSpan().CopyTo(buffer.Slice(0, strValue.Length).Span);
    }

    static void DisplayBufferToConsole(Memory<char> buffer) =>
        Console.WriteLine($"Contents of the buffer: '{buffer}'");
}

Başlangıçta Memory<T> örneğini oluşturan yöntem, arabelleğin örtük sahibidir. Aktarımı kolaylaştıracak bir örnek olmadığından IMemoryOwner<T> sahiplik başka bir bileşene aktarılamaz. (Alternatif olarak, çalışma zamanının çöp toplayıcısının arabelleğe sahip olduğunu ve tüm yöntemlerin yalnızca arabelleği kullandığını düşünebilirsiniz.)

Kullanım yönergeleri

Bellek bloğu birden fazla bileşene devredilmek üzere sahiplenilir ve bazı bileşenler belirli bir bellek bloğu üzerinde aynı anda çalışabilir. Bu nedenle, hem Memory<T> hem de Span<T> kullanımına yönelik yönergeler oluşturmak önemlidir. Bir bileşenin şunlara sahip olması mümkün olduğundan yönergeler gereklidir:

  • Sahibi serbest bıraktıktan sonra bir bellek bloğuna referansı tutun.
  • Başka bir bileşenin de çalıştığı bir zamanda, bir arabellekte işlem yaparak veri bozulmasına neden olmak.

Bellek bloğu üzerinde çalışmak için Span<T> tercih edilen tür yapıyorken, yığına ayrılan yapısı Span<T> performansı optimize eder, ancak bu durum Span<T>'yi bazı önemli kısıtlamalara da tabi kılar. Span<T>'ın ne zaman ve Memory<T>'in ne zaman kullanılacağını bilmek önemlidir.

Aşağıda, Memory<T> ve ilgili türlerini başarıyla kullanmaya yönelik önerilerimiz verilmiştir. Aksi belirtilmedikçe, Memory<T> ve Span<T> için geçerli olan yönergeler, ReadOnlyMemory<T> ve ReadOnlySpan<T> için de geçerlidir.

Kural 1: Zaman uyumlu api için mümkünse parametre olarak Bellek<T yerine Span>< T> kullanın

Span<T>, Memory<T>'den daha çok yönlüdür ve daha geniş bir çeşitlilikteki bitişik bellek arabelleklerini temsil edebilir. Span<T> ayrıca değerinden Memory<T>daha iyi performans sunar. Son olarak, bir Memory<T>.Span örneğini Memory<T> öğesine dönüştürmek için Span<T> özelliğini kullanabilirsiniz, ancak Span<T>'den Memory<T>'ye dönüşüm mümkün değildir. Bu nedenle çağıranlarınızın bir Memory<T> örneği varsa, yöntemlerinizi Span<T> parametreleriyle zaten çağırabilecekler.

Tür Span<T> yerine tür Memory<T> parametresi kullanmak, doğru bir tüketen yöntem uygulaması yazmanıza da yardımcı olur. Yönteminizin tanımlı sınırlarının ötesinde arabelleğe erişmeye çalışmadığınızdan emin olmak için otomatik olarak olan derleme zamanı denetimleri alırsınız (bu konuda daha sonra daha fazla bilgi verilecektir).

Bazen, tamamen senkron bile olsanız, Memory<T> parametresi yerine Span<T> parametresini kullanmanız gerekebilir. Belki de bağımlı olduğunuz bir API yalnızca Memory<T> bağımsız değişkenleri kabul eder. Bu iyi, ancak eşzamanlı kullanırken Memory<T> söz konusu olan ödünleri unutmayın.

Kural 2: Bu arabelleğin salt okunur olması gerekiyorsa ReadOnlySpan<T> veya ReadOnlyMemory<T> kullanmalısınız.

Önceki örneklerde DisplayBufferToConsole yöntemi yalnızca arabellekten okur; arabelleğin içeriğini değiştirmez. Yöntem imzası aşağıdaki şekilde değiştirilmelidir.

void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);

Aslında, bu kuralı ve Kural #1'i birleştirirseniz, daha da iyisini yapabilir ve yöntem imzasını aşağıdaki gibi yeniden yazabiliriz:

void DisplayBufferToConsole(ReadOnlySpan<char> buffer);

DisplayBufferToConsole yöntemi artık sanal olarak hayal edilebilen her arabellek türüyle çalışır: T[], stackalloc ile ayrılan depolama alanı vb. Hatta doğrudan içine bir String geçirebilirsiniz! Daha fazla bilgi için bkz. GitHub sorunu dotnet/docs #25551.

Kural 3: Yönteminiz Bellek<T'yi> kabul eder ve döndürürse void, yönteminiz döndürdüğünde Bellek<T> örneğini kullanmamalısınız

Bu, daha önce bahsedilen "kira" kavramıyla ilgilidir. Void döndüren bir metodun örnek üzerindeki Memory<T> kirası, metoda girildiğinde başlar ve metoddan çıkıldığında sona erer. Konsoldan gelen girişe göre bir döngüde Log çağrısı yapan aşağıdaki örneği düşünün.

// <Snippet1>
using System;
using System.Buffers;

public class Example
{
    // implementation provided by third party
    static extern void Log(ReadOnlyMemory<char> message);

    // user code
    public static void Main()
    {
        using (var owner = MemoryPool<char>.Shared.Rent())
        {
            var memory = owner.Memory;
            var span = memory.Span;
            while (true)
            {
                string? s = Console.ReadLine();

                if (s is null)
                    return;

                int value = Int32.Parse(s);
                if (value < 0)
                    return;

                int numCharsWritten = ToBuffer(value, span);
                Log(memory.Slice(0, numCharsWritten));
            }
        }
    }

    private static int ToBuffer(int value, Span<char> span)
    {
        string strValue = value.ToString();
        int length = strValue.Length;
        strValue.AsSpan().CopyTo(span.Slice(0, length));
        return length;
    }
}
// </Snippet1>

// Possible implementation of Log:
    // private static void Log(ReadOnlyMemory<char> message)
    // {
    //     Console.WriteLine(message);
    // }

Tam olarak zaman uyumlu bir yöntemse Log , herhangi bir zamanda bellek örneğinin yalnızca bir etkin tüketicisi olduğundan bu kod beklendiği gibi davranır. Ancak bunun yerine Log'in şu şekilde bir uygulaması olduğunu hayal edin.

// !!! INCORRECT IMPLEMENTATION !!!
static void Log(ReadOnlyMemory<char> message)
{
    // Run in background so that we don't block the main thread while performing IO.
    Task.Run(() =>
    {
        StreamWriter sw = File.AppendText(@".\input-numbers.dat");
        sw.WriteLine(message);
    });
}

Bu uygulamada, özgün yöntem döndürdükten sonra yine de arka planda Log örneğini kullanmaya çalışarak Memory<T> kirasını ihlal eder. Main yöntemi, Log arabellekten veri okumaya çalışırken arabelleği değiştirebilir ve bu da veri bozulmasına neden olabilir.

Bunu çözmenin birkaç yolu vardır:

  • Log yöntemi, Task yönteminin aşağıdaki uygulamasında olduğu gibi, void yerine bir Log döndürebilir.

    // An acceptable implementation.
    static Task Log(ReadOnlyMemory<char> message)
    {
        // Run in the background so that we don't block the main thread while performing IO.
        return Task.Run(() => {
            StreamWriter sw = File.AppendText(@".\input-numbers.dat");
            sw.WriteLine(message);
            sw.Flush();
        });
    }
    
  • Log bunun yerine aşağıdaki gibi uygulanabilir:

    // An acceptable implementation.
    static void Log(ReadOnlyMemory<char> message)
    {
        string defensiveCopy = message.ToString();
        // Run in the background so that we don't block the main thread while performing IO.
        Task.Run(() =>
        {
            StreamWriter sw = File.AppendText(@".\input-numbers.dat");
            sw.WriteLine(defensiveCopy);
            sw.Flush();
        });
    }
    

Kural 4: Yönteminiz Bir Bellek<T'sini> kabul ederse ve görev döndürüyorsa, Görev terminal durumuna geçtikten sonra Bellek<T> örneğini kullanmamalısınız

Bu yalnızca Kural #3'ün asenkron varyantıdır. Önceki Log örnekte yer alan yöntem, bu kurala uymak için aşağıdaki gibi yazılabilir:

// An acceptable implementation.
static Task Log(ReadOnlyMemory<char> message)
{
    // Run in the background so that we don't block the main thread while performing IO.
    return Task.Run(() => {
        StreamWriter sw = File.AppendText(@".\input-numbers.dat");
        sw.WriteLine(message);
        sw.Flush();
    });
}

Burada "terminal durumu", görevin tamamlanmış, hatalı veya iptal edilmiş bir duruma geçiş yaptığı anlamına gelir. Başka bir deyişle, "terminal durumu" "yürütmeye veya yürütmeye devam etmeye neden olabilecek her şey" anlamına gelir.

Bu kılavuz, Task, Task<TResult>, ValueTask<TResult> veya benzer bir tür döndüren yöntemler için geçerlidir.

Kural 5: Oluşturucunuz Parametre olarak Bellek<T'yi> kabul ederse, oluşturulan nesnedeki örnek yöntemlerinin Bellek<T> örneğinin tüketicileri olduğu varsayılır

Aşağıdaki örneği göz önünde bulundurun:

class OddValueExtractor
{
    public OddValueExtractor(ReadOnlyMemory<int> input);
    public bool TryReadNextOddValue(out int value);
}

void PrintAllOddValues(ReadOnlyMemory<int> input)
{
    var extractor = new OddValueExtractor(input);
    while (extractor.TryReadNextOddValue(out int value))
    {
      Console.WriteLine(value);
    }
}

Burada oluşturucu OddValueExtractor bir ReadOnlyMemory<int> oluşturucu parametresi olarak kabul eder, bu nedenle oluşturucunun kendisi örneğin tüketicisi ReadOnlyMemory<int> olur ve döndürülen değerdeki tüm örnek yöntemleri de özgün ReadOnlyMemory<int> örneğin tüketicileridir. Bu, örneğin TryReadNextOddValueReadOnlyMemory<int> yöntemine doğrudan aktarılmamasına rağmen TryReadNextOddValue tarafından tüketildiği anlamına gelir.

Kural 6: Türünüzde ayarlanabilir bir Memory<T-typed> özelliği (veya eşdeğer bir örnek yöntemi) varsa, bu nesnedeki örnek yöntemlerinin Bellek<T> örneğinin tüketicileri olduğu varsayılır

Bu, kural 5'in yalnızca bir çeşididir. Bu kural, özellik ayarlayıcılarının veya eşdeğer yöntemlerin girişlerini yakalayıp kalıcı hale getirmek için varsayıldığından, aynı nesnedeki örnek yöntemleri yakalanan durumu kullanabilir.

Aşağıdaki örnek bu kuralı tetikler:

class Person
{
    // Settable property.
    public Memory<char> FirstName { get; set; }

    // alternatively, equivalent "setter" method
    public SetFirstName(Memory<char> value);

    // alternatively, a public settable field
    public Memory<char> FirstName;
}

Kural 7: IMemoryOwner<T> referansınız varsa, bir noktada bu referansın yok edilmesi veya sahipliğini aktarmanız zorundasınız (ikisini birden değil)

Bir Memory<T> örneği, yönetilen veya yönetilmeyen bellek tarafından desteklenebileceğinden, örnek üzerinde yapılan çalışma tamamlandığında, sahibinin Dispose üzerinde IMemoryOwner<T> çağrısını yapması gerekir. Alternatif olarak, sahibi, IMemoryOwner<T> örneğinin sahipliğini farklı bir bileşene aktarabilir; bu durumda, alan bileşen uygun zamanda Dispose çağırmaktan sorumlu olur (bu konu hakkında daha fazla bilgi verilecektir).

Dispose örneğinde IMemoryOwner<T> yönteminin çağrılmaması, yönetilmeyen bellek sızıntılarına veya diğer performans düşüşlerine neden olabilir.

Bu kural, gibi MemoryPool<T>.Rentfabrika yöntemlerini çağıran kod için de geçerlidir. Çağıran, döndürülen IMemoryOwner<T>'in sahibi olur ve işi bittiğinde örneği yok etmekten sorumludur.

Kural 8: API yüzeyinizde bir IMemoryOwner<T> parametresi varsa bu örneğin sahipliğini kabul etmiş olursunuz

Bu tür bir örneği kabul etmek, bileşeninizin bu örneğin sahipliğini almayı amaçladığını gösterir. Bileşeniniz Kural 7'ye göre uygun şekilde imha edilmesinden sorumlu hale gelir.

Örneğin sahipliğini IMemoryOwner<T> farklı bir bileşene aktaran bileşenler, yöntem çağrısı tamamlandıktan sonra artık bu örneği kullanmamalıdır.

Önemli

Oluşturucunuz IMemoryOwner<T> bir parametre olarak kabul ederse, türü IDisposable arayüzünü uygulamalıdır ve Dispose yöntemi Dispose nesnesi üzerinde IMemoryOwner<T> çağırmalıdır.

Kural 9: Zaman uyumlu p/invoke yöntemini sarmalarsanız, API'niz Span<T'yi> parametre olarak kabul etmelidir

Kural 1'e göre, Span<T> genellikle zaman uyumlu API'ler için kullanılacak doğru türdür. Anahtar sözcüğü olan Span<T>'yi kullanarak fixed örneklerini aşağıdaki örnekte olduğu gibi sabitleyebilirsiniz.

using System.Runtime.InteropServices;

[DllImport(...)]
private static extern unsafe int ExportedMethod(byte* pbData, int cbData);

public unsafe int ManagedWrapper(Span<byte> data)
{
    fixed (byte* pbData = &MemoryMarshal.GetReference(data))
    {
        int retVal = ExportedMethod(pbData, data.Length);

        /* error checking retVal goes here */

        return retVal;
    }
}

Önceki örnekte, pbData örneğin giriş aralığı boşsa null olabilir. Dışa aktarılan yöntem, pbData 0 olsa bile cbData'ın kesinlikle null olmaması gerekiyorsa, yöntem aşağıdaki gibi uygulanabilir:

public unsafe int ManagedWrapper(Span<byte> data)
{
    fixed (byte* pbData = &MemoryMarshal.GetReference(data))
    {
        byte dummy = 0;
        int retVal = ExportedMethod((pbData != null) ? pbData : &dummy, data.Length);

        /* error checking retVal goes here */

        return retVal;
    }
}

Kural 10: Zaman uyumsuz p/invoke yöntemini sarmalarsanız, API'niz parametre olarak Bellek<T'yi> kabul etmelidir

Asenkron işlemler arasında fixed anahtar sözcüğünü kullanamadığınız için, Memory<T>.Pin örneklerini, bu örneğin temsil ettiği bitişik bellek türünden bağımsız olarak sabitlemek için Memory<T> yöntemini kullanırsınız. Aşağıdaki örnekte, zaman uyumsuz p/invoke çağrısı gerçekleştirmek için bu API'nin nasıl kullanılacağı gösterilmektedir.

using System.Runtime.InteropServices;

[UnmanagedFunctionPointer(...)]
private delegate void OnCompletedCallback(IntPtr state, int result);

[DllImport(...)]
private static extern unsafe int ExportedAsyncMethod(byte* pbData, int cbData, IntPtr pState, IntPtr lpfnOnCompletedCallback);

private static readonly IntPtr _callbackPtr = GetCompletionCallbackPointer();

public unsafe Task<int> ManagedWrapperAsync(Memory<byte> data)
{
    // setup
    var tcs = new TaskCompletionSource<int>();
    var state = new MyCompletedCallbackState
    {
        Tcs = tcs
    };
    var pState = (IntPtr)GCHandle.Alloc(state);

    var memoryHandle = data.Pin();
    state.MemoryHandle = memoryHandle;

    // make the call
    int result;
    try
    {
        result = ExportedAsyncMethod((byte*)memoryHandle.Pointer, data.Length, pState, _callbackPtr);
    }
    catch
    {
        ((GCHandle)pState).Free(); // cleanup since callback won't be invoked
        memoryHandle.Dispose();
        throw;
    }

    if (result != PENDING)
    {
        // Operation completed synchronously; invoke callback manually
        // for result processing and cleanup.
        MyCompletedCallbackImplementation(pState, result);
    }

    return tcs.Task;
}

private static void MyCompletedCallbackImplementation(IntPtr state, int result)
{
    GCHandle handle = (GCHandle)state;
    var actualState = (MyCompletedCallbackState)(handle.Target);
    handle.Free();
    actualState.MemoryHandle.Dispose();

    /* error checking result goes here */

    if (error)
    {
        actualState.Tcs.SetException(...);
    }
    else
    {
        actualState.Tcs.SetResult(result);
    }
}

private static IntPtr GetCompletionCallbackPointer()
{
    OnCompletedCallback callback = MyCompletedCallbackImplementation;
    GCHandle.Alloc(callback); // keep alive for lifetime of application
    return Marshal.GetFunctionPointerForDelegate(callback);
}

private class MyCompletedCallbackState
{
    public TaskCompletionSource<int> Tcs;
    public MemoryHandle MemoryHandle;
}

Ayrıca bkz.