More Effective C# 26.在 Iterators 與 Async 方法中使用區域函式啟動立即錯誤回報 More Effective C# 26.在 Iterators 與 Async 方法中使用區域函式啟動立即錯誤回報(Enable Immediate Error Reporting in Iterators and Async Methods using Local Functions)

Published on 2024年11月18日 星期一

這個做法討論 Local Functions 的應用大部分的內容在 Effective C# 29 有稍微提到過,主要就是 C# 有一些延遲執行特性的方法,當這個延遲特性與一些需要馬上回報的功能會產生衝突, 例如要立即檢查傳入參數,發現錯誤後馬上拋出 ArgumentException,如果這段檢查邏輯是包含在距由延遲特性的方法裡面,那麼檢查邏輯就會被延後到呼叫發法時才會檢查。

比較常用具有延遲特性的方法有 Iterators 與 Async 方法,下面就是常見的 Iterators 方法,雖然有包含參數檢查邏輯但實際上要等到最後一行 foreach 讀取資料的時候才會拋出錯誤。

public IEnumerable<T> GenerateSample<T>(IEnumerable<T> sequence, int sampleFrequency)
{
    if (sequence == null)
        throw new ArgumentException("Source sequence cannot be null.");
    if (sampleFrequency < 1)
        throw new ArgumentException("Sample frequency must be positive.");

    int index = 0;
    foreach (var item in sequence)
    {
        if (index % sampleFrequency == 0)
            yield return item;
    }
}

// 錯誤未即時拋出,要等到真正讀取資料才會拋出。
var samples = GenerateSample(fullSequence, -1);

foreach (var item in samples)
{
    Console.WriteLine(item);
}

同樣的問題也會發生在 async 方法上,這裡的 LoadMessage 方法經由編譯器處理過後會回傳一個 Task 物件用來管理狀態和非同步工作, 必須等到 await Task 發生時才會真正執行 LoadMessage 方法。

async void Main()
{
	// 呼叫方法
	var task = LoadMessage(null);
	Console.WriteLine("Not Throw");
	await task;
}

public async Task<string> LoadMessage(string userName)
{
	if (string.IsNullOrWhiteSpace(userName))
		throw new ArgumentException("Invalid username.");

	return userName ?? "No message.";
}

解決的方法也很簡單,就是把驗證的邏輯拆分成另一個方法,讓它不再具備延遲特性。 這裡使用的是 Local Functions 寫法,能夠把一個方法包裝在另一個方法裡面,這個寫法的好處就是避免被其他方法給呼叫, 因為 generateSampleImpl 是 private 的,所以只有 GenerateSample 才有能力呼叫它。

同時也代表你沒辦法跳過檢查邏輯直接執行 generateSampleImpl 方法,如果不是用 Local Functions 寫法就有可能被不知情的使用者跳過檢查邏輯。

public IEnumerable<T> GenerateSample<T>(IEnumerable<T> sequence, int sampleFrequency)
{
	if (sequence == null)
		throw new ArgumentException("Source sequence cannot be null.");
	if (sampleFrequency < 1)
		throw new ArgumentException("Sample frequency must be positive.");

	return generateSampleImpl();

	IEnumerable<T> generateSampleImpl()
	{
		int index = 0;
		foreach (var item in sequence)
		{
			if (index % sampleFrequency == 0)
				yield return item;
		}
	}
}

同樣的寫法也可以套用在 async 方法上,注意到 async 是寫在 Local Functions 上,這樣外層可以做到馬上執行。

public Task<string> LoadMessage(string userName)
{
    if (string.IsNullOrWhiteSpace(userName))
        throw new ArgumentException("Invalid username.");

    return loadMessageImpl();

    async Task<string> loadMessageImpl()
    {
        return userName ?? "No message.";
    }
}

Summary

這個做法把 Effective C# 29 提到的延遲驗證的問題複習了一下,並且改成用 Local Functions 的寫法,來避免不知情的人略過驗證邏輯 直接執行主要方法,而且 Local Functions 也提供了封裝性,避免方法被外部的人呼叫。