Leitfaden zur Verwendung von Memory<T> und Span<T>

.NET enthält eine Reihe von Typen, die einen beliebigen zusammenhängenden Bereich im Arbeitsspeicher darstellen. Span<T> und ReadOnlySpan<T> sind einfache Arbeitsspeicherpuffer, die Verweise auf verwalteten oder nicht verwalteten Arbeitsspeicher umschließen. Da diese Typen nur im Stapel gespeichert werden können, sind sie für Szenarien wie asynchrone Methodenaufrufe ungeeignet. Um dieses Problem zu beheben, wurden in .NET 2.1 einige zusätzliche Typen hinzugefügt, darunter Memory<T>, ReadOnlyMemory<T>, IMemoryOwner<T> und MemoryPool<T>. Wie Span<T> kann auch Memory<T> und die verwandten Typen durch verwalteten und nicht verwalteten Speicher gesichert werden. Im Gegensatz zu Span<T> kann Memory<T> im verwalteten Heap gespeichert werden.

Sowohl Span<T> als auch Memory<T> sind Wrapper über Puffer von strukturierten Daten, die in Pipelines verwendet werden können. Das heißt, sie sind so konzipiert, dass einige oder alle Daten effizient an Komponenten in der Pipeline übergeben werden können, die sie verarbeiten und optional den Puffer modifizieren können. Da Memory<T> und die zugehörigen Typen von mehreren Komponenten oder von mehreren Threads aufgerufen werden können, ist es wichtig, einige Standardnutzungsrichtlinien zu befolgen, um robusten Code zu generieren.

Besitzer, Consumer und Verwaltung der Lebensdauer

Puffer können zwischen APIs ausgetauscht werden, und manchmal wird von mehreren Threads auf Puffer zugegriffen. Daher ist es wichtig, die Lebensdauerverwaltung eines Puffers zu berücksichtigen. Es gibt drei grundlegende Konzepte:

  • Besitz. Der Besitzer einer Pufferinstanz ist für das die Verwaltung der Lebensdauer verantwortlich, einschließlich der Zerstörung des Puffers, wenn er nicht mehr in Gebrauch ist. Alle Puffer haben nur einen einzigen Besitzer. Im Allgemeinen ist der Besitzer die Komponente, die den Puffer erstellt hat, oder die den Puffer von einer Factory erhalten hat. Der Besitz kann auch übertragen werden; Komponente-A kann die Kontrolle über den Puffer an Komponente-B abgeben, wobei Komponente-A den Puffer nicht mehr verwenden darf, und Komponente-B wird für die Zerstörung des Puffers verantwortlich, wenn er nicht mehr verwendet wird.

  • Verbrauch. Der Consumer einer Pufferinstanz darf die Pufferinstanz nutzen, indem er aus ihr liest und eventuell in sie schreibt. Puffer können jeweils einen Consumer haben, es sei denn, es ist ein externer Synchronisationsmechanismus vorgesehen. Der aktive Consumer eines Puffers ist nicht unbedingt der Besitzer des Puffers.

  • Leasedauer. Die Leasedauer ist die Zeitspanne, in der eine bestimmte Komponente als Consumer des Puffers zugelassen wird.

Im folgenden Pseudocodebeispiel werden diese drei Konzepte veranschaulicht. Buffer im Pseudocode stellt einen Memory<T>- oder Span<T>-Puffer vom Typ Chardar. Die Main-Methode instanziiert den Puffer, ruft die WriteInt32ToBuffer-Methode auf, um die Zeichenfolgendarstellung einer ganzen Zahl in den Puffer zu schreiben, und ruft dann die DisplayBufferToConsole-Methode auf, um den Wert des Puffers anzuzeigen.

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

Die Main-Methode erstellt den Puffer und ist somit sein Besitzer. Daher ist Main dafür verantwortlich, den Puffer zu zerstören, wenn er nicht mehr verwendet wird. Der Pseudocode veranschaulicht dies durch Aufrufen einer Destroy-Methode für den Puffer. (Weder Memory<T> noch Span<T> verfügt tatsächlich über eine Destroy-Methode. Die tatsächlichen Codebeispiele werden weiter unten in diesem Artikel angezeigt.)

Der Puffer hat zwei Consumer: WriteInt32ToBuffer und DisplayBufferToConsole. Es gibt immer nur jeweils einen Consumer (zuerst WriteInt32ToBuffer, dann DisplayBufferToConsole), und keiner der Consumer besitzt den Puffer. Beachten Sie auch, dass „Consumer“ in diesem Zusammenhang keine schreibgeschützte Ansicht des Puffers impliziert; Consumer können den Inhalt des Puffers ändern, wie WriteInt32ToBuffer, wenn sie eine Lese-/Schreibansicht des Puffers erhalten.

Die WriteInt32ToBuffer-Methode verfügt über eine Leasedauer (kann den Puffer zwischen dem Start des Methodenaufrufs und der Zeit, die die Methode zurückgibt, verbrauchen). Ebenso hat DisplayBufferToConsole eine Leasingdauer für den Puffer, während er ausgeführt wird, und die Leasedauer wird freigegeben, wenn die Methode entladen wird. (Es gibt keine API für die Leaseverwaltung; eine „Leasedauer“ ist nur ein Konzept.)

Memory<T> und das Besitzer/Consumer-Modell

Wie im Abschnitt Besitzer, Consumer und Verwaltung der Lebensdauer beschrieben, hat ein Puffer immer einen Besitzer. .NET unterstützt zwei Besitzmodelle:

  • Ein Modell, das alleinigen Besitz unterstützt. Ein Puffer hat einen einzigen Besitzer für seine gesamte Lebensdauer.

  • Ein Modell, das die Besitzübertragung unterstützt. Der Besitz eines Puffers kann von seinem ursprünglichen Besitzer (seinem Ersteller) auf eine andere Komponente übertragen werden, die dann für die Lebensdauerverwaltung des Puffers verantwortlich ist. Dieser Besitzer kann seinerseits den Besitz auf eine andere Komponente übertragen, und so weiter.

Sie verwenden die System.Buffers.IMemoryOwner<T>-Schnittstelle, um den Besitz eines Puffers explizit zu verwalten. IMemoryOwner<T> unterstützt beide Besitzmodelle. Die Komponente, die einen Verweis IMemoryOwner<T> hat, besitzt den Puffer. Das folgende Beispiel verwendet eine IMemoryOwner<T>-Instanz, um den Besitz eines Memory<T>-Puffers anzugeben.

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}'");
}

Dieses Beispiel kann auch mit der using-Anweisung geschrieben werden:

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}'");
}

In diesem Code:

  • Die Main-Methode enthält den Verweis auf die IMemoryOwner<T>-Instanz, sodass die Main-Methode der Besitzer des Puffers ist.

  • Die WriteInt32ToBuffer- und DisplayBufferToConsole-Methode akzeptieren Memory<T> als öffentliche API. Daher sind sie der Consumer des Puffers. Diese Methoden nutzen den Puffer jeweils einzeln.

Die WriteInt32ToBuffer-Methode dient dazu, einen Wert in den Puffer zu schreiben. Bei der DisplayBufferToConsole-Methode ist dies dagegen nicht der Fall. Um dies anzugeben, hätte sie ein Argument vom Typ ReadOnlyMemory<T> akzeptieren können. Weitere Informationen zu ReadOnlyMemory<T> finden Sie unter Regel 2: ReadOnlySpan<T> oder ReadOnlyMemory<T> verwenden, wenn der Puffer schreibgeschützt sein soll.

Memory<T>-Instanzen ohne Besitzer

Sie können einen Memory<T>-Instanz ohne die Verwendung von IMemoryOwner<T> erstellen. In diesem Fall ist der Besitz des Puffers eher implizit als explizit, und es wird nur das Einzelbesitzermodell unterstützt. Gehen Sie dafür so vor:

  • Direkter Aufruf eines der Memory<T>-Konstruktoren, Übergabe eines T[], wie im folgenden Beispiel.

  • Aufrufen der String.AsMemory-Erweiterungsmethode zur Erstellung einer ReadOnlyMemory<char>-Instanz.

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}'");
}

Die Methode, die die Memory<T>-Instanz initial erstellt, ist der implizite Besitzer des Puffers. Der Besitz kann nicht auf eine andere Komponente übertragen werden, da es keine IMemoryOwner<T>-Instanz gibt, die die Übertragung vereinfacht. (Alternativ ist es auch vorstellbar, dass der Garbage Collector der Runtime den Puffer besitzt und alle Methoden nur den Puffer verbrauchen.)

Verwendungsrichtlinien

Da ein Arbeitsspeicherblock einen Besitzer hat, aber an mehrere Komponenten übergeben werden soll, von denen einige gleichzeitig in einem bestimmten Arbeitsspeicherblock aktiv sein können, ist es wichtig, Richtlinien für die Verwendung von Memory<T> und Span<T> festzulegen. Richtlinien sind erforderlich, da für eine Komponente Folgendes möglich ist:

  • Behalten eines Verweises auf einen Arbeitsspeicherblock, nachdem sein Besitzer ihn freigegeben hat

  • Ausführen von Aktionen in einem Puffer, während gleichzeitig eine andere Komponente Aktionen in dem Puffer ausführt, was die Beschädigung der Daten im Puffer zur Folge hat

  • Durch die Stapelzuordnung von Span<T> wird zwar die Leistung optimiert und Span<T> zum bevorzugten Typ für den Betrieb auf einem Speicherblock, aber auch Span<T> einigen bedeutenden Einschränkungen unterworfen. Es ist wichtig zu wissen, wann Span<T> und wann Memory<T> verwendet werden muss.

Nachfolgend finden Sie unsere Empfehlungen für den erfolgreichen Einsatz von Memory<T> und den verwandten Typen. Die Richtlinien, die für Memory<T> und Span<T> gelten, gelten auch für ReadOnlyMemory<T> und ReadOnlySpan<T>, sofern nicht anders angegeben.

Regel 1: Für eine synchrone API Span<T> anstelle von Memory<T> als Parameter verwenden, wenn möglich.

Span<T> ist vielseitiger als Memory<T> und kann eine größere Anzahl von zusammenhängenden Speicherpuffern darstellen. Span<T> bietet außerdem eine bessere Leistung als Memory<T>. Schließlich können Sie die Memory<T>.Span-Eigenschaft verwenden, um eine Memory<T>-Instanz in eine Span<T>-Instanz zu konvertieren, obwohl eine Span<T>-to-Memory<T>-Konvertierung nicht möglich ist. Wenn Ihre Aufrufer also eine Memory<T>-Instanz haben, können sie Ihre Methoden trotzdem mit Span<T>-Parametern aufrufen.

Die Verwendung eines Parameters vom Typ Span<T> anstelle von Typ Memory<T> unterstützt Sie auch dabei, eine korrekte Implementierung einer konsumierenden Methode zu schreiben. Die Kompilierzeit wird automatisch überprüft, um sicherzustellen, dass Sie nicht versuche, nach Ablauf der Leasedauer auf den Puffer zuzugreifen (weitere Informationen dazu im weiteren Verlauf).

Manchmal müssen Sie einen Memory<T>-Parameter anstelle eines Span<T>-Parameters verwenden, auch wenn Sie vollständig synchron sind. Vielleicht akzeptiert eine API, von der Sie abhängen, nur Memory<T>-Argumente. Dies ist in Ordnung, aber beachten Sie die Vor- und Nachteile, die bei der synchronen Verwendung von Memory<T> auftreten.

Regel 2: ReadOnlySpan<T> oder ReadOnlyMemory<T> verwenden, wenn der Puffer schreibgeschützt sein soll.

In den früheren Beispielen liest die DisplayBufferToConsole-Methode nur aus dem Puffer und ändert den Inhalt des Puffers nicht. Die Methodensignatur sollte wie folgt geändert werden.

void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);

Wenn wir diese Regel und Regel 1 kombinieren, können wir die Methodensignatur wie folgt umschreiben:

void DisplayBufferToConsole(ReadOnlySpan<char> buffer);

Die DisplayBufferToConsole-Methode funktioniert nun mit praktisch jedem denkbaren Puffertyp: T[], Speicher, der mit stackalloc zugewiesen wurde, und so weiter. Sie können einen String sogar direkt übergeben! Weitere Informationen finden Sie unter dem GitHub-Issue dotnet/docs #25551.

Regel 3: Wenn Ihre Methode Memory<T> akzeptiert und void zurückgibt, dürfen Sie die Memory<T>-Instanz nicht verwenden, nachdem Ihre Methode ein Ergebnis zurückgegeben hat.

Dies bezieht sich auf das bereits erwähnte Konzept der „Leasedauer“. Die Leasedauer für eine Methode auf der Memory<T>-Instanz, die „void“ zurückgibt, beginnt, wenn die Methode aufgerufen wird, und endet, wenn die Methode beendet wird. Schauen Sie sich das folgende Beispiel an, in dem basierend auf der Eingabe von der Konsole ein Log aufgerufen wird.

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

Wenn Log eine vollständig synchrone Methode ist, verhält sich dieser Code wie erwartet, da es zu einem bestimmten Zeitpunkt nur einen aktiven Verbraucher der Speicherinstanz gibt. Aber stellen Sie sich vor, Log hat stattdessen diese Implementierung.

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

In dieser Implementierung verstößt Log gegen die Leasedauer, weil immer noch versucht wird, die Memory<T>-Instanz im Hintergrund zu verwenden, nachdem die ursprüngliche Methode zurückgegeben wurde. Die Main-Methode könnte den Puffer verändern, während Log versucht, aus ihm zu lesen. Das könnte zu einer Datenbeschädigung führen.

Es gibt mehrere Möglichkeiten, dieses Problem zu lösen:

  • Die Log-Methode kann Task anstelle von void zurückgeben, wie es bei der folgenden Implementierung der Log-Methode der Fall ist.

    // 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 kann stattdessen wie folgt implementiert werden:

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

Regel 4: Wenn Ihre Methode Memory<T> akzeptiert und einen Task zurückgibt, dürfen Sie die Memory<T>-Instanz nicht verwenden, nachdem der Task in einen Endzustand übergeht.

Dies ist nur die asynchrone Variante von Regel 3. Die Log-Methode aus dem früheren Beispiel kann wie folgt geschrieben werden, um dieser Regel zu entsprechen:

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

Hier bedeutet „Endzustand“, dass der Task in einen abgeschlossenen, fehlerhaften oder abgebrochenen Zustand übergeht. Anders ausgedrückt bedeutet „Endzustand“ „alles, was das Warten auf das Auslösen oder Fortsetzen der Ausführung verursachen würde“.

Diese Anleitung gilt für Methoden, die Task, Task<TResult>, ValueTask<TResult> oder einen ähnlichen Typen zurückgeben würden.

Regel 5: Wenn Ihr Konstruktor Memory<T> als Parameter akzeptiert, werden Instanzmethoden auf dem konstruierten Objekt als Consumer der Memory<T>-Instanz angenommen.

Betrachten Sie das folgende Beispiel:

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

Hier akzeptiert der OddValueExtractor-Konstruktor ReadOnlyMemory<int> als Konstruktorparameter, sodass der Konstruktor selbst ein Consumer der ReadOnlyMemory<int>-Instanz ist, und alle Instanzmethoden auf dem zurückgegebenen Wert sind auch Consumer der ursprünglichen ReadOnlyMemory<int>-Instanz. Das bedeutet, dass TryReadNextOddValue die ReadOnlyMemory<int>-Instanz verbraucht, obwohl die Instanz nicht direkt an die TryReadNextOddValue-Methode übergeben wird.

Regel 6: Wenn Sie eine einstellbare Memory<T>-typisierte Eigenschaft (oder eine gleichwertige Instanzmethode) für Ihren Typ haben, werden Instanzmethoden auf diesem Objekt als Consumer der Memory<T>-Instanz angenommen.

Dies ist eigentlich nur eine Variante der Regel 5. Diese Regel existiert, weil davon ausgegangen wird, dass Property Setter oder gleichwertige Methoden ihre Eingaben erfassen und beibehalten, sodass Instanzmethoden auf demselben Objekt den erfassten Zustand verwenden können.

Das folgende Beispiel löst diese Regel aus:

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

Regel 7: Wenn Sie einen IMemoryOwner<T> Verweis haben, müssen Sie ihn irgendwann verwerfen oder den Besitz übertragen (aber nicht beides).

Da eine Memory<T>-Instanz entweder durch verwalteten oder nicht verwalteten Speicher gesichert werden kann, muss der Besitzer Dispose für IMemoryOwner<T> aufrufen, wenn die Arbeit an der Memory<T>-Instanz abgeschlossen ist. Alternativ kann der Besitzer die Besitzrechte an der IMemoryOwner<T>-Instanz auf eine andere Komponente übertragen, wobei die erwerbende Komponente dann für den Aufruf von Dispose zum gegebenen Zeitpunkt (mehr dazu später) verantwortlich ist.

Wenn die Dispose-Methode nicht für eine IMemoryOwner<T>-Instanz aufgerufen wird, kann dies zu nicht verwalteten Arbeitsspeicherverlusten oder anderen Leistungsbeeinträchtigungen führen.

Diese Regel gilt auch für Code, der Factorymethoden wie MemoryPool<T>.Rent aufruft. Der Aufrufer wird zum Besitzer des zurückgegebenen IMemoryOwner<T> und ist verantwortlich für das Verwerfen der Instanz, wenn sie beendet ist.

Regel 8: Wenn Sie einen IMemoryOwner<T>-Parameter in Ihrer API-Oberfläche haben, akzeptieren Sie den Besitz dieser Instanz.

Die Annahme einer Instanz dieses Typs signalisiert, dass Ihre Komponente beabsichtigt, den Besitz dieser Instanz zu übernehmen. Ihre Komponente ist für das ordnungsgemäße Verwerfen gemäß Regel 7 verantwortlich.

Jede Komponente, die den Besitz der IMemoryOwner<T>-Instanz auf eine andere Komponente überträgt, sollte diese Instanz nach Abschluss des Methodenaufrufs nicht mehr verwenden.

Wichtig

Wenn Ihr Konstruktor IMemoryOwner<T> als Parameter akzeptiert, sollte sein Typ IDisposable implementieren, und Ihre Dispose-Methode sollte Dispose für das IMemoryOwner<T>-Objekt aufrufen.

Regel 9: Wenn Sie eine synchrone P/Invoke-Methode umschließen, sollte Ihre API Span<T> als Parameter akzeptieren.

Gemäß Regel 1 ist Span<T> im Allgemeinen der richtige Typ für synchrone APIs. Sie können Span<T>-Instanzen über das Schlüsselwort fixed anheften, wie im folgenden Beispiel.

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

Im vorherigen Beispiel kann pbData Null sein, wenn beispielsweise der Eingangs-Span-Wert leer ist. Wenn die exportierte Methode unbedingt erfordert, dass pbData nicht Null ist, auch wenn cbData 0 ist, kann die Methode wie folgt implementiert werden:

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

Regel 10: Wenn Sie eine synchrone P/Invoke-Methode umschließen, sollte Ihre API Memory<T> als Parameter akzeptieren.

Da Sie das Schlüsselwort fixed nicht über asynchrone Vorgänge hinweg verwenden können, verwenden Sie die Memory<T>.Pin-Methode, um Memory<T>-Instanzen anzuheften, unabhängig von der Art des zusammenhängenden Speichers, den die Instanz repräsentiert. Das folgende Beispiel zeigt, wie man diese API verwendet, um einen asynchronen P/Invoke-Aufruf durchzuführen.

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

Siehe auch