重學c#系列——異常(六)

團隊buff工具人發表於2020-07-31

前言

使用者覺得異常是不好的,認為出現異常是寫的人的問題。

這是不全面,錯誤的出現並不總是編寫程式的人的原因,有時會因為應用程式的終端使用者引發的動作或執行程式碼的環境而發生錯誤,比如你用android4去安裝現在的微信,或者說我們寫的android程式不需要相容android4,需要的效果就是在android4上安裝然後崩潰。

我們編寫程式碼的人需要做的是預測程式中出現的錯誤,並進行相應的處理,甚至有時候我們應該主動丟擲異常。

無論編寫的程式碼技術多麼強,程式都必須處理可能發生的錯誤,比如說做客戶端的時候,突然網路中斷了,那麼後續操作需要中斷,我們需要做相應的處理;又比如說我們的檔案出現許可權問題,那麼我們又需要處理。

這裡介紹一下為什麼我們呼叫io方法,如果是去讀取一個不存在的檔案為什麼丟擲的是異常,而不是返回一個空的物件。

因為判斷是這樣子的,先把程式碼貼出來:

string filePath = Environment.CurrentDirectory + "/content.txt";
Stream source = new FileStream(filePath, FileMode.Open, FileAccess.Read);

如果filePath 不存在那麼將會丟擲異常。

之所以丟擲異常是這樣子的,FileStream是要去建立一個檔案流,按理說檔案流建立不成功,那麼我的目的沒有達到那麼就應該返回異常,異常就是我並沒有達到我使用的目的。

那麼為啥我們呼叫indexof可以返回-1,表示我們沒有找到呢?是因為返回-1是約定,如果有約定那麼可以不用丟擲異常。

那麼我們是否可以約定new FileStream返回為空呢?是不能的,因為存在歧義。檔案流建立不成功,到底是檔案不存在呢?還是許可權問題呢?這些都是需要告訴我們的。

總結一下為什麼我們需要主動丟擲異常:

1.呼叫的目的沒有達到就應該丟擲異常。

2.如果有約定就可以按照約定,如果約定存在歧義,那麼不能使用約定。

好吧,那麼開始正文吧。

正文

異常處理功能使用 try、catch 和 finally 關鍵字來嘗試執行可能失敗的操作、在你確定合理的情況下處理故障,以及在事後清除資源。

在try、catch、finally中,catch塊的執行方式值得關注:

如果引發異常的語句不在 try 塊內或者包含該語句的 try 塊沒有匹配的 catch 塊,

則執行時將檢查呼叫方法中是否有 try 語句和 catch 塊。 執行時將繼續呼叫堆疊,搜尋相容的 catch 塊。 

在找到並執行 catch 塊之後,控制權將傳遞給 catch 塊之後的下一個語句。

舉個例子:

static void Main(string[] args)
{
	FileStream sw = null;
	try
	{
	   sw = new FileStream(@"C:\test\test.txt", FileMode.Open, FileAccess.ReadWrite)
	   sw.Write(new byte[1]);
	}
	catch (DirectoryNotFoundException ex)
	{
		//檔案目錄沒有找到異常
		Console.WriteLine(ex);
	}
	catch (FileNotFoundException ex)
	{
		//檔案沒有找到異常
		Console.WriteLine(ex);
	}
	catch (IOException ex)
	{
		//io操作異常
		Console.WriteLine(ex);
	}
	finally
	{
		sw?.Flush();
		sw?.Close();
	}
}

沒有找到找到相應的catch,那麼會一步一步往下找。

首先是檢視DirectoryNotFoundException ,然後是FileNotFoundException,只要是io操作出現問題都會是IOException。

那麼我該如何知道DirectoryNotFoundException、FileNotFoundException、IOException他們的關係呢?

這時候就需要看圖了,圖不大,可以記下來。

如圖:

上面這些是常用的,可以說是必會的吧,下面介紹一下他們的作用。

異常 詳細
ArgumentException 方法引數異常
ArgumentNullException 引數為空異常
ArgumentOutOfRangeException 索引小於零或超出陣列邊界時,嘗試對陣列編制索引時引發
ArithmeticException 算術運算期間出現的異常的基類,例如 DivideByZeroException 和 OverflowException。
OverflowException 當在檢查的上下文中執行的算術、強制轉換或轉換運算導致溢位時引發的異常
StackOverflowException 執行堆疊由於有過多掛起的方法呼叫而用盡時引發;通常表示非常深的遞迴或無限遞迴。
IOException 發生I/O錯誤時引發的異常。
FileLoadException 找到託管程式集但不能載入時引發的異常
FileNotFoundException 檔案沒有找到
EndOfSteamExcetion 讀操作試圖超出流的末尾時引發的異常。
DriveNotFoundException 當嘗試訪問的驅動器或共享不可用時引發的異常。
ApplicationException 用作應用程式定義的異常的基類
TargetInvocationException 由通過反射呼叫的方法引發的異常
CompositionException 表示在 CompositionContainer 物件中進行組合期間發生一個或多個錯誤時引發的異常。
ChangeRejectedException 一個指示部件在組合期間是否已遭拒絕的異常。

有些異常是我們寫程式碼不應該去產生異常的,比如說ApplicationException作為基類的異常,如果出現這些異常一般是我們程式碼寫的有問題,

而不是我們的主觀因素。應從 Exception 類(而不是 ApplicationException 類)派生自定義異常。 不應在程式碼中引發 ApplicationException 異常,除非你

打算重新引發原始異常,否則不應捕獲 ApplicationException 異常。 這裡舉個例子:TargetInvocationException的基類是ApplicationException,這個是

由通過反射呼叫的方法引發的異常。如果產生這個問題,可以想象是我們反射使用的有問題。同樣如果我們去寫反射的程式,我們不應該去catch這個,而是讓他直接

報錯,表示程式碼就不應該這麼寫,出錯然後去解決。

好吧,回到原問題上,假如沒有找到相容性的catch塊那麼會怎麼樣呢?

那麼實驗一下吧。

實驗如下:

會丟擲異常的,所以我們寫程式碼的時候需要確保最後一個一定能捕獲到異常的,如果sw.Write(new byte[1]);出現錯誤那麼非託管資源就沒有釋放。

那麼如何萬一我們沒有捕獲到異常也能是否非託管資源呢?使用using。

static void Main(string[] args)
{
	try
	{
		using (var sw = new FileStream(@"C:\test\test.txt", FileMode.Open, FileAccess.ReadWrite))
		{
			sw.Write(new byte[1]);
		} 
	}
	catch (DirectoryNotFoundException ex)
	{
		//檔案目錄沒有找到異常
		Console.WriteLine(ex);
	}
	finally
	{
	}
}

這樣寫無論是否異常,那麼都會釋放非託管資源。這個是可以實驗的,我把我的實驗貼一下。

上面的說明已經被關閉了,我們再次關閉的時候將會產生異常。這個using可以看下IL,就清楚其中的原理。

異常過濾器

異常過濾器是c#的東西,這裡只是做簡單的介紹,後面異常程式碼優化章節中會重點介紹。

public static bool ConsoleLogException(Exception e)
{
	var oldColor = Console.ForegroundColor;
	Console.ForegroundColor = ConsoleColor.Red;
	Console.WriteLine("Error:{0}",e);
	Console.ForegroundColor = oldColor;
	return false;
}
static void Main(string[] args)
{
	int i = 10;
	try
	{
		throw new TimeoutException("time out");
	}
	catch (Exception e) when(ConsoleLogException(e))
	{

	}
	catch (TimeoutException e) when (i == 10)
	{

	}
	finally
	{
		Console.ReadKey();
	}
}

輸出結果:

上面說明一個問題,無論catch是否匹配,when都會執行。
同樣來做一個實驗,判斷when 如果不匹配也就是沒有catch塊執行那麼會怎麼樣?

static void Main(string[] args)
{
	int i = 10;
	try
	{
		throw new TimeoutException("time out");
	}
	catch (Exception e) when(ConsoleLogException(e))
	{

	}
	catch (TimeoutException e) when (i == 9)
	{
		Console.WriteLine("enter timeoutexception");
	}
	finally
	{
		Console.ReadKey();
	}
}

結果:

重新丟擲異常

為什麼要重新丟擲異常呢?

static int GetValueFromArray(int[] array, int index)
{
	try
	{
		return array[index];
	}
	catch (System.IndexOutOfRangeException ex)
	{
		System.ArgumentException argEx = new System.ArgumentException("Index is out of range", "index", ex);
		throw argEx;
	}
}

本來是要丟擲IndexOutOfRangeException 改為了ArgumentException 這是為什麼呢?

原因如下:使用者要呼叫的是我們的方法GetValueFromArray,傳入的是引數,方法沒有越界這麼一說,而不是一個陣列,所以我們要爭對我們的目的來確定我們丟擲的異常。

使用者自定義異常

自定義異常水比較深,在此只做一個簡單的介紹,單獨一節補齊。

class CostumExcetion:Exception
{
	public CostumExcetion(string message) : base(message)
	{

	}
}

呼叫如下:

static void Main(string[] args)
{
	int i = 10;
	try
	{
		throw new CostumExcetion("自定義異常");
	}
	catch (Exception e) when(ConsoleLogException(e))
	{

	}
	catch (CostumExcetion e) when (i == 10)
	{
		Console.WriteLine(e.Message);
	}
	finally
	{
		Console.ReadKey();
	}
}

呼叫者資訊

現在又一個需要,知道try中出現錯誤,但是我需要知道是那一會出現錯誤,這個怎麼破呢?

也就是說我們希望定位到行級,那麼我們就需要呼叫者資訊了。

在異常中,我們又很多方法呼叫一個方法,然後在這個方法中出現問題,我們需要知道是怎麼報錯的,到底是哪個函式呼叫報錯的,這時候我們需要使用查詢到呼叫者資訊。

舉個例子:

public class CallerInformationHelper
{
	public void Log([CallerLineNumber]int line = -1, [CallerFilePath] string path = null, [CallerMemberName]string name = null)
	{
		Console.WriteLine((line<0)?"no line":"Line"+line);
		Console.WriteLine((path==null)?"No file path":path);
		Console.WriteLine((name==null)?"No Member name":name);
		Console.WriteLine();
	}
}

呼叫:

 CallerInformationHelper helper = new CallerInformationHelper();
 helper.Log();

結果:

因為異常整理較多,所以後續還有兩節整理。

1.異常注意事項,本章續。

2.解析盛派框架如何自定義異常類(以前做小程式的時候看過原始碼)。

以上只是個人理解,如有問題請指出。如果學習,看文件最佳。

相關文章