C# 類型轉換中,父類與子類之間的**向上轉型 (隱式)**、**向下轉型 (顯式、風險)**、**模式匹配**、**虛方法調用**是核心考點與常見 Bug 源頭。本文通過完整實驗代碼、內存模型圖解、IL 分析、最佳實踐,徹底講透轉換機制。
public class Animal
{
public string Name { get; set; }
public virtual void Speak() => Console.WriteLine($"[Animal] {Name} makes a sound.");
public void Breathe() => Console.WriteLine($"[Animal] {Name} breathes.");
}
public class Dog : Animal
{
public string Breed { get; set; }
public override void Speak() => Console.WriteLine($"[Dog] {Name} ({Breed}) barks: Woof!");
public void Fetch() => Console.WriteLine($"[Dog] {Name} fetches the ball.");
}
public class Cat : Animal
{
public bool IsIndoor { get; set; }
public override void Speak() => Console.WriteLine($"[Cat] {Name} meows: Meow~");
public void Purr() => Console.WriteLine($"[Cat] {Name} purrs.");
}
Dog dog = new Dog { Name = "Buddy", Breed = "Golden Retriever" };
// 隱式向上轉型
Animal animal = dog; // 編譯器自動插入轉換
// 調用虛方法:運行時調用 Dog.Speak() (多態)
animal.Speak(); // 輸出: [Dog] Buddy (Golden Retriever) barks: Woof!
// 調用非虛方法:調用 Animal.Breathe()
animal.Breathe(); // 輸出: [Animal] Buddy breathes.
// 訪問父類成員:OK
Console.WriteLine(animal.Name); // Buddy
// 訪問子類特有成員:編譯錯誤
// animal.Breed; // CS1061: 'Animal' 不包含 'Breed' 定義
// animal.Fetch(); // CS1061
本質:引用變量類型變為 `Animal`,但堆上對象實例 **不變**,依然是完整的 `Dog` 對象 (包含 `Breed` 字段、方法表指向 `Dog` 的虛方法槽)。只是編譯器限制了通過此引用可訪問的成員集合。
Animal animal = new Dog { Name = "Max", Breed = "Husky" };
// 1. 強制轉型 (舊式) - 失敗拋 InvalidCastException
try
{
Dog d = (Dog)animal; // 運行時檢查類型兼容
d.Fetch(); // 安全調用
}
catch (InvalidCastException) { Console.WriteLine("轉型失敗"); }
// 2. as 操作符 - 失敗返回 null (不拋異常)
Dog d2 = animal as Dog;
if (d2 != null) d2.Fetch(); // 安全
// 3. is 類型模式 (C# 7+) - 判斷並聲明變量 (推薦)
if (animal is Dog d3)
{
d3.Fetch(); // d3 作用域僅在 if 內
}
// 4. switch 表達式模式匹配 (C# 8+)
string sound = animal switch
{
Dog d => $"Dog {d.Breed} barks",
Cat c => $"Cat {(c.IsIndoor ? "indoor" : "outdoor")} meows",
Animal a => $"Animal {a.Name} sounds",
_ => "Unknown"
};
public static void MemoryLayoutDemo()
{
Dog dog = new Dog { Name = "Rex", Breed = "Lab" };
// 1. 同一對象,不同引用類型
Animal a = dog;
object o = dog;
Console.WriteLine($"dog.GetHashCode(): {dog.GetHashCode()}");
Console.WriteLine($"a.GetHashCode(): {a.GetHashCode()}");
Console.WriteLine($"o.GetHashCode(): {o.GetHashCode()}");
// 三者相同,證明指向同一堆對象
// 2. typeof vs GetType
Console.WriteLine($"dog.GetType(): {dog.GetType()}"); // Dog (運行時類型)
Console.WriteLine($"a.GetType(): {a.GetType()}"); // Dog (對象真實類型)
Console.WriteLine($"typeof(Animal): {typeof(Animal)}"); // Animal (編譯時類型)
Console.WriteLine($"typeof(Dog): {typeof(Dog)}"); // Dog
// 3. is/as 驗證
Console.WriteLine($"dog is Animal: {dog is Animal}"); // true
Console.WriteLine($"a is Dog: {a is Dog}"); // true
Console.WriteLine($"o is Dog: {o is Dog}"); // true
// 4. 虛方法表驗證
a.Speak(); // 通過 Animal 引用調用 Dog.Speak() - 多態
((Dog)a).Speak(); // 顯式轉型後調用 - 同一方法
}
| 場景 | 錯誤代碼 | 後果 | 正確做法 |
|---|---|---|---|
| 盲目向下轉型 | `((Dog)animal).Fetch();` | `animal` 是 `Cat` 時拋 `InvalidCastException` 崩潰 | `if (animal is Dog d) d.Fetch();` |
| 非虛方法隱藏 | `Animal a = new Dog(); a.Breathe();` 調用父類方法 | 無法多態,若子類 `new void Breathe()` 會隱藏而非重寫 | 標記 `virtual`/`override`;避免 `new` 隱藏 |
| 值類型裝箱轉型 | `object o = 123; int i = (int)o; long l = (long)o;` | 第二行拋異常:裝箱後只能拆箱為**原確切類型** | `long l = (int)o;` 或 `Convert.ToInt64(o)` |
| 接口轉型 | `IEnumerable e = new List | 若對象未實現 `IList` 拋異常 | `if (e is IList l) ...` |
// C#: Animal a = dog;
// IL:
// ldloc.0 (dog)
// stloc.1 (a) // 直接複製引用,無任何指令!隱式向上轉型零開銷。
// C#: Dog d = (Dog)a;
// IL:
// ldloc.1 (a)
// castclass Dog // 運行時類型檢查指令,不通過拋 InvalidCastException
// C#: if (a is Dog d) ...
// IL:
// ldloc.1
// isinst Dog // 檢查類型,兼容留引用,不兼容壓入 null
// dup
// brfalse.s SKIP
// stloc.2 (d) // 存儲到局部變量
`isinst` 比 `castclass` 快 (無異常開銷),`is` 模式編譯器優化極佳。