C#基礎之前處理器,異常處理

上善若泪發表於2024-12-01

目錄
  • 1 前處理器
    • 1.1 簡介
      • 1.1.1 定義
      • 1.1.2 前處理器指令列表
    • 1.2 指令示例詳解
      • 1.2.1 #define 和 #undef 前處理器
      • 1.2.2 條件指令:#if, #elif, #else 和 #endif
      • 1.2.3 綜合示例
  • 2 異常處理
    • 2.1 簡介
      • 2.1.1 定義
      • 2.1.2 異常類
    • 2.2 異常處理
      • 2.2.1 常規處理
      • 2.2.2 不指定具體異常
        • 2.2.2.1 catch 中不指定異常型別
        • 2.2.2.2 throw 不指定異常物件
        • 2.2.2.3 throw ex 與 throw 的區別
      • 2.2.3 使用using
    • 2.3 自定義異常

1 前處理器

1.1 簡介

1.1.1 定義

前處理器指令(Preprocessor Directives)指導編譯器在實際編譯開始之前對資訊進行預處理。
透過這些指令,可以控制編譯器如何編譯檔案或編譯哪些部分。常見的前處理器指令包括條件編譯、宏定義等。
所有的前處理器指令都是以 # 開始,且在一行上,只有空白字元可以出現在前處理器指令之前。前處理器指令不是語句,所以它們不以分號 ; 結束

C# 編譯器沒有一個單獨的前處理器,但指令被處理時就像是有一個單獨的前處理器一樣。在 C# 中,前處理器指令用於在條件編譯中起作用。與 C 和 C++ 不同的是,它們不是用來建立宏。一個前處理器指令必須是該行上的唯一指令。

使用前處理器指令特點:

  • 提高程式碼可讀性:使用#region可以幫助分隔程式碼塊,提高程式碼的組織性。
  • 條件編譯:透過 #if 等指令可以在開發和生產環境中編譯不同的程式碼,方便除錯和釋出。
  • 警告和錯誤:透過 #warning#error 可以在編譯時提示開發人員注意特定問題

1.1.2 前處理器指令列表

指令 描述
#define 定義一個符號,可以用於條件編譯
#undef 取消定義一個符號
#if 開始一個條件編譯塊,如果符號被定義則包含程式碼塊
#elif 如果前面的 #if 或 #elif 條件不滿足,且當前條件滿足,則包含程式碼塊
#else 如果前面的 #if 或 #elif 條件不滿足,則包含程式碼塊
#endif 結束一個條件編譯塊
#warning 生成編譯器警告資訊
#error 生成編譯器錯誤資訊
#region 標記一段程式碼區域,可以在IDE中摺疊和展開這段程式碼,便於程式碼的組織和閱讀
#endregion 結束一個程式碼區域
#line 更改編譯器輸出中的行號和檔名,可以用於除錯或生成工具的程式碼
#pragma 用於給編譯器傳送特殊指令,例如禁用或恢復特定的警告
#nullable 控制可空性上下文和註釋,允許啟用或禁用對可空引用型別的編譯器檢查

1.2 指令示例詳解

1.2.1 #define 和 #undef 前處理器

#define 用於定義符號(通常用於條件編譯),#undef 用於取消定義符號。
#define 允許定義一個符號,這樣,透過使用符號作為傳遞給 #if 指令的表示式,表示式將返回 true

下面的程式說明了這點:

#define PI
using System;
namespace PreprocessorDAppl
{
   class Program
   {
      static void Main(string[] args)
      {
         #if (PI)
            Console.WriteLine("PI is defined");
         #else
            Console.WriteLine("PI is not defined");
         #endif
         Console.ReadKey();
      }
   }
}

1.2.2 條件指令:#if, #elif, #else 和 #endif

條件指令用於測試符號是否為真。如果為真,編譯器會執行 #if 和下一個指令之間的程式碼。
常見運算子有:== (等於),!= (不等於),&& (與),|| (或)
也可以用括號把符號和運算子進行分組。條件指令用於在除錯版本或編譯指定配置時編譯程式碼。一個以 #if 指令開始的條件指令,必須顯示地以一個 #endif 指令終止。

#define DEBUG

#if DEBUG
    Console.WriteLine("Debug mode");
#elif RELEASE
    Console.WriteLine("Release mode");
#else
    Console.WriteLine("Other mode");
#endif

例項

#define DEBUG
#define VC_V10
using System;
public class TestClass
{
   public static void Main()
   {
      #if (DEBUG && !VC_V10)
         Console.WriteLine("DEBUG is defined");
      #elif (!DEBUG && VC_V10)
         Console.WriteLine("VC_V10 is defined");
      #elif (DEBUG && VC_V10)
         Console.WriteLine("DEBUG and VC_V10 are defined");
      #else
         Console.WriteLine("DEBUG and VC_V10 are not defined");
      #endif
      Console.ReadKey();
   }
}

1.2.3 綜合示例

#define DEBUG

#if DEBUG
    Console.WriteLine("Debug mode");
#elif RELEASE
    Console.WriteLine("Release mode");
#else
    Console.WriteLine("Other mode");
#endif

#warning This is a warning message
#error This is an error message

#region MyRegion
    // Your code here
#endregion

#line 100 "MyFile.cs"
    // The next line will be reported as line 100 in MyFile.cs
    Console.WriteLine("This is line 100");
#line default
    // Line numbering returns to normal

#pragma warning disable 414
    private int unusedVariable;
#pragma warning restore 414

#nullable enable
    string? nullableString = null;
#nullable disable

2 異常處理

2.1 簡介

2.1.1 定義

異常是在程式執行期間出現的問題。C# 中的異常是對程式執行時出現的特殊情況的一種響應,異常提供了一種把程式控制權從某個部分轉移到另一個部分的方式。C# 異常處理時建立在四個關鍵詞之上的:trycatchfinallythrow

  • try:一個 try 塊標識了一個將被啟用的特定的異常的程式碼塊。後跟一個或多個 catch 塊。
  • catch:程式透過異常處理程式捕獲異常,catch 關鍵字表示異常的捕獲。
  • finallyfinally 塊用於執行給定的語句,不管異常是否被丟擲都會執行,如果開啟一個檔案,不管是否出現異常檔案都要被關閉。
  • throw:當問題出現時,程式丟擲一個異常。使用 throw 關鍵字來完成。

2.1.2 異常類

C# 異常是使用類來表示的。C# 中的異常類主要是直接或間接地派生於 System.Exception 類。
System.ApplicationException 類支援由應用程式生成的異常。所以程式設計師定義的異常都應派生自該類。
System.SystemException 類是所有預定義的系統異常的基類。

派生自 System.SystemException 類的預定義的異常類:

異常類 描述
System.IO.IOException 處理 I/O 錯誤
System.IndexOutOfRangeException 處理當方法指向超出範圍的陣列索引時生成的錯誤
System.ArrayTypeMismatchException 處理當陣列型別不匹配時生成的錯誤
System.NullReferenceException 處理當依從一個空物件時生成的錯誤
System.DivideByZeroException 處理當除以零時生成的錯誤
System.InvalidCastException 處理在型別轉換期間生成的錯誤
System.OutOfMemoryException 處理空閒記憶體不足生成的錯誤
System.StackOverflowException 處理棧溢位生成的錯誤

2.2 異常處理

2.2.1 常規處理

C# 以 try 和 catch 塊的形式提供了一種結構化的異常處理方案。使用這些塊,把核心程式語句與錯誤處理語句分離開。

這些錯誤處理塊是使用 try、catch 和 finally 關鍵字實現的。下面是一個當除以零時丟擲異常的例項:

using System;
namespace ErrorHandlingApplication
{
    class DivNumbers
    {
        int result;
        DivNumbers()
        {
            result = 0;
        }
        public void division(int num1, int num2)
        {
            try
            {
                result = num1 / num2;
            }
            catch (DivideByZeroException e)
            {
                Console.WriteLine("Exception caught: {0}", e);
            }
            finally
            {
                Console.WriteLine("Result: {0}", result);
            }

        }
        static void Main(string[] args)
        {
            DivNumbers d = new DivNumbers();
            d.division(25, 0);
            Console.ReadKey();
        }
    }
}

執行結果:
Exception caught: System.DivideByZeroException: Attempted to divide by zero. 
at ...
Result: 0

2.2.2 不指定具體異常

用法 特點 使用場景
catch 不指定異常型別 捕獲所有異常,但無法訪問異常物件
throw 重新丟擲當前異常,保留原始堆疊資訊 捕獲異常後記錄日誌或執行其他操作再丟擲
throw ex 丟擲捕獲的異常,但丟失原始堆疊資訊 不推薦,除非需要自定義異常或修改異常資訊

2.2.2.1 catch 中不指定異常型別

在 catch 中不指定異常型別,這種方式會捕獲所有型別的異常
語法如下:

try
{
    // 可能引發異常的程式碼
}
catch
{
    // 捕獲所有異常
    Console.WriteLine("發生了一個異常");
}

特點:

  • 捕獲所有異常:catch 塊不指定異常型別時,任何異常都會被捕獲。
  • 不提供異常上下文:因為沒有捕獲具體的異常物件,無法訪問異常的詳細資訊(如訊息、堆疊跟蹤等)。
  • 使用場景:適合處理一些通用的異常情況,但不建議濫用,因為可能會掩蓋真正的問題。

2.2.2.2 throw 不指定異常物件

throw 後面可以不指定異常物件。這種方式會重新丟擲當前捕獲的異常,並保留原始異常的堆疊跟蹤資訊。
語法如下:

try
{
    // 可能引發異常的程式碼
}
catch (Exception)
{
    Console.WriteLine("捕獲到異常,但繼續丟擲");
    throw; // 重新丟擲當前異常
}

特點:

  • 重新丟擲原始異常:保留原始異常的堆疊跟蹤資訊。
  • 使用場景:適用於需要在捕獲異常後進行一些處理(如記錄日誌)然後繼續將異常傳遞給上層呼叫者。

2.2.2.3 throw ex 與 throw 的區別

catch 塊中,既可以使用 throw; 重新丟擲當前異常,也可以使用 throw ex; 丟擲捕獲的異常物件。

複製程式碼
try
{
    throw new InvalidOperationException("測試異常");
}
catch (Exception ex)
{
    Console.WriteLine("捕獲異常並重新丟擲");
    throw; // 重新丟擲,保留原始堆疊資訊
    // throw ex; // 丟擲 ex,但堆疊資訊會被重置
}

區別:

  • throw:重新丟擲當前異常,保留原始異常的堆疊跟蹤。
  • throw ex:丟擲捕獲的異常物件,但會重置堆疊跟蹤資訊,使得原始異常的來源資訊丟失。

2.2.3 使用using

using 語句確保在塊結束時自動釋放資源,即使發生異常也是如此。這在處理實現了 IDisposable 介面的物件時特別有用,例如檔案流、資料庫連線等。

using (ResourceType resource = new ResourceType())
{
    // 使用資源的程式碼
}

在這個語法中,ResourceType 必須實現 IDisposable 介面。using 語句會在塊結束時自動呼叫 Dispose 方法,釋放資源。

使用 using 語句處理檔案讀取寫入的示例:

using System;
using System.IO;

public class Program
{
    public static void Main()
    {
        string inputPath = "input.txt";
        string outputPath = "output.txt";		
        // C# 8.0 之前版本
        using (StreamReader reader = new StreamReader(inputPath))
        using (StreamWriter writer = new StreamWriter(outputPath))
        // 適用於 C# 8.0 及以上版本
 		//using (StreamReader reader = new StreamReader(inputPath),writer = new StreamWriter(outputPath))
        {
            string content = reader.ReadToEnd();
            writer.Write(content);
        }
    }
}

2.3 自定義異常

自定義的異常類是派生自 ApplicationException 類。下面的例項演示了這點:

using System;
namespace UserDefinedException
{
   class TestTemperature
   {
      static void Main(string[] args)
      {
         Temperature temp = new Temperature();
         try
         {
            temp.showTemp();
         }
         catch(TempIsZeroException e)
         {
            Console.WriteLine("TempIsZeroException: {0}", e.Message);
         }
         Console.ReadKey();
      }
   }
}
public class TempIsZeroException: ApplicationException
{
   public TempIsZeroException(string message): base(message)
   {
   }
}
public class Temperature
{
   int temperature = 0;
   public void showTemp()
   {
      if(temperature == 0)
      {
         throw (new TempIsZeroException("Zero Temperature found"));
      }
      else
      {
         Console.WriteLine("Temperature: {0}", temperature);
      }
   }
}