作为ASP.NET开发人员,我们一直在寻找能让我们的Web应用程序运行得更快、更高效的方法。Span<T>
和Memory<T>
这两个强大的工具就能帮我们达成这一目标。它们于几年前被引入,如今已成为编写高性能C#代码必不可少的部分。
让我们通过实际示例以及针对2024年C# 12的一些技巧,来探讨如何在ASP.NET应用程序中有效地使用Span<T>
和Memory<T>
。
Span<T>
和Memory<T>
是什么?让我们先从基础知识讲起:
Span<T>
是一种表示连续内存块的类型。它可用于处理数组、字符串或非托管内存,而且无需创建副本。
Memory<T>
与Span<T>
类似,但它可用于异步方法中,并且能存储在字段里。
可以把Span<T>
想象成能直接操作的内存视图,而Memory<T>
则是对该内存的引用,能更自由地传递。
下面让我们来看一些在常见的ASP.NET场景中,Span<T>
和Memory<T>
能发挥重大作用的情况:
Span<T>
设想你正在构建一个会接收大量JSON数据的API。你可以使用Span<T>
更高效地解析数据,而不必为每条数据都分配新的字符串:
[HttpPost]
public IActionResult ProcessOrder([FromBody] string orderJson)
{
ReadOnlySpan<char> jsonSpan = orderJson.AsSpan();
// 在不分配新字符串的情况下查找 "totalAmount" 字段
int startIndex = jsonSpan.IndexOf("\"totalAmount\":") + "\"totalAmount\":".Length;
int endIndex = jsonSpan.Slice(startIndex).IndexOf(',');
if (decimal.TryParse(jsonSpan.Slice(startIndex, endIndex), out decimal totalAmount))
{
// 处理订单...
return Ok($"Order processed with total amount: {totalAmount}");
}
return BadRequest("Invalid order data");
}
这种方法减少了内存分配,提升了性能,尤其在处理大型有效载荷时效果显著。
在发送响应(特别是大型响应)时,Span<T>
有助于优化内存使用:
[HttpGet]
public IActionResult GetLargeData()
{
const int bufferSize = 1024 * 1024; // 1 MB缓冲区
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
int dataSize = GenerateLargeData(buffer.AsSpan());
return File(buffer.AsMemory(0, dataSize).ToArray(), "application/octet-stream", "large-data.bin");
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
private int GenerateLargeData(Span<byte> buffer)
{
// 用数据填充缓冲区...
return /* 实际数据大小 */;
}
这个示例结合使用ArrayPool<T>
与Span<T>
和Memory<T>
,能在不过度分配内存的情况下高效地处理大型数据。
自定义中间件通常需要检查URL。以下展示了如何高效地进行这项操作:
public class CustomUrlMiddleware
{
private readonly RequestDelegate _next;
public CustomUrlMiddleware(RequestDelegate next) => _next = next;
public Task InvokeAsync(HttpContext context)
{
ReadOnlySpan<char> path = context.Request.Path.Value.AsSpan();
if (path.StartsWith("/api/".AsSpan()))
{
// 处理API请求
context.Items["IsApiRequest"] = true;
}
return _next(context);
}
}
这个中间件在检查URL时无需分配新的字符串,这对于高流量应用程序来说非常有用。
Memory<T>
在需要以下操作时可使用Memory<T>
:
Memory<T>
的方法。[HttpPost("upload")]
public async Task<IActionResult> UploadFile(IFormFile file)
{
if (file.Length > 10_000_000) // 10 MB限制
return BadRequest("File too large");
var memory = new Memory<byte>(new byte[file.Length]);
using (var stream = file.OpenReadStream())
{
await stream.ReadAsync(memory);
}
await ProcessUploadedFileAsync(memory);
return Ok("File processed successfully");
}
private async Task ProcessUploadedFileAsync(Memory<byte> fileData)
{
// 模拟一些异步处理
await Task.Delay(100); // 实际处理的占位符
// 示例:统计非零字节数
int nonZeroBytes = 0;
foreach (byte b in fileData.Span)
{
if (b!= 0) nonZeroBytes++;
}
Console.WriteLine($"Processed file with {nonZeroBytes} non-zero bytes");
}
这个示例展示了如何使用Memory<T>
在异步方法间处理文件数据,而无需不必要的复制操作。
在ASP.NET应用程序中缓存大型对象时,Memory<T>
会很有用:
public class LargeObjectCache
{
private Memory<byte> cachedData;
public async Task<Memory<byte>> GetOrCreateAsync(Func<Task<byte[]>> createFunc)
{
if (cachedData.IsEmpty)
{
byte[] newData = await createFunc();
cachedData = new Memory<byte>(newData);
}
return cachedData;
}
}
// 在控制器中的用法
[HttpGet("large-data")]
public async Task<IActionResult> GetLargeData([FromServices] LargeObjectCache cache)
{
var data = await cache.GetOrCreateAsync(async () =>
{
// 模拟获取大型数据
await Task.Delay(1000);
return new byte[1_000_000]; // 1 MB的数据
});
return File(data.ToArray(), "application/octet-stream");
}
对于构建大型字符串的后台任务,Memory<T>
可能比StringBuilder
更高效:
public class ReportGenerator : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await GenerateReportAsync();
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
private async Task GenerateReportAsync()
{
var writer = new ArrayBufferWriter<char>(initialCapacity: 1024 * 1024); // 初始容量为1 MB
await WriteReportHeaderAsync(writer);
await WriteReportBodyAsync(writer);
await WriteReportFooterAsync(writer);
string report = new string(writer.WrittenMemory.Span);
await SaveReportAsync(report);
}
private async Task WriteReportHeaderAsync(IBufferWriter<char> writer)
{
var memory = writer.GetMemory(1024);
int written = System.Text.Encoding.UTF8.GetBytes("Report Header\n", memory.Span);
writer.Advance(written);
await Task.Delay(100); // 模拟一些异步工作
}
// 用于WriteReportBodyAsync和WriteReportFooterAsync的类似方法
private async Task SaveReportAsync(string report)
{
// 将报告保存到数据库或文件
await File.WriteAllTextAsync("report.txt", report);
}
}
这个示例展示了如何在后台任务中结合使用Memory<T>
和IBufferWriter<T>
来高效构建字符串,这在ASP.NET应用程序中对于诸如生成报告或数据处理之类的任务很常见。
对于希望优化应用程序的ASP.NET开发人员来说,Span<T>
和Memory<T>
是强大的工具。通过使用这些类型,你可以编写更高效的代码,这些代码占用更少的内存且运行速度更快。