這個做法建議不要在 Func 與 Action 的執行過程中拋出 Exceptions,因為這會導致物件狀態難以還原回原狀態的情況發生。

例如下面這段程式碼會將所有員工的薪資增加 5%,假如說在運行期間拋出錯誤這個時候可能已經有部分員工已經加了薪另一部份員工並沒有被加到薪, 這種數據錯亂的情況就很難還原回原狀態,最糟情況只能手動一一重新檢查數據。

void Main()
{
	var allEmployees = FindAllEmployees();
	allEmployees.ForEach(e => e.MonthlySalary *= 1.05M);
	Console.WriteLine(allEmployees);
}

public List<Employee> FindAllEmployees()
{
	var result = new List<Employee>
	{
		new Employee{
			Name = "User1",
			Classification = EmployeeType.Salary,
			MonthlySalary = 1000,
			YearsOfService = 1
		},
		new Employee{
			Name = "User2",
			Classification = EmployeeType.Salary,
			MonthlySalary = 3000,
			YearsOfService = 21
		},
		new Employee{
			Name = "User3",
			Classification = EmployeeType.Salary,
			MonthlySalary = 1500,
			YearsOfService = 1
		}
	};

	return result;
}

public class Employee
{
	public string Name { get; set; }
	public EmployeeType Classification { get; set; }
	public int YearsOfService { get; set; }
	public decimal MonthlySalary { get; set; }
}

public enum EmployeeType
{
	Salary
}

會發生這種問題也很明顯,因為我們直接修改了源數據的元素,雖然這時數據狀態可能是錯亂的但還是可以進行操作,這種例外錯誤的設計方式叫做 Basic exception guarantee

其它還有兩種,分別是 No-fail guaranteeStrong exception guarantee,以下是它們各自的定義。

  1. No-throw guarantee: 保證你寫的方法不會拋出例外錯誤,也是約束最強的一種設計,一但方法內部有錯誤產生需要馬上進行內部處理,不能讓外部的使用者發覺。
  2. Strong exception guarantee: 可以拋出錯誤但需要保證發生錯誤時數據要返回原始狀態,要保證錯誤產生時不能有副作用。
  3. Basic exception guarantee: 最寬鬆的一種設計,錯誤產生時數據可能是錯亂的狀態但還是可以操作數據。

所以在加薪的這種需求至少需要 Strong exception guarantee 保證發生錯誤的時候還可以回滾成原始狀態,那麼要怎麼做才能確保我們的程式碼 能有 Strong exception guarantee?

再次看範例的關鍵程式就是下面的修改邏輯,所以最簡單的方式就是確保執行這段程式的期間不要拋出錯誤就好。

allEmployees.ForEach(e => e.MonthlySalary *= 1.05M);

在這個範例是把所有員工都查詢出來後一一進行加薪操作,所以可能發生的問題點之一就在於員工裡面的數據可能造成錯誤的產生, 例如離職的員工,那麼我們就可以先把可能發生錯誤的數據過濾掉提前避免錯誤的產生。

allEmployees.FindAll(
    e => e.Classification == EmployeeType.Active).
    ForEach(e => e.MonthlySalary *= 1.05M);

但有的時候問題只是偶發性的,這樣就很難找出問題點那麼就可以利用做法 37 提到的積極求值,也就是用 ToList() 先在記憶體內做數據備份, 等到錯誤真的產生的時候在進行回滾覆蓋,不過這種方式是透過額外備份一組資料所以這樣做會增加程式碼運作的負擔。

下面就是使用 ToList() 方法先備份數據,這樣成功之後才會賦值給 allEmployees,如果運作途中拋出錯誤也不會運行到賦值的那段程式碼, 這樣就能保證 allEmployees 一值都是原始數據。

var updates = (from e in allEmployees
               select new Employee
               {
                   EmployeeID = e.EmployeeID,
                   Classification = e.Classification,
                   YearsOfService = e.YearsOfService,
                   MonthlySalary = e.MonthlySalary *= 1.05M
               }).ToList();
allEmployees = updates;

Summary

只要是運行的 Action 與 Func 拋出錯誤,那麼就很難確保正確的狀態,問題點就發生在直接修改原數據, 所以搭配 ToList() 方法先備份數據,並且確定運行都無誤才對原數據進行覆蓋是相對可行的處理方式。