Chapter 5 - Item 48 : Prefer the Strong Exception Guarantee

Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得

程式的“狀態”是開發者要關心的,任何操作都有可能拋出例外;如何確保在擲出例外時程式的可靠度(資料的完整性)是一門課題。以下作者提出幾種針對確保資料完整性的做法。

主要分為三個步驟:

1. 建立物件的副本。
2. 修改副本的狀態(包含可能拋出例外的操作)。
3. 若操作成功,將副本與原始值交換;完成更新操作。

範例:

PayrollData _payrollData = new PayrollData();
public void PhysicalMove(string title, decimal newPay)
{
	// PayrollData 是值型別。
	// 如果參數無效,建構子會拋出例外。
	var d = new PayrollData(title, newPay, _payrollData.DateOfHire);

	// 如果 d 正確建構,則將目前值與 d 交換。
	_payrollData = d;
}

public struct PayrollData
{
	public string Title { get; set; }
	public decimal Pay { get; set; }
	public DateTime DateOfHire { get; set; }

	public PayrollData(string title, decimal pay, DateTime dateOfHire)
	{
		Title = title;
		Pay = pay;
		DateOfHire = dateOfHire;
	}
}

若欲修改的目標是參考型別,情況就不太一樣。

private IList<PayrollData> _data;
public IList<PayrollData> MyCollection => _data;

public void UpdateData()
{
	// 可能擲出例外的操作。
	var temp = UnreliableOperation();

	// 如果正確執行,將 temp 參考指定給 _data。
	_data = temp;
}

private List<PayrollData> UnreliableOperation()
{
	return new List<PayrollData>();
}

外部可能已透過 MyCollection 屬性取得 _data 參考,當呼叫 UpdateData 後;外部仍參考著原始的 _data 參考(除非再次透過 MyCollection 取得參考)。這樣會造成資料不同步的問題。

修改後版本:

private IList<PayrollData> _data;
public IList<PayrollData> MyCollection => _data;

public void UpdateData()
{
	// 可能擲出例外的操作。
	var temp = UnreliableOperation();

	// 如果正確執行,清空 _data 並將 temp 元素加入 _data。
	_data.Clear();
	foreach (var item in temp)
		_data.Add(item);
}

private List<PayrollData> UnreliableOperation()
{
	return new List<PayrollData>();
}

也可以將資料物件封裝起來:

public class Envelop : IList<PayrollData>
{
	private List<PayrollData> _data = new List<PayrollData>();

	public void SafeUpdate(IEnumerable<PayrollData> souceList)
	{
		// 可能拋出例外的操作
		var updates = new List<PayrollData>(souceList.ToList());
		
		// 若沒有例外發生,更新 _data。
		_data = updates;
	}

	public PayrollData this[int index]
	{
		get => _data[index];
		set => _data[index] = value;
	}

	public int Count => _data.Count;

	public bool IsReadOnly => ((IList<PayrollData>)_data).IsReadOnly;

	public void Add(PayrollData item) => _data.Add(item);

	public void Clear() => _data.Clear();

	public bool Contains(PayrollData item) =>_data.Contains(item);

	public void CopyTo(PayrollData[] array, int arrayIndex)
		=> _data.CopyTo(array,arrayIndex);
		
	public IEnumerator<PayrollData> GetEnumerator()
		=> _data.GetEnumerator();
		
	public int IndexOf(PayrollData item)
		=> _data.IndexOf(item);
		
	public void Insert(int index, PayrollData item)
		=> _data.Insert(index,item);
	
	public bool Remove(PayrollData item)
		=> _data.Remove(item);
	
	public void RemoveAt(int index)
		=> _data.RemoveAt(index);
	
	IEnumerator IEnumerable.GetEnumerator()
		=> _data.GetEnumerator();	
}

讓 Envelop 實作 IList,讓外部直接存取 _data 內元素而非是擁有 _data 參考副本;這樣就解決的資料不同步的問題。

結論:
1. 在拋出例外時,同時需注意資料物件的狀態正確性。
2. 並非所有操作都需要拋出例外。
    a. Dispose, Finalizer 皆不可拋出例外,避免資源洩漏。
    b. Exception filter 不可拋出例外,這會造成外部無法取得原始的例外訊息。
    c.​ 委派鏈中方法不應拋出例外,其中某一方法擲出例外會造成後續方法不會執行。