Memory<T> と Span<T> の使用ガイドラインMemory<T> and Span<T> usage guidelines

.NET Core には、任意の連続したメモリ領域を表す多くの型があります。.NET Core includes a number of types that represent an arbitrary contiguous region of memory. .NET Core 2.0 では Span<T>ReadOnlySpan<T> が導入されました。これらはマネージド メモリまたはアンマネージド メモリでサポートできる軽量のメモリ バッファーです。.NET Core 2.0 introduced Span<T> and ReadOnlySpan<T>, which are lightweight memory buffers that can be backed by managed or unmanaged memory. このような型はスタック上にのみ格納できるため、非同期メソッドの呼び出しを含む多くのシナリオには適していません。Because these types can only be stored on the stack, they are unsuitable for a number of scenarios, including asynchronous method calls. .NET Core 2.1 では、Memory<T>ReadOnlyMemory<T>IMemoryOwner<T>MemoryPool<T> など、さらに多くの型が追加されています。.NET Core 2.1 adds a number of additional types, including Memory<T>, ReadOnlyMemory<T>, IMemoryOwner<T>, and MemoryPool<T>. Span<T> と同様に、Memory<T> とそれに関連する型は、マネージド メモリとアンマネージド メモリの両方でサポートできます。Like Span<T>, Memory<T> and its related types can be backed by both managed and unmanaged memory. Span<T> とは異なり、Memory<T> はマネージド ヒープ上に格納できます。Unlike Span<T>, Memory<T> can be stored on the managed heap.

Span<T>Memory<T> は、どちらもパイプラインで使用できる構造化データのバッファーです。Both Span<T> and Memory<T> are buffers of structured data that can be used in pipelines. つまり、データの一部または全部をパイプライン内のコンポーネントに効率的に渡すことができるように設計されています。そのため、データを処理し、必要に応じてバッファーを変更できます。That is, they are designed so that some or all of the data can be efficiently passed to components in the pipeline, which can process them and optionally modify the buffer. Memory<T> とその関連する型には、複数のコンポーネントまたは複数のスレッドからアクセスできるため、開発者が標準的な使用ガイドラインに従って堅牢なコードを作成することが重要です。Because Memory<T> and its related types can be accessed by multiple components or by multiple threads, it's important that developers follow some standard usage guidelines to produce robust code.

所有者、コンシューマー、有効期間管理Owners, consumers, and lifetime management

バッファーは API 間で渡すことができ、複数のスレッドからバッファーにアクセスされることがあるため、有効期間管理を考慮することが重要です。Since buffers can be passed around between APIs, and since buffers can sometimes be accessed from multiple threads, it's important to consider lifetime management. 次の 3 つの中心的な概念があります。There are three core concepts:

  • 所有権Ownership. バッファー インスタンスの所有者は、バッファーが使用されなくなったときにバッファーを破棄することを含め、有効期間の管理を担当します。The owner of a buffer instance is responsible for lifetime management, including destroying the buffer when it's no longer in use. すべてのバッファーの所有者は 1 つです。All buffers have a single owner. 通常、所有者とは、バッファーを作成したコンポーネント、またはファクトリーからバッファーを受け取ったコンポーネントです。Generally the owner is the component that created the buffer or that received the buffer from a factory. 所有権は譲渡することもできます。コンポーネント A はバッファーの制御をコンポーネント B に譲渡することができます。その時点でコンポーネント A はバッファーを使用できなくなり、コンポーネント B が、使用されなくなったバッファーの破棄を担当します。Ownership can also be transferred; Component-A can relinquish control of the buffer to Component-B, at which point Component-A may no longer use the buffer, and Component-B becomes responsible for destroying the buffer when it's no longer in use.

  • 消費Consumption. バッファー インスタンスのコンシューマーは、読み取り、場合によっては書き込みでバッファー インスタンスを使用できます。The consumer of a buffer instance is allowed to use the buffer instance by reading from it and possibly writing to it. 何らかの外部の同期メカニズムが提供されていない限り、バッファーは同時に持つことができるコンシューマーは 1 つです。Buffers can have one consumer at a time unless some external synchronization mechanism is provided. バッファーのアクティブなコンシューマーは必ずしもバッファーの所有者ではありません。The active consumer of a buffer isn't necessarily the buffer's owner.

  • リースLease. リースは、特定のコンポーネントがバッファーの消費者になることができる時間の長さです。The lease is the length of time that a particular component is allowed to be the consumer of the buffer.

これら 3 つの概念を示す疑似コード例を次に示します。The following pseudo-code example illustrates these three concepts. これには、型 CharMemory<T> バッファーをインスタンス化し、WriteInt32ToBuffer メソッドを呼び出して整数の文字列表現をバッファーに書き込み、次に DisplayBufferToConsole メソッドを呼び出してバッファーの値を表示する Main メソッドが含まれます。It includes a Main method that instantiates a Memory<T> buffer of type Char, calls the WriteInt32ToBuffer method to write the string representation of an integer to the buffer, and then calls the DisplayBufferToConsole method to display the value of the buffer.

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 メソッドによってバッファー (この場合は Span<T> インスタンス) が作成され、その所有者も作成されます。The Main method creates the buffer (in this case an Span<T> instance) and so is its owner. そのため、Main には使用されなくなったバッファーを破棄する責任があります。Therefore, Main is responsible for destroying the buffer when it's no longer in use. この処理は、バッファーの Span<T>.Clear() メソッドを呼び出して実行しますIt does this by calling the buffer's Span<T>.Clear() method. (実際には、この Clear() メソッドによってバッファーのメモリがクリアされます。Span<T> 構造体には、実際にバッファーを破棄するメソッドはありません)。(The Clear() method here actually clears the buffer's memory; the Span<T> structure doesn't actually have a method that destroys the buffer.)

バッファーには WriteInt32ToBufferDisplayBufferToConsole という 2 つのコンシューマーがあります。The buffer has two consumers, WriteInt32ToBuffer and DisplayBufferToConsole. 同時に存在するコンシューマーは 1 つのみであり (最初は WriteInt32ToBuffer、次に DisplayBufferToConsole)、どちらのコンシューマーもバッファーを所有していません。There is only one consumer at a time (first WriteInt32ToBuffer, then DisplayBufferToConsole), and neither of the consumers owns the buffer. この文脈における "コンシューマー" は、バッファーの読み取り専用ビューを意味していない点にも注意してください。バッファーの読み取り/書き込みビューがある場合、コンシューマーは WriteInt32ToBuffer と同様にバッファーの内容を変更できます。Note also that "consumer" in this context doesn't imply a read-only view of the buffer; consumers can modify the buffer's contents, as WriteInt32ToBuffer does, if given a read/write view of the buffer.

メソッド呼び出しの開始からメソッドから返されるまでの間に、WriteInt32ToBuffer メソッドはバッファー上にリースを持ちます (消費することができます)。The WriteInt32ToBuffer method has a lease on (can consume) the buffer between the start of the method call and the time the method returns. 同様に、DisplayBufferToConsole は実行中にバッファー上にリースを持ち、メソッドがアンワインドするとリースは解放されますSimilarly, DisplayBufferToConsole has a lease on the buffer while it's executing, and the lease is released when the method unwinds. (リース管理のための API はありません。"リース" は概念的なものです)。(There is no API for lease management; a "lease" is a conceptual matter.)

Memory<T> と所有者またはコンシューマー モデルMemory<T> and the owner/consumer model

所有者、コンシューマー、有効期間管理」セクションで説明したように、バッファーには常に所有者がいます。As the Owners, consumers, and lifetime management section notes, a buffer always has an owner. .NET Core は 2 つの所有権モデルをサポートしています。.NET Core supports two ownership models:

  • 単一の所有権をサポートするモデル。A model that supports single ownership. バッファーは、その有効期間全体にわたって単一の所有者を持ちます。A buffer has a single owner for its entire lifetime.

  • 所有権の譲渡をサポートするモデル。A model that supports ownership transfer. バッファーの所有権は、元の所有者 (作成者) から別のコンポーネントに譲渡できます。譲渡されたコンポーネントがバッファーの有効期間管理を担当するようになります。Ownership of a buffer can be transferred from its original owner (its creator) to another component, which then becomes responsible for the buffer's lifetime management. さらにその所有者が所有権を別のコンポーネントに譲渡することもできます。That owner can in turn transfer ownership to another component, and so on.

明示的にバッファーの所有権を管理するには、System.Buffers.IMemoryOwner<T> インターフェイスを使用します。You use the System.Buffers.IMemoryOwner<T> interface to explicitly manage the ownership of a buffer. IMemoryOwner<T> は両方の所有権モデルをサポートしています。IMemoryOwner<T> supports both ownership models. IMemoryOwner<T> の参照を持つコンポーネントがバッファーを所有します。The component that has an IMemoryOwner<T> reference owns the buffer. 次の例では、IMemoryOwner<T> インスタンスを使用して Memory<T> バッファーの所有権を反映しています。The following example uses an IMemoryOwner<T> instance to reflect the ownership of a Memory<T> buffer.

using System;
using System.Buffers;

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

        Console.Write("Enter a number: ");
        try {
            var value = Int32.Parse(Console.ReadLine());

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

この例を using と記述することもできます。We can also write this example with the using:

using System;
using System.Buffers;

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

                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 this code:

  • Main メソッドは IMemoryOwner<T> インスタンスへの参照を保持しているため、Main メソッドはバッファーの所有者です。The Main method holds the reference to the IMemoryOwner<T> instance, so the Main method is the owner of the buffer.

  • WriteInt32ToBuffer および DisplayBufferToConsole メソッドは、Memory<T> をパブリック API として受け入れます。The WriteInt32ToBuffer and DisplayBufferToConsole methods accept Memory<T> as a public API. そのため、これらはバッファーの消費者です。Therefore, they are consumers of the buffer. また、消費できるのは一度に 1 つのみです。And they only consume it one at a time.

WriteInt32ToBuffer メソッドはバッファーに値を書き込むことが意図されていますが、DisplayBufferToConsole メソッドではそうではありません。Although the WriteInt32ToBuffer method is intended to write a value to the buffer, the DisplayBufferToConsole method isn't. これを反映するために、型 ReadOnlyMemory<T> の引数を受け入れておくことができます。To reflect this, it could have accepted an argument of type ReadOnlyMemory<T>. ReadOnlyMemory<T> の詳細については、「規則 2:バッファーを読み取り専用にする場合は ReadOnlySpan<T> または ReadOnlyMemory<T> を使用する」を参照してください。For more information on ReadOnlyMemory<T>, see Rule #2: Use ReadOnlySpan<T> or ReadOnlyMemory<T> if the buffer should be read-only.

"所有者なし" の Memory<T> インスタンス"Ownerless" Memory<T> instances

IMemoryOwner<T> を使用せずに Memory<T> インスタンスを作成できます。You can create a Memory<T> instance without using IMemoryOwner<T>. この場合、バッファーの所有権は明示的ではなく暗黙的であり、単一の所有者モデルのみがサポートされます。In this case, ownership of the buffer is implicit rather than explicit, and only the single-owner model is supported. これは次の方法で実行できます。You can do this by:

  • 次の例のように、Memory<T> コンストラクターのいずれかを直接呼び出し、T[] を渡します。Calling one of the Memory<T> constructors directly, passing in a T[], as the following example does.

  • String.AsMemory 拡張メソッドを呼び出して ReadOnlyMemory<char> インスタンスを生成します。Calling the String.AsMemory extension method to produce a ReadOnlyMemory<char> instance.

using System;

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

        Console.Write("Enter a number: ");
        var value = Int32.Parse(Console.ReadLine());

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

最初に Memory<T> インスタンスを作成したメソッドは、バッファーの暗黙的な所有者です。The method that initially creates the Memory<T> instance is the implicit owner of the buffer. 譲渡を支援する IMemoryOwner<T> インスタンスはないため、所有権を他のコンポーネントに譲渡することはできませんOwnership cannot be transferred to any other component because there is no IMemoryOwner<T> instance to facilitate the transfer. (または、ランタイムのガベージ コレクターがバッファーを所有していて、すべてのメソッドがバッファーを消費するだけであると想像することもできます)。(As an alternative, you can also imagine that the runtime's garbage collector owns the buffer, and all methods just consume the buffer.)

使用ガイドラインUsage guidelines

メモリ ブロックは所有されていますが、複数のコンポーネントに渡されることが意図されており、その一部は特定のメモリ ブロック上で同時に動作する可能性があるため、Memory<T>Span<T> の両方を使用するためのガイドラインを確立することが重要です。Because a memory block is owned but is intended to be passed to multiple components, some of which may operate upon a particular memory block simultaneously, it is important to establish guidelines for using both Memory<T> and Span<T>. ガイドラインが必要な理由は以下のとおりです。Guidelines are necessary because:

  • 所有者がメモリ ブロックを解放した後に、コンポーネントがメモリ ブロックへの参照を保持する可能性があります。It is possible for a component to retain a reference to a memory block after its owner has released it.

  • あるコンポーネントがバッファーに対して動作し、それ同時に他のコンポーネントがそのバッファーに対して動作する可能性があり、そのようなプロセスでは、バッファー内のデータが破損します。It is possible for a component to operate on a buffer at the same time that another component is operating on it, in the process corrupting the data in the buffer.

  • スタックに割り当てられる Span<T> の性質によってパフォーマンスが最適化され、Span<T> はメモリ ブロック上での操作に適した型になりますが、Span<T> にはいくつかの大きな制限もあります。While the stack-allocated nature of Span<T> optimizes performance and makes Span<T> the preferred type for operating on a memory block, it also subjects Span<T> to some major restrictions. Span<T> を使用するタイミングと、Memory<T> を使用するタイミングを把握することが重要です。It is important to know when to use a Span<T> and when to use Memory<T>.

以下は、Memory<T> とその関連する型を正しく使用するためのレコメンデーションです。The following are our recommendations for successfully using Memory<T> and its related types. 特に明記しない限り、Memory<T>Span<T> に適用されるガイダンスは ReadOnlyMemory<T>ReadOnlySpan<T> にも適用されます。Guidance that applies to Memory<T> and Span<T> also applies to ReadOnlyMemory<T> and ReadOnlySpan<T> unless we explicitly note otherwise.

規則 1:同期 API の場合、可能であればパラメーターとして Memory<T> ではなく Span<T> を使用する。Rule #1: For a synchronous API, use Span<T> instead of Memory<T> as a parameter if possible.

Span<T>Memory<T> よりも汎用性が高く、さまざまな連続するメモリ バッファーを表すことができます。Span<T> is more versatile than Memory<T> and can represent a wider variety of contiguous memory buffers. Span<T>Memory<T>> よりもパフォーマンスに優れています。Span<T> also offers better performance than Memory<T>. 最後に、Memory<T>.Span プロパティを使用して Memory<T> インスタンスを Span<T> に変換することはできますが、Span<T> から Memory<T> に変換することはできません。Finally, you can use the Memory<T>.Span property to convert a Memory<T> instance to a Span<T>, although Span<T>-to-Memory<T> conversion isn't possible. そのため、呼び出し元が Memory<T> インスタンスを持っていた場合、いずれにしても Span<T> パラメーターを使用してメソッドを呼び出すことができます。So if your callers happen to have a Memory<T> instance, they'll be able to call your methods with Span<T> parameters anyway.

Memory<T> ではなく型 Span<T> のパラメーターを使用すると、適切な消費メソッドの実装を記述する場合にも役立ちます。Using a parameter of type Span<T> instead of type Memory<T> also helps you write a correct consuming method implementation. メソッドのリース時間を過ぎてバッファーにアクセスを試行しないように、コンパイル時のチェックが自動的に実行されます (詳細については後述します)。You'll automatically get compile-time checks to ensure that you're not attempting to access the buffer beyond your method's lease (more on this later).

完全に同期していても、Span<T> パラメーターではなく Memory<T> パラメーターの使用が必要な場合があります。Sometimes, you'll have to use a Memory<T> parameter instead of a Span<T> parameter, even if you're fully synchronous. おそらく、利用している API は Memory<T> 引数のみを受け入れます。Perhaps an API that you depend accepts only Memory<T> arguments. これは問題ありませんが、Memory<T> を同期的に使用するときに伴うトレードオフに注意してください。This is fine, but be aware of the tradeoffs involved when using Memory<T> synchronously.

規則 2:バッファーを読み取り専用にする場合は ReadOnlySpan<T> または ReadOnlyMemory<T> を使用する。Rule #2: Use ReadOnlySpan<T> or ReadOnlyMemory<T> if the buffer should be read-only.

前述の例では、DisplayBufferToConsole メソッドはバッファーからの読み取りのみを行います。バッファーの内容は変更しません。In the earlier examples, the DisplayBufferToConsole method only reads from the buffer; it doesn't modify the contents of the buffer. メソッドのシグネチャは次のように変更する必要があります。The method signature should be changed to the following.

void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);

実際、この規則と規則 1 を組み合わせると、さらに改善され、メソッドのシグネチャを次のように書き換えることができます。In fact, if we combine this rule and Rule #1, we can do even better and rewrite the method signature as follows:

void DisplayBufferToConsole(ReadOnlySpan<char> buffer);

DisplayBufferToConsole メソッドは、考えられるほぼすべてのバッファー型 (T[]stackalloc で割り当てられたストレージなど) で動作します。The DisplayBufferToConsole method now works with virtually every buffer type imaginable: T[], storage allocated with stackalloc, and so on. String を直接渡すこともできます。You can even pass a String directly into it!

規則 3:メソッドが Memory<T> を受け入れて void を返した場合、メソッドが返した後に Memory<T> インスタンスを使用してはならない。Rule #3: If your method accepts Memory<T> and returns void, you must not use the Memory<T> instance after your method returns.

これは、前述した "リース" の概念に関連しています。This relates to the "lease" concept mentioned earlier. Memory<T> インスタンスに対する void を返すメソッドのリースは、メソッドが開始時に開始され、メソッドの終了時に終了します。A void-returning method's lease on the Memory<T> instance begins when the method is entered, and it ends when the method exits. コンソールからの入力に基づいてループで Log を呼び出す次の例を考えてみましょう。Consider the following example, which calls Log in a loop based on input from the console.

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)
            {
                int value = Int32.Parse(Console.ReadLine());
                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;
    }
}

Log が完全同期メソッドである場合、メモリ インスタンスのアクティブなコンシューマーは常に 1 つのみなので、このコードは予想どおりに動作します。If Log is a fully synchronous method, this code will behave as expected because there is only one active consumer of the memory instance at any given time. ただし、代わりに Log がこの実装を持っている場合を想像してください。But imagine instead that Log has this implementation.

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

この実装では、元のメソッドから返された後も、Log はバックグラウンドで Memory<T> インスタンスを使用しようとするため、リースに違反することになります。In this implementation, Log violates its lease because it still attempts to use the Memory<T> instance in the background after the original method has returned. Main メソッドがバッファーを変更しているときに、Log がバッファーを読み込もうとすると、データが破損する可能性があります。The Main method could mutate the buffer while Log attempts to read from it, which could result in data corruption.

これを解決する方法はいくつかあります。There are several ways to resolve this:

  • 以下の Log メソッドの実装のように、Log メソッドは void ではなく Task を返す可能性があります。The Log method can return a Task instead of void, as the following implementation of the Log method does.

    // 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 は代わりに次のように実装できます。Log can instead be implemented as follows:

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

規則 4:メソッドが Memory<T> を受け入れて Task を返す場合、Task が終了状態に遷移した後に Memory<T> インスタンスを使用してはならない。Rule #4: If your method accepts a Memory<T> and returns a Task, you must not use the Memory<T> instance after the Task transitions to a terminal state.

これは規則 3 の単なる非同期版です。This is just the async variant of Rule #3. 前の例の Log メソッドは、この規則に従うために次のように記述することができます。The Log method from the earlier example can be written as follows to comply with this rule:

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

ここで、"終了状態" とは、タスクの状態が完了、失敗、またはキャンセル済みに移行することを意味します。Here, "terminal state" means that the task transitions to a completed, faulted, or canceled state. つまり、"終了状態" とは、"スローまたは実行継続の待機を発生させるすべてのもの" を意味します。In other words, "terminal state" means "anything that would cause await to throw or to continue execution."

このガイダンスは、TaskTask<TResult>ValueTask<TResult>、または同様の型を返すメソッドに適用されます。This guidance applies to methods that return Task, Task<TResult>, ValueTask<TResult>, or any similar type.

規則 5:コンストラクターがパラメーターとして Memory<T> を受け入れる場合、構築されたオブジェクトのインスタンス メソッドは Memory<T> インスタンスの消費者であると見なされる。Rule #5: If your constructor accepts Memory<T> as a parameter, instance methods on the constructed object are assumed to be consumers of the Memory<T> instance.

次に例を示します。Consider the following example:

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

ここで、OddValueExtractor コンストラクターは ReadOnlyMemory<int> をコンストラクター パラメーターとして受け入れます。そのため、コンストラクター自体が ReadOnlyMemory<int> インスタンスのコンシューマーであり、戻り値に対するすべてのインスタンス メソッドも元の ReadOnlyMemory<int> インスタンスのコンシューマーです。Here, the OddValueExtractor constructor accepts a ReadOnlyMemory<int> as a constructor parameter, so the constructor itself is a consumer of the ReadOnlyMemory<int> instance, and all instance methods on the returned value are also consumers of the original ReadOnlyMemory<int> instance. つまり、インスタンスが TryReadNextOddValue メソッドに直接渡されない場合でも、TryReadNextOddValueReadOnlyMemory<int> インスタンスを消費します。This means that TryReadNextOddValue consumes the ReadOnlyMemory<int> instance, even though the instance isn't passed directly to the TryReadNextOddValue method.

規則 6:型に設定できる Memory<T> 型のプロパティ (または同等のインスタンス メソッド) がある場合、そのオブジェクトに対するインスタンス メソッドは Memory<T> インスタンスの消費者であると見なされる。Rule #6: If you have a settable Memory<T>-typed property (or an equivalent instance method) on your type, instance methods on that object are assumed to be consumers of the Memory<T> instance.

これは単に規則 5 の一種です。This is really just a variant of Rule #5. この規則が存在する理由は、プロパティ セッターまたは同等のメソッドは入力をキャプチャして永続化すると想定されているため、同じオブジェクトに対するインスタンス メソッドがキャプチャした状態を利用する可能性があるためです。This rule exists because property setters or equivalent methods are assumed to capture and persist their inputs, so instance methods on the same object may utilize the captured state.

この規則をトリガーする例を次に示します。The following example triggers this rule:

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

規則 7:IMemoryOwner<T> 参照がある場合、どこかの時点でそれを破棄するか所有権を譲渡する必要がある (両方を実行する必要はない)。Rule #7: If you have an IMemoryOwner<T> reference, you must at some point dispose of it or transfer its ownership (but not both).

Memory<T> インスタンスはマネージド メモリまたはアンマネージド メモリのいずれかでサポートされている可能性があるため、Memory<T> インスタンスに対して実行された作業が完了したら、所有者は MemoryPool<T>.Dispose を呼び出す必要があります。Since a Memory<T> instance may be backed by either managed or unmanaged memory, the owner must call MemoryPool<T>.Dispose when work performed on the Memory<T> instance is complete. または、所有者が IMemoryOwner<T> インスタンスの所有権を別のコンポーネントに譲渡することもできます。譲渡の時点で、獲得した側のコンポーネントは適切なタイミングで MemoryPool<T>.Dispose を呼び出す責任を負います (詳細については後述します)。Alternatively, the owner may transfer ownership of the IMemoryOwner<T> instance to a different component, at which point the acquiring component becomes responsible for calling MemoryPool<T>.Dispose at the appropriate time (more on this later).

Dispose メソッドの呼び出しに失敗すると、マネージド メモリのリークやその他のパフォーマンス低下が発生する可能性があります。Failure to call the Dispose method may lead to unmanaged memory leaks or other performance degradation.

この規則は、MemoryPool<T>.Rent のようなファクトリ メソッドを呼び出すコードにも適用されます。This rule also applies to code that calls factory methods like MemoryPool<T>.Rent. 呼び出し元は、返された IMemoryOwner<T> の所有者になり、終了時にインスタンスを破棄する責任を負います。The caller becomes the owner of the returned IMemoryOwner<T> and is responsible for disposing of the instance when finished.

規則 8:API サーフェスに IMemoryOwner<T> パラメーターがある場合、そのインスタンスの所有権を受け入れていることを示す。Rule #8: If you have an IMemoryOwner<T> parameter in your API surface, you are accepting ownership of that instance.

この種類のシグナルを持つインスタンスを受け入れることは、コンポーネントがこのインスタンスの所有権を取得しようとしていることを示します。Accepting an instance of this type signals that your component intends to take ownership of this instance. 規則 7 に従い、コンポーネントは適切な破棄の責任を負うようになります。Your component becomes responsible for proper disposal according to Rule #7.

IMemoryOwner<T> インスタンスの所有権を別のコンポーネントに譲渡したコンポーネントは、メソッド呼び出しの完了後にそのインスタンスを使用できなくなります。Any component that transfers ownership of the IMemoryOwner<T> instance to a different component should no longer use that instance after the method call completes.

重要

コンストラクターがパラメーターとして IMemoryOwner<T>を受け入れる場合、その型は IDisposable を実装し、Dispose メソッドは MemoryPool<T>.Dispose を呼び出す必要があります。If your constructor accepts IMemoryOwner<T> as a parameter, its type should implement IDisposable, and your Dispose method should call MemoryPool<T>.Dispose.

規則 9:同期的 p/invoke メソッドをラップしている場合、API は Span<T> をパラメーターとして受け入れる必要がある。Rule #9: If you're wrapping a synchronous p/invoke method, your API should accept Span<T> as a parameter.

規則 1 に従うと、Span<T> は一般に同期的 API に使用するために適した型です。According to Rule #1, Span<T> is generally the correct type to use for synchronous APIs. 次の例のように、fixed キーワードを介して Span<T> インスタンスを固定できます。You can pin Span<T> instances via the fixed keyword, as in the following example.

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

前の例では、入力の範囲が空の場合などに、pbData が null になる可能性があります。In the previous example, pbData can be null if, for example, the input span is empty. cbData が 0 であっても、エクスポートされたメソッドで pbData を null 以外にする必要がある場合、このメソッドは次のように実装できます。If the exported method absolutely requires that pbData be non-null, even if cbData is 0, the method can be implemented as follows:

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

規則 10:同期的 p/invoke メソッドをラップしている場合、API は Memory<T> をパラメーターとして受け入れる必要がある。Rule #10: If you're wrapping an asynchronous p/invoke method, your API should accept Memory<T> as a parameter.

非同期操作で fixed キーワードは使用できないので、インスタンスが表す連続するメモリの種類に関係なく、Memory<T>.Pin メソッドを使用して Memory<T> インスタンスを固定します。Since you cannot use the fixed keyword across asynchronous operations, you use the Memory<T>.Pin method to pin Memory<T> instances, regardless of the kind of contiguous memory the instance represents. 次の例は、この API を使用して非同期の p/invoke 呼び出しを実行する方法を示しています。The following example shows how to use this API to perform an asynchronous p/invoke call.

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

関連項目See also