`Task` (Heap Object) vs `ValueTask` (Zero-Allocation Struct)
1Concept
`Task` is a reference type allocating ~80 bytes on the heap. When an async method frequently completes synchronously (e.g. cache hit), returning `ValueTask<T>` eliminates all heap allocations.
2Architecture Diagram
Cache Hit (Synchronous Path): return ValueTask.FromResult(data); ──> ZERO Heap Allocations (Value type on stack!) Cache Miss (Asynchronous Path): return new ValueTask(FetchFromDatabaseAsync()); ──> Wraps standard Task seamlessly
3Code Example
C# 13 & .NET 9
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public class ValueTaskDemo
{
private static readonly Dictionary<int, string> Cache = new() { [101] = "CachedPayload" };
public static ValueTask<string> GetUserAsync(int id)
{
// Synchronous cache hit path: ZERO heap allocation!
if (Cache.TryGetValue(id, out var val))
{
return new ValueTask<string>(val);
}
// Asynchronous database fallback path
return new ValueTask<string>(FetchFromDbAsync(id));
}
private static async Task<string> FetchFromDbAsync(int id)
{
await Task.Delay(10);
return $"DbUser_{id}";
}
public static async Task Main()
{
string user = await GetUserAsync(101);
Console.WriteLine($"Fetched User (Zero Allocation Path): {user}");
}
}4Expected Output
Fetched User (Zero Allocation Path): CachedPayload
5Key Takeaways
- ✓Use `ValueTask<T>` for high-frequency methods that complete synchronously >= 80% of the time.
- ✓Never `await` a `ValueTask` multiple times (call `.Preserve()` if multi-await is required).
- ✓Never call `.Result` or `.GetAwaiter().GetResult()` on a non-completed `ValueTask`.