Setiap method asinkron harus menyatakan return type yang menggambarkan bentuk hasilnya. Pilihan yang tersedia bukan hanya Task dan Task<T>; ada pula ValueTask<T> untuk jalur panas yang sering mengembalikan hasil dari cache, serta void yang hanya sah untuk event handler. Memilih return type yang tepat membuat API asinkron jelas bagi pemanggilnya dan menghindari pemborosan alokasi.

Ringkasan Pilihan Return Type #

Return typeKegunaanCatatan
TaskPekerjaan tanpa nilai kembaliPasangan await tanpa hasil
Task<T>Pekerjaan dengan satu nilai TPilihan utama untuk sebagian besar kasus
ValueTask<T>Hasil sering sudah tersedia sinkronMenghemat alokasi task pada jalur panas
voidEvent handler asinkron sajaException tidak dapat ditangkap pemanggil

ValueTask<T> membungkus hasil atau Task<T> dalam satu struct, sehingga kasus dengan hasil cache tidak perlu mengalokasikan objek task sama sekali.

Contoh: Tiga Bentuk Hasil Asinkron #

Kode berikut memperlihatkan Task, Task<string>, dan ValueTask<int> yang berperilaku berbeda sesuai sumber datanya:

csharp
static async Task SimpanLogAsync(string pesan)
{
    await Task.Delay(30);
    Console.WriteLine($"Log tersimpan: {pesan}");
}

static async Task<string> AmbilSapaanAsync(string nama)
{
    await Task.Delay(30);
    return $"Halo, {nama}!";
}

static int _stokCache = 42;

static ValueTask<int> AmbilStokAsync(bool perluCekGudang)
{
    if (!perluCekGudang)
    {
        return new ValueTask<int>(_stokCache);
    }
    return new ValueTask<int>(CekGudangAsync());
}

// Pemanggil:
await SimpanLogAsync("Aplikasi kasir dimulai");

string sapaan = await AmbilSapaanAsync("Budi");
Console.WriteLine(sapaan);

ValueTask<int> stok = AmbilStokAsync(perluCekGudang: false);
Console.WriteLine($"Stok dari cache: {await stok}");

stok = AmbilStokAsync(perluCekGudang: true);
Console.WriteLine($"Stok dari gudang: {await stok}");

Output yang Dihasilkan:

text
Log tersimpan: Aplikasi kasir dimulai
Halo, Budi!
Stok dari cache: 42
Stok dari gudang: 37

Mari kita bedah tiga bentuknya:

  1. await SimpanLogAsync(...): pemanggil hanya menunggu pekerjaan selesai; tidak ada nilai yang perlu ditampung.
  2. string sapaan = await AmbilSapaanAsync(...): hasil await bertipe string sesuai T pada Task<string>.
  3. return new ValueTask<int>(_stokCache): jalur cache mengembalikan nilai secara langsung tanpa membuat Task<int>, sehingga tidak ada alokasi task.
  4. return new ValueTask<int>(CekGudangAsync()): jalur gudang membungkus Task<int> biasa, karena pekerjaan memang belum selesai saat pemanggilan.

Bab berikutnya membahas cara merangkai beberapa task menjadi satu alur kerja yang utuh.