在某些場合我們需要手動對 object 類型進行轉型的操作,目前常用的轉型方式有以下幾種:

  1. 轉換運算式(Cast expression)
  2. as 運算子
  3. pattern matching

首先建立兩個類別 CarBus,我們可以設定乘客數量,並且添加使用者自訂的轉換型別方法讓我們可以直接進行隱含轉換。

public class Car
{
	private int _passenger;
	public int Passenger
	{
		get { return _passenger; }
		set
		{
			if (value > 3)
				throw new ArgumentException("Too Many People");
			_passenger = value;
		}
	}
	
	public Car(int passenger)
	{
		Passenger = passenger;
	}

	public static implicit operator Bus(Car car)
	{
		return new Bus(car.Passenger);
	}
}

public class Bus
{
	private int _passenger;
	public int Passenger
	{
		get { return _passenger; }
		set { _passenger = value; }
	}

	public Bus(int passenger)
	{
		Passenger = passenger;
	}

	public static implicit operator Car(Bus bus)
	{
		return new Car(bus.Passenger);
	}
}

型別轉換

為了方便測試,我們建立一個 Factory 類別,並回傳 object 類型。

public class Factory
{
    public static object GetObject(int passenger, Type t)
	{
		return Activator.CreateInstance(t, passenger);
	}
}

最傳統的的轉換方式需要搭配 try/catch 來攔截例外錯誤,還需要另外檢查 null 類型,因為 null 可以直接轉換成任何參考型別。

// 1.轉換運算式(Cast expression)
object o = Factory.GetObject(3, typeof(Car));
try
{	        
	if (o != null)
	{
		Car t = (Car)o;
		Console.WriteLine($"Passenger Count: {t.Passenger}");
	}
}
catch (Exception ex)
{
	Console.WriteLine("Error");
}

使用 as 運算子如果轉換錯誤並不會拋出例外,因此只要檢查 null 類型就好。

// 2. as 運算子
object o = Factory.GetObject(3, typeof(Car));
Car t = o as Car;
if (t != null)
	Console.WriteLine($"Passenger Count: {t.Passenger}");
else
	Console.WriteLine("Error");

使用 is 運算子搭配 pattern matching 寫法可以更精簡。

// 3. pattern matching
object o = Factory.GetObject(3, typeof(Car));
if (o is Car t)
	Console.WriteLine($"Passenger Count: {t.Passenger}");
else
	Console.WriteLine("Error");

使用者自訂的轉換型別方法

接下來測試一下之前寫的自訂的轉換型別方法,根據程式碼可以得知 CarBus 可以來回轉換,唯一的限制是 Car 類型 Passenger 不能超過三人否則會報錯。

從三名乘客的 Car 隱含轉換成 Bus 類別可以轉換成功。

var c = new Car(3);
Bus b = c;
Console.WriteLine($"Passenger Count: {b.Passenger}");

從四名乘客的 Bus 隱含轉換成 Car 類別因為超過三人所以會報錯。

var b = new Bus(4);
Car c = b;
Console.WriteLine($"Passenger Count: {b.Passenger}");

另外我們也可以用明確轉型,但是要注意自訂的轉換型別方法只能用 Cast expression 處理轉型操作,沒辦法使用 aspattern matching

//無法編譯
Car c = new Car(3);
Bus b = c as Bus;
Console.WriteLine($"Passenger Count: {b.Passenger}");

Car c = new Car(3);
Bus b = (Bus)c;
Console.WriteLine($"Passenger Count: {b.Passenger}");

那麼使用上一節的 Factory.GetObject 方法建立出來的隱藏型別的 object 是否也能進行隱含轉換呢?

object o = Factory.GetObject(3, typeof(Car));
if (o != null)
{
	Bus c = (Bus)o;
	Console.WriteLine($"Passenger Count: {c.Passenger}");
}
else
	Console.WriteLine("Error");

答案是不行的,因為編譯器不會知道 o 的執行期型別,編譯器認為沒有 objectBus 的使用者自訂轉換型別方法。

Foreach

在這個例子中我們傳入一個非泛型的 IEnumerable,並且使用 foreach 遍歷每一個元素。 可以發現 foreach 背後將元素轉換為 Car 型別,並不是由我們使用一開始提到的三種轉換方式來轉換。

public void UseCollection(IEnumerable array)
{
	foreach (Car c in array)
	{
		Console.WriteLine("Success");
	}
}

使用反編譯軟體查看背後的程式碼,發現他內部是使用 Cast expression 來轉換,所以要注意可能會拋出 InvalidCastException

IEnumerator enumerator = array.GetEnumerator ();
try
{
     while (enumerator.MoveNext ())
     {
          Car c = (Car)enumerator.Current;
          Console.WriteLine ("Success");
     }
}
finally
{
     IDisposable disposable = enumerator as IDisposable;
     if (disposable != null)
     {
          disposable.Dispose ();
     }
}

Summary

最好是盡量避免轉型的操作,如果一定要的話優先使用 asis 來進行,不僅可以避免拋出 InvalidCastException, 而且只需要查結果是否為 null 就好,另外語意也更接近英文可讀性會提高,另外如果方法有提供泛型的版本那就優先使用。