0Pricing
C# Academy · Lección

ValueTask y cómo evitar asignaciones

Use ValueTask para reducir las asignaciones en el heap en rutas críticas, comprenda sus limitaciones y evite errores habituales de uso.

ValueTask y cómo evitar asignaciones es una lección gratuita de C# Academy en CoddyKit. Esta es la lección 3 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de C# Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de C# Academy incluye 4 lecciones en total.

El problema de las asignaciones con Task

Task<T> es un tipo de referencia: cada método async asigna un Task en el montón, incluso si el resultado está disponible de forma síncrona (por ejemplo, desde una caché). En las rutas críticas, esto puede generar una presión considerable sobre el GC. ValueTask<T> resuelve este problema.

¿Qué es ValueTask?

ValueTask<T> es una estructura que puede representar tanto un resultado disponible de forma síncrona como una operación asíncrona pendiente. Cuando el resultado es síncrono, no se necesita ninguna asignación en el montón.

// Task<T>: always allocates, even when result is cached
public async Task<string> GetCached_Task() =>
    _cache.TryGetValue("key", out var v) ? v : await FetchAsync();

// ValueTask<T>: zero allocation when value is cached
public ValueTask<string> GetCached_ValueTask() =>
    _cache.TryGetValue("key", out var v)
        ? ValueTask.FromResult(v)  // no allocation
        : new ValueTask<string>(FetchAsync()); // allocation only when needed

Devolver ValueTask desde una interfaz

Use ValueTask<T> en métodos de interfaz que se llamen con frecuencia y que a menudo finalicen de forma síncrona. Los consumidores lo usan exactamente igual que Task<T>.

public interface IProductRepository
{
    // ValueTask: often completes synchronously from cache
    ValueTask<Product?> GetByIdAsync(int id);

    // Task: always async (DB query)
    Task<List<Product>> SearchAsync(string query);
}

// Consumption is identical to Task:
var product = await _repo.GetByIdAsync(42);
var results = await _repo.SearchAsync("keyboard");

ValueTask con IValueTaskSource

Para obtener un rendimiento extremo, implemente IValueTaskSource<T> a fin de agrupar el objeto de estado subyacente en un pool. Esto evita todas las asignaciones, incluso en las finalizaciones asíncronas; .NET lo usa internamente en el código de sockets y de E/S.

// In most cases, use the simple ValueTask approach.
// IValueTaskSource is for library authors needing zero-alloc async at extreme scale.

// Example: System.IO.Pipelines uses this internally
var result = await pipe.Reader.ReadAsync(ct);
// ReadAsync returns ValueTask<ReadResult> backed by IValueTaskSource
// -- zero allocation for every read even under high throughput

Restricciones de ValueTask: usar await una sola vez

Regla fundamental: un ValueTask<T> se debe esperar como máximo una vez, y no puede llamar a .Result antes de que finalice. Incumplir esta regla provoca un comportamiento indefinido.

var vt = repo.GetByIdAsync(42);

// BAD: await multiple times
var a = await vt; // OK
var b = await vt; // UNDEFINED BEHAVIOR — don't do this!

// BAD: two concurrent awaits
var t1 = vt.AsTask(); // converts to Task (safe to share)
var t2 = vt.AsTask(); // second AsTask() on same ValueTask — WRONG

// GOOD: if you need multiple awaits, convert to Task
var task = repo.GetByIdAsync(42).AsTask();
var r1 = await task;
var r2 = await task; // OK — Task can be awaited multiple times

Guía para decidir entre ValueTask<T> y Task<T>

Use ValueTask<T> solo cuando el análisis de rendimiento muestre presión por las asignaciones de los métodos asíncronos. No lo use en todas partes: añade complejidad sin aportar beneficios en código que se ejecuta con poca frecuencia.

// USE ValueTask when:
// 1. The method often completes synchronously (cache hit)
// 2. The method is on a hot path called millions of times/sec
// 3. You're writing a library with high-throughput I/O

// USE Task when:
// 1. The result is always async
// 2. The method might be awaited multiple times
// 3. You use Task.WhenAll / Task.WhenAny
// 4. You store the task in a variable and share it

ValueTask sin tipo genérico

ValueTask (sin tipo genérico) funciona para los métodos que devuelven Task (asíncronos sin valor de retorno). Úselo en métodos llamados con frecuencia que a menudo finalicen de forma síncrona.

public interface ICache
{
    // Often a no-op when value exists — ValueTask avoids allocation
    ValueTask SetAsync(string key, string value);
    ValueTask InvalidateAsync(string key);
}

public class InMemoryCache : ICache
{
    private readonly Dictionary<string, string> _data = new();

    public ValueTask SetAsync(string key, string value)
    {
        _data[key] = value;
        return ValueTask.CompletedTask; // no allocation
    }
}

Medir asignaciones con BenchmarkDotNet

Use [MemoryDiagnoser] en BenchmarkDotNet para medir las asignaciones por operación. Es la única forma fiable de verificar que ValueTask realmente resulta útil.

[MemoryDiagnoser]
public class CacheBenchmark
{
    private readonly CachedRepo _repo = new();

    [Benchmark]
    public async Task<Product?> Task_GetById()
        => await _repo.GetByIdTask(1);

    [Benchmark]
    public async ValueTask<Product?> ValueTask_GetById()
        => await _repo.GetByIdValueTask(1);
}

// Run: dotnet run -c Release
// Compare Gen0 allocations column — ValueTask should show 0B on cache hit

ValueTask en SocketsHttpHandler

La pila HTTP de .NET usa ValueTask ampliamente de forma interna. Comprenderlo le ayuda a escribir clientes HTTP de alto rendimiento que se ajusten a la estrategia de asignaciones del runtime.

// HttpClient uses ValueTask-based sockets internally.
// When using HttpClient.GetAsync(), the response is awaited with Task<HttpResponseMessage>.
// For streaming large bodies, use:
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
await using var stream = await response.Content.ReadAsStreamAsync();
// Stream reads internally use ValueTask<int> for zero-allocation I/O

Caso real: servicio de caché para una ruta crítica

Un servicio de caché en el que cada llamada puede ser un acierto de caché: ValueTask elimina las asignaciones en la ruta crítica y, al mismo tiempo, admite una alternativa asíncrona para los fallos de caché.

public class ProductCache
{
    private readonly Dictionary<int, Product> _cache = new();
    private readonly IProductRepository _repo;

    public ProductCache(IProductRepository repo) => _repo = repo;

    public ValueTask<Product?> GetAsync(int id)
    {
        // Cache hit: no allocation
        if (_cache.TryGetValue(id, out var cached))
            return ValueTask.FromResult<Product?>(cached);

        // Cache miss: async fetch
        return new ValueTask<Product?>(FetchAndCacheAsync(id));
    }

    private async Task<Product?> FetchAndCacheAsync(int id)
    {
        var product = await _repo.GetByIdAsync(id);
        if (product is not null) _cache[id] = product;
        return product;
    }
}

Comprobación rápida

¿Cuál es la regla fundamental sobre esperar un ValueTask?

Resumen: ValueTask y cómo evitar asignaciones

Conclusiones clave:

  • ValueTask<T>: estructura que evita asignaciones en el montón cuando el resultado es síncrono
  • Ideal para rutas críticas que finalizan con frecuencia de forma síncrona (aciertos de caché)
  • NUNCA espere un ValueTask<T> más de una vez; use .AsTask() para compartirlo
  • Use [MemoryDiagnoser] para verificar el ahorro de asignaciones antes de optimizar
  • Prefiera Task<T> para uso general; use ValueTask<T> solo cuando el análisis de rendimiento lo justifique

Preguntas frecuentes

¿La lección «ValueTask y cómo evitar asignaciones» es gratis?

Sí — el texto completo de «ValueTask y cómo evitar asignaciones» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de C# Academy, actualiza a CoddyKit PRO. El curso de C# Academy incluye 4 lecciones en total.

¿Qué aprenderé en «ValueTask y cómo evitar asignaciones»?

Use ValueTask para reducir las asignaciones en el heap en rutas críticas, comprenda sus limitaciones y evite errores habituales de uso. Practicas C# Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar C# Academy?

No se requiere experiencia previa. C# Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 3 de 4.

¿Cuánto tiempo toma la lección «ValueTask y cómo evitar asignaciones»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de C# Academy?

Sí. Cada lección de C# Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. IAsyncEnumerable y await foreach
  2. System.Threading.Channels
  3. ValueTask y cómo evitar asignaciones
  4. ConfigureAwait y contexto de sincronización
← Volver a C# Academy