Phase 21 of 30 · Topic 21.1

`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`.