1. 介紹
在動手寫 code 前,先用白話講清楚。記得在 .NET 裡建立一個物件(更別說一個 task)是要花記憶體跟一些 CPU 時間的嗎?現在想像一個非同步 API,半數情況下結果是立刻可用的(例如從快取拿),另一半情況下要去查資料庫,所以操作就變成真正的非同步,需要 Task。這種情況很常見——像檔案快取、網路 API、物件池以及其他那些「非同步但有時瞬間完成」的場景。
如果我們總是回傳 Task,即便結果是即時的,也得去建立不必要的物件。要是可以在結果已就緒時直接回傳結果而不要 Task 呢?這就是 ValueTask 誕生的理由。
事實: 標準的 Task.CompletedTask 和 Task.FromResult(…) 確實能節省執行時間,但它們會建立共享物件,對高負載場景不一定理想。
什麼是 ValueTask
ValueTask 是一個特殊的 struct 包裝器,它可以表示已就緒的結果,或者(如果操作是真正非同步的)包含一個 Task。簡單說,它是個「包裹」,裡面要麼是直接結果,要麼是指向 Task 的參考。
主要有兩種變體:
- ValueTask — 無回傳值(像 Task)
- ValueTask<TResult> — 包裝值(類似 Task<TResult>)
比較 Task 與 ValueTask
| 類型 | 分配次數 | 是否可同步完成 | 通常用在 |
|---|---|---|---|
|
一次(heap) | 是/否 | 幾乎總是 |
|
零次/一次 | 是/否 | 優化用 |
|
零次/一次 | 是/否 | 優化用 |
何時應用 ValueTask
有條黃金法則:如果你寫的非同步方法總是在某個 async 操作之後才回傳結果,就用 Task。簡單、安全,大家都懂。
當以下情況適合用 ValueTask:
- 結果可能同步可得(例如從快取、物件池或記憶體拿),你想省掉不必要的分配。
- 真正需要非同步的情況不常發生(否則代價會被結構複製與程式複雜度吃掉)。
注意! 如果你總是回傳非同步結果——大膽用 Task。如果想讓 API 在常見的瞬時結果下「超優化」——考慮用 ValueTask。
2. 同步或非同步結果
看一個尋找使用者的函式範例。若使用者在快取裡就立刻回傳;若不在則從「資料庫」非同步載入:
// 模擬我們的使用者
public class User
{
public string Name { get; set; }
}
// 我們的 cache(非常簡單)
private readonly Dictionary<string, User> _localCache = new();
public async ValueTask<User> FindUserAsync(string name)
{
// 檢查本地快取
if (_localCache.TryGetValue(name, out var user))
{
// 結果是同步的 — 不會分配 Task!
return user;
}
// 這裡是假設的慢的非同步操作(例如從資料庫)
user = await LoadUserFromDbAsync(name);
// 放進快取以備未來使用
_localCache[name] = user;
return user;
}
private async Task<User> LoadUserFromDbAsync(string name)
{
// 模擬延遲
await Task.Delay(500);
return new User { Name = name };
}
重要: 在快取命中時我們回傳的是普通結果——沒有 Task 分配!只有在必須「取得」結果時,才會建立非同步工作。
ValueTask 在內部是怎麼運作的
ValueTask 內部可能包含已就緒的值,或是一個 Task 的參考:
ValueTask result = ValueTask.CompletedTask;
ValueTask<int> valueResult = new ValueTask<int>(42);
ValueTask<int> valueResult2 = new ValueTask<int>(Task.Run(() => 42));
使用 await 時,編譯器會自動處理:如果結果是即時的,就不會建立多餘的物件。
關於 await 與 ValueTask 的重要事項
async 方法可以很好地與 await 和 ValueTask 搭配:
public async ValueTask PingAsync()
{
// ...
await Task.Delay(10);
}
但如果你把 ValueTask 實例儲存起來,想晚點再等它,請記住:不能 對同一個實例做多次 await。Task 可以被重複等待,ValueTask 則不行。
錯誤:對 ValueTask 重複 await
ValueTask<int> task = ComputeAsync();
// 這是可以的
int a = await task;
// 這是錯誤!同一個 ValueTask 實例不允許第二次 await:
// int b = await task; // 不行!
3. 重寫我們的 console 應用部分
假設我們正在做一個簡單的電子書閱讀器:部分文本已在快取、其餘的會非同步下載。用 ValueTask 實作一個取得書籍第一行的最佳化方法:
private readonly Dictionary<string, string> _bookCache = new();
public async ValueTask<string> GetFirstLineOfBookAsync(string title)
{
if (_bookCache.TryGetValue(title, out var bookText))
{
var firstLine = bookText.Split('\n')[0];
return firstLine;
}
// 假設這裡是非同步下載書籍
var downloadedBook = await DownloadBookTextAsync(title);
_bookCache[title] = downloadedBook;
return downloadedBook.Split('\n')[0];
}
private async Task<string> DownloadBookTextAsync(string title)
{
// 模擬延遲(例如從網路下載)
await Task.Delay(1000);
return $"Book: {title}\nThis is the first line.\nSecond line...";
}
這樣一來,當書已在快取時,ValueTask 可以省掉建立 Task。
4. 有用的細節
提示:何時不該使用 ValueTask
如果你的 API 永遠是非同步的(例如總是跟網路打交道)——就用 Task,比較簡單也更安全。
如何把 ValueTask 轉成 Task,以及反過來
有時 ValueTask 無法直接傳給需要 Task 的 API,這時用 .AsTask():
ValueTask<int> valueTask = ComputeAsync();
Task<int> task = valueTask.AsTask();
若要從 Task 產生 ValueTask — 直接用 Task 建構 ValueTask:
Task<int> task = ComputeAsyncTask();
ValueTask<int> valueTask = new ValueTask<int>(task);
比較:ValueTask vs Task
| 特性 | |
|
|---|---|---|
| 重複 await | 允許 | 不允許 |
| 快速回應時的分配 | 會有 | 沒有(若結果即時) |
| 相容性介面 | 廣泛支援 | 需要額外包裝 |
| 適用場景 | 各處適用 | 僅作為優化 |
| Pool(物件池) | 使用共用池 | 沒有,為 struct |
| 簡單度 | 簡單 | 較複雜 |
在現實中使用 ValueTask
public async ValueTask<string> GetMessageAsync(int id)
{
if (_messageCache.TryGetValue(id, out var value))
return value;
var result = await LoadMessageFromDbAsync(id);
_messageCache[id] = result;
return result;
}
面試應答要點: 如果被問到如何進一步提升帶有快取的非同步 API 的效能——提到 ValueTask。
與 LINQ 及 IAsyncEnumerable<T> 的相容範例
想把 ValueTask 用在非同步 LINQ(IAsyncEnumerable<T>)時,.NET 是支援的(例如 ToListAsync 等方法):
public async ValueTask<List<int>> GetPrimeNumbersAsync()
{
// 假設部分數字已計算,部分是非同步產生
// ...(範例為簡短省略)
return new List<int> { 2, 3, 5, 7, 11 };
}
5. 結論與實作要點
- 當大量時間內結果是立即可用(例如來自快取)時,使用 ValueTask 做優化。
- 不要「隨便」用 ValueTask——會增加程式複雜度與錯誤機率。
- 不要對同一個 ValueTask 做多次 await。需要多次等待就轉成 Task。
- 與 Task 的相容性通常透過 .AsTask()。
- 記住 ValueTask 是個 struct,複製時行為會不同,使用時要小心。
6. 使用 ValueTask 的常見錯誤
錯誤 №1:對同一個 ValueTask 實例重複 await。 ValueTask 是 struct,不能被 await 兩次。第二次 await 會導致例外或行為不正確。
錯誤 №2:在不了解相容性下使用 ValueTask。 有些第三方 API 需要 Task,直接傳遞 ValueTask 會出問題。
錯誤 №3:不必要地使用 ValueTask。 如果結果總是非同步的,使用 ValueTask 只會讓程式更複雜但沒有實際收益。
錯誤 №4:複製 ValueTask 結構。 被複製的 struct 在 await 時可能表現異常,要盡量操作原始實例。
GO TO FULL VERSION