CLR via C#:異常和狀態管理

異常基礎:如下所示:
1.在visual studio的監視窗口中,可以添加特殊變量名稱$exception來查看當前拋出的異常實例。
2.C#將非Exception派生異常封裝在RuntimeWrappedException類的m_wrappedException字段中,然後拋出RuntimeWrappedException異常供使用者捕獲處理。
3.調用方法前,可調用RuntimeHelpers類的EnsureSufficientExecutionStack函數來檢查棧空間是否夠用。當棧空間不夠用時,該函數就會拋出一個InsufficientExecutionStackException異常。
4.異常處理會造成額外的性能開銷,但是相比異常處理帶來的收益是遠遠大於性能損耗的。

異常結構:由一個try塊+零個catch塊+一個finally塊組成;或者由一個try塊+至少一個catch塊+可選的一個finally塊組成。參考僞代碼如下所示:

try
{
	// 需要得體地進行異常恢復和/或資源清理的代碼放在這裏
}
catch(InvalidOperationException)
{
	// 從InvalidOperationException恢復的代碼放這裏
}
catch(IOException)
{
	// 從IOException恢復的代碼放這裏
}
catch
{
	// 從除了上述異常之外的其他所有異常恢復的代碼放在這裏
	...
	// 如果什麼異常都要捕捉,通常要重新拋出異常。
	throw;
}
finally
{
	// 這裏的代碼對始於try塊的任何資源操作進行清理。
	// 不管是否拋出異常,這裏的代碼總是執行的。
}
// 如果try塊沒有拋出異常,或者某個catch塊捕獲到異常,但沒有拋出或重新拋出異常,就執行下面代碼
...

異常執行流程:當try塊的代碼中拋出異常時,執行流程如下所示:
1.CLR會從調用棧中按照從下自上的順序來搜索捕捉類型與拋出的異常相同的catch塊。如果沒有任何匹配的catch塊的話,就會發生未處理異常;否則就執行下面的步驟2。
2.CLR會執行所有內層的finally塊中的代碼。
3.當所有內層的finally塊執行完畢後,匹配異常的那個catch塊中的代碼纔開始執行。catch塊的末尾可以提供以下三種操作:
1>.重新拋出相同異常,向調用棧高一層的代碼通知該異常的發生。
2>.拋出一個不同的異常,向調用棧高一層的代碼提供更豐富的異常信息。
3>.讓線程從catch塊底部退出。如果存在finally塊的話就執行該代碼塊,然後執行finally塊後面的代碼;否則就執行最後一個catch塊後面的語句。

異常堆棧追蹤:Exception類的StackTrace屬性用來記錄堆棧執行過程。具有以下特性:
1.當構建Exception派生對象時,StackTrace屬性值爲null。
2.當一個異常拋出時,CLR記錄異常拋出位置;一個catch塊捕捉到該異常時,CLR記錄捕捉位置。當catch塊內訪問被拋出的異常實例的StackTrace屬性時,會創建一個字符串來指出從異常拋出位置到異常捕捉位置的所有函數。
3.CLR只記錄最新的異常實例的拋出位置。
4.StackTrace屬性返回的字符串不包含調用棧中比接受異常實例的那個catch塊高的任何函數。
5.CLR從程序集調試符號(存儲在.pdb文件)中獲取源代碼的文件路徑和代碼行號。
6.StackTrace屬性返回的字符串中如果不存在某些函數信息時,可能由以下原因造成:
1>.調用棧記錄的是線程的返回位置,而不是異常的來源位置。
2>.JIT編譯器對這些函數進行了內聯優化。可以使用以下方案來禁止該優化:
1>>.將函數應用MethodImplAttribute.NoInlining標誌。
2>>.使用C#編譯器的/debug開關來將程序集應用Debuggabletrribute.DisableOptimizations標誌。

自定義異常類:以自定義磁盤滿異常爲例,流程如下所示:
1.定義異常參數抽象基類。參考代碼如下所示:

[Serializable]
public abstract class ExceptionArgs
{
	public virtual String Message { get { return String.Empty; } }
}

2.定義Exception派生類,並處理序列化操作。參考代碼如下所示:

[Serializable]
public sealed class Exception<TExceptionArgs> : Exception, ISerializable where TExceptionArgs : ExceptionArgs
{
	private const String ARGS = "Args";
	
	private readonly TExceptionArgs m_args;
	public TExceptionArgs Args
	{
		get { return m_args; }
	}
	
	public Exception(String message = null, Exception innerException = null) : this(null, message, innerException)
	{
	}
	
	public Exception(TExceptionArgs args, String message = null, Exception innerException = null) : base(message, innerException)
	{
		m_args = args;
	}

	// 這個構造函數用於反序列化。由於類是密封的,所以訪問權限是私有的;
	// 如果類不是密封的話,訪問權限要爲保護的
	[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializableFormatter)]
	private Exception(SerializableInfo info, StreamingContext context) : base(info, context)
	{
		m_args = (TExceptionArgs)info.GetValue(ARGS, typeof(TExceptionArgs));
	}

	// 這個函數用於序列化。由於ISerializable接口,所以訪問權限是公共的
	[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializableFormatter)]
	public override void GetObjectData(SerializableInfo info, StreamingContext context)
	{
		info.AddValue(ARGS, m_args);
		base.GetObjectData(info, context);
	}

	public override String Message
	{
		get
		{
			String baseMsg = base.Message;
			return (m_args == null) ? baseMsg : baseMsg + " (" + m_args.Message + ")";
		}
	}

	public override Boolean Equals(Object obj)
	{
		Exception<TExceptionArgs> other = obj as Exception<TExceptionArgs>;
		if (other == null)
		{
			return false;
		}
		
		// 比較子類成員和基類成員是否相等
		return Object.Equals(m_args, other.m_args) && base.Equals(obj);
	}

	public override int GetHashCode()
	{
		return base.GetHashCode();
	}
}

3.定義一個磁盤滿的異常參數類。參考代碼如下所示:

[Serializable]
public sealed class DiskFullExceptionArgs : ExceptionArgs
{
	private readonly String m_diskpath;
	public String DiskPath
	{
		get
		{
			return m_diskpath;
		}
	}
	
	public DiskFullExceptionArgs(String diskpath)
	{
		m_diskpath = diskpath;
	}

	// 重寫Message屬性來包含自己的Message
	public override String Message
	{
		get
		{
			return (m_diskpath == null) ? base.Message : "DiskPath=" + m_diskpath;
		}
	}
}

4.拋出&捕獲磁盤滿異常。參考代碼如下所示:

try
{
	throw new Exception<DiskFullExceptionArgs>(new DiskFullExceptionArgs(@"C:\"), "the disk is full");
}
catch(Exception<DiskFullExceptionArgs> e)
{
	Console.WriteLine(e.Message);
}

約束執行區域:CER是創建可靠託管代碼機制的一部分。使用流程如下:
1.在try語句塊的前面調用RuntimeHelpers.PrepareConstrainedRegions函數,此時JIT就會遍歷整個調用圖來提前編譯與try語句塊關聯的catch和finally語句塊中的具有ReliabilityContractAttribute定製特性的代碼。如果提前編譯的代碼造成異常,那麼這個異常會在線程進入try塊之前拋出。
2.ReliabilityContractAttribute定製特性可以應用於接口,構造函數,類,程序集或者函數等。該定製特性內部使用Cer和Consistency枚舉類型來提供可靠性協定。其中Cer的定義如下:

enum Cer
{
	None, // 不進行任何CER保證
	MayFail, // 可能會失敗
	Success, // 一定會成功
}

Consistency的定義如下:

enum Consistency
{
	MayCorruptProcess, // 可能損壞進程狀態
	MayCorruptAppDomain, // 可能損壞AppDomain狀態
	MayCorruptInstance, // 可能損壞實例狀態
	WillNotCorruptState, // 不損壞任何狀態
}

3.編譯器和CLR不會驗證提前編譯的代碼是否真的符合通過ReliabilityContractAttribute來做出的保證。所以如果犯了錯誤,狀態仍有可能損壞。
4.即使提前編譯的代碼都準備好了,在調用時仍有可能造成StackOverflowException。在CLR沒有寄宿的前提下,StackOverflowException會造成CLR在內部調用Environment.FailFast來立即終止進程。在已經寄宿的前提下,RuntimeHelpers.PrepareConstrainedRegions函數會檢查是否剩下約48KB的棧空間。當棧空間不足時就在進入try語句塊前拋出StackOverflowException。
5.可以使用RuntimeHelpers.ExecuteCodeWithGuaranteedCleanUp函數來在資源保證得到清理的前提下調用該函數。
6.可以使用RuntimeHelpers.PrepareMethod或者RuntimeHelpers.PrepareDelegate以及RuntimeHelpers.PrepareContractedDelegate函數來創建可靠的函數。

代碼協定:提供了直接在代碼中聲明代碼設計決策的一種方式。具有以下特性:
1.類型爲Contract,具有以下特性:
1>.前置條件:一般用於對實參進行驗證。
2>.後置條件:函數因爲一次普通的返回或者拋出異常而終止時,對狀態進行驗證。
3>.對象不變性:在對象的整個生命週期內,確保對象的字段的良好狀態。
4>.前置條件,後置條件和對象不變性測試中引用的任何成員都一定不能改變對象的狀態。
5>.前置條件測試中引用的成員的可訪問性都至少要和定義前置條件的函數一樣;後置條件和對象不變性測試中引用的成員具有任何可訪問性,只要代碼能編譯就行。
6>.派生類型不能重寫並更改基類型中定義的虛成員的前置條件。
7>.爲了兼容之前的代碼協定,新版本的代碼中協定不能更嚴格。
2.工具爲Microsoft Code Contracts,具有以下特性:
1>.屬性頁勾選Perform Runtime Contract Checking之後,Visual Studio會在生成項目時自動調用位於C:/Program Files(x86)/Microsoft/Contracts/Bin目錄下的CCRewrite.exe。
2>.CCRewrite.exe會使任何後置條件的協定都在函數的末尾執行。
3>.CCRewrite.exe會在類型中查找標記了[ContractInvariantMethod]特性的任何函數。當查找到該函數時,CCRewrite.exe就會在每一個公共實例函數的末尾插入調用該函數的IL代碼,從而確保實例函數在調用時是否有違反協定。
4>.代碼運行時違反協定會觸發Contract的ContractFailed事件。具有以下處理情況:
1>>.註冊了ContractFailed事件的處理函數時會收到一個ContractFailedEventArgs對象。可以調用該對象的SetHandled函數來忽略違反協定的情況;可以調用該對象的SetUnwind函數來強制拋出ContractException;可以使用默認的方式進行處理。
2>>.沒有註冊了ContractFailed事件的處理函數時會使用默認的方式進行處理。
3>>.默認的方式處理流程如下:
1>>>.如果CLR已經寄宿,會向宿主通知協定失敗。
2>>>.如果CLR正在非交互式窗口站上運行應用程序,會調用Environment.FailFast來立即終止進程。
3>>>.如果編譯時在屬性頁中勾選了Assert On Contract Failure,就會出現一個斷言對話框,此時允許將一個調試器連接到你的應用程序;否則就會拋出ContractException。
5>.屬性頁勾選Perform Static Contract Checking之後,Visual Studio會在生成項目時自動調用位於C:/Program Files(x86)/Microsoft/Contracts/Bin目錄下的CCCheck.exe。
6>.CCCheck.exe會分析C#編譯器生成的IL,靜態驗證函數中是否有代碼違反協定。
7>.CCCheck.exe會嘗試證明Assert的任何條件都爲true以及假設傳給Assume的任何條件都已經爲true。一般建議先用Assert,然後在CCCheck.exe不能靜態證明表達式爲true的前提下將Assert更改爲Assume。
8>.CCRefGen.exe工具用來創建獨立的,包含協定的一個引用程序集。該程序集名字爲AssemblyName.Contract.dll且只包含對協定進行描述的元數據和IL。
9>.CCDocGen.exe用來生成具有MSDN風格的,含有協定信息的文檔。

用可靠性換取開發效率:可以使用以下方案來緩解拋出異常對狀態的破壞:
1.執行catch或finally塊中的代碼時,CLR不允許線程終止。
2.利用Contract類向函數應用代碼協定。通過代碼協定,在用實參/變量對狀態進行修改之前,可以先對這些實參/變量進行驗證。
3.利用CER來消除CLR的某些不確定性。
4.利用事務機制來確保狀態要麼都修改,要麼都不修改。
5.如果覺得狀態已經損壞到無法修復的程度,就應該使用AppDomain的Unload函數來卸載整個AppDomain,以防止它造成更多的傷害。
6.如果覺得狀態過於糟糕,以至於整個進程都應該終止,那麼應該調用Environment的靜態FailFast函數。該函數具有以下特性:
1>.在終止進程時,不會調用任何活動的try/finally塊或者Finalize函數。
2>.爲CriticalFinalizerObject實例提供進行回收的機會。
3>.將消息字符串和可選的異常實例寫入Windows Application事件日誌,生成Windows錯誤報告,創建應用程序的內存轉儲,最後終止當前進程。

設計規範和最佳實踐:如下所示:
1.善用finally塊。當使用lock,using,foreach語句或者重寫類的析構函數時,編譯器自動生成try/finally塊。其中try塊中放入開發者寫的代碼;finally塊中放入清理資源代碼。參考僞代碼如下所示:

using(FileStream fs = new FileStream(@"C:\Data.bin", FileMode.Open))
{
	Console.WriteLine(100 / fs.ReadByte());
}
// 等價於
FileStream fs = new FileStream(@"C:\Data.bin", FileMode.Open)
try
{
	Console.WriteLine(100 / fs.ReadByte());
}
finally
{
	fs.close()
}

2.不要什麼異常都捕捉。如果非要捕捉所有異常的話,最好使用throw重新拋出來交給上層去處理。
3.得體的從異常中恢復。
4.發生不可恢復的異常時回滾部分完成的操作。
5.隱藏實現細節來維繫協定。也就是在捕獲的異常裏面拋出調用者希望拋出的異常。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章