Quick Reference
동기 메서드가 연속 메모리의 일부만 읽거나 바꿀 때는 Span<T> 또는 ReadOnlySpan<T>를 받습니다. buffer를 field에 보관하거나 비동기 작업이 끝날 때까지 빌려야 하면 Memory<T> 또는 ReadOnlyMemory<T>를 사용합니다. Span은 배열·문자열·stackalloc을 가리킬 수 있지만 ref struct 제약 때문에 heap field, boxing, await를 넘는 수명에는 쓸 수 없습니다.
ReadOnlySpan<char> line = "port=8080";
ReadOnlySpan<char> value = line[(line.IndexOf('=') + 1)..];
int port = int.Parse(value);
byte[] bytes = new byte[1024];
Span<byte> header = bytes.AsSpan(0, 16);
Memory<byte> pendingRead = bytes.AsMemory();- 읽기 전용 API는
ReadOnlySpan<T>/ReadOnlyMemory<T>를 먼저 선택합니다. Memory<T>.Span으로 얻은 Span도await를 넘겨 보관하지 않습니다.- allocation을 줄이는 것은 측정 가능한 hot path에서만 목표로 둡니다.
Span과 Memory의 수명
Span<T>와 ReadOnlySpan<T>는 ref struct입니다. 이 타입 자체는 heap object field로 저장하거나 boxing, lambda capture, iterator의 장기 상태로 만들 수 없습니다. 배열을 가리키는 Span은 배열 데이터가 heap에 있어도 가능하고, stackalloc 결과를 가리키는 Span은 stack storage를 가리킬 수 있습니다. “Span은 항상 stack memory”라는 말은 정확하지 않습니다.
최신 C#은 async method 안의 ref struct local을 일부 허용하지만, 그 local은 await나 yield 경계를 넘을 수 없습니다. 비동기 호출 전후에 같은 buffer view를 보관해야 한다면 Memory<T>를 저장하고, 동기 처리의 짧은 구간에서만 .Span을 꺼냅니다.
static int ParsePort(ReadOnlySpan<char> text) =>
int.TryParse(text, out int port)
? port
: throw new FormatException("Invalid port.");
static async Task<int> ReadAsync(Stream stream, Memory<byte> buffer)
{
return await stream.ReadAsync(buffer);
}Memory<T>도 소유권을 뜻하지는 않습니다. 원본 배열이나 memory owner가 buffer를 반납·변경하면 그 view를 가진 쪽도 영향을 받습니다. void method는 받은 memory를 반환 후 사용하지 않고, Task를 반환하는 method는 그 Task가 완료된 뒤 사용하지 않는다는 lease를 지켜야 합니다.
슬라이스와 API 표면
Span slice는 원본 buffer의 view이므로 범위 안의 쓰기가 원본에 반영됩니다. string에서 얻은 ReadOnlySpan<char>는 string의 문자를 복사하지 않고 읽을 수 있지만 수정할 수는 없습니다.
int[] values = [10, 20, 30, 40];
Span<int> middle = values.AsSpan(1, 2);
middle[0] = 99;
Console.WriteLine(values[1]); // 99
ReadOnlySpan<char> key = "theme=dark".AsSpan()[..5];API가 동기적으로 소비만 한다면 ReadOnlySpan<T> parameter가 가장 넓은 caller를 받을 수 있습니다. T[], stackalloc, Memory<T>.Span, string의 char span을 모두 전달할 수 있습니다. 결과를 오래 보관하거나 비동기적으로 생산·소비해야 한다면 Memory<T>/ReadOnlyMemory<T>와 완료 시점을 명확히 합니다.
ArrayPool과 buffer 반납
ArrayPool<T>.Shared.Rent(minimumLength)는 요청 길이보다 큰 배열을 줄 수 있고, 이전 사용자의 데이터가 남아 있을 수 있습니다. 유효한 data 길이는 read result처럼 별도로 관리하며, finally에서 같은 배열을 반드시 반납합니다.
byte[] rented = ArrayPool<byte>.Shared.Rent(4096);
try
{
int read = stream.Read(rented, 0, rented.Length);
Process(rented.AsSpan(0, read));
}
finally
{
ArrayPool<byte>.Shared.Return(rented, clearArray: true);
}clearArray: true는 password·token처럼 민감한 데이터에 유용하지만 전체 배열을 지우는 비용이 있습니다. pool에서 빌린 배열을 field에 저장하거나 반환 뒤에 계속 읽지 않습니다. 소유권 이동이 필요하면 IMemoryOwner<T>/MemoryPool<T>처럼 Dispose로 lease를 표현하는 API를 검토합니다.
자주 틀리는 부분
Span<T>를 class field에 저장하거나await뒤에 사용하려 하지 않습니다.- Span slice는 복사본이 아닙니다. 원본 mutation과 lifetime을 함께 고려합니다.
Memory<T>가 있다고 해서 buffer가 thread-safe하거나 immutable한 것은 아닙니다.- pool array의 전체 길이를 실제 데이터 길이로 처리하거나
Return을 누락하지 않습니다.
참고 링크
3 sources