More Effective C# 25.Array 引數限制只使用 params 陣列 More Effective C# 25.Array 引數限制只使用 params 陣列(Limit Array Parameters to Params Arrays)

Published on 2024年11月18日 星期一

這個做法提出使用陣列做為參數可能會引發某些問題,並且建議改用 params 陣列。

在 C# 中,當陣列做為輸入參數時支持 Covariance,代表我們能夠將一個衍生類別的陣列賦值給一個基底類別。 下面這段程式能夠通過編譯,但是會拋出 ArrayTypeMismatchException,就是因為 Covariance 允許把不同類型的陣列傳遞給方法參數, 代表你可以把方法當初設計預期以外的陣列型別丟進去也不會影響編譯,但是會影響最終結果。

void Main()
{
	D1[] storage = new D1[10];
	FillArray(storage);
}

void FillArray(B[] array)
{
	for (int i = 0; i < array.Length; i++)
		array[i] = new B();
}

class B { }
class D1 : B { }

另一個特性是不支持 Contravariance,所以你不能直接把 B 物件放進 D 物件中。

void Main()
{
	B[] storage = new B[10];
	// 以下操作會產生編譯錯誤:
	FillArray(storage);
}

void FillArray(D1[] array)
{
	for (int i = 0; i < array.Length; i++)
		array[i] = new D1();
}

class B { }
class D1 : B { }

上面提到的兩點都是使用陣列的時候需要考量到的問題點,如果你只會把參數用來迭代並不會修改內容的話,建議可以改用 IEnumerable<T>, 因為它是 immutable type 內部沒有提供修改的方法。

void ProcessItems(IEnumerable<int> items)
{
    foreach (var item in items)
    {
        Console.WriteLine(item);
    }
}

如果確定要修改參數的話,應該考慮回傳一份新的修改後結果而不是去直接修改傳入的內容。

IEnumerable<int> IncrementValues(IEnumerable<int> items)
{
    foreach (var item in items)
    {
        yield return item + 1;
    }
}

如果今天傳入的參數數量會變動的話則可以考慮使用 Params Array,相比之下普通陣列還需要另外宣告類型,Params 陣列使用起來更靈活, 你不用再去建立一個只用來傳遞參數陣列,同時也可以省略掉準備參數時的轉型工作。

void Main()
{
	PrintItems("one", "two", "three");
}

void PrintItems(params object[] items)
{
	foreach (var item in items)
		Console.WriteLine(item);
}

也可以做到傳入不同型別或者不傳入任何參數。

void Main()
{
	PrintItems("one", "two", 3);
	PrintItems();
}

void PrintItems(params object[] items)
{
	foreach (var item in items)
		Console.WriteLine(item);
}

可以看一下普通陣列與 Params 陣列的呼叫區別,普通陣列在同樣的方法邏輯會拋出 ArrayTypeMismatchException,Params 陣列則不會, 不過這裡的 Params 陣列是編譯器幫忙產生的,呼叫方法的人也沒要要回傳結果,你去修改它的內容也沒有意義。

void Main()
{
	string[] labels = new string[] { "one", "two", "three" };
	ReplaceIndices(labels);
	ReplaceIndicesParams("one", "two", "three");
}

void ReplaceIndicesParams(params object[] array)
{
	Console.WriteLine(array);
	for (int i = 0; i < array.Length; i++)
		array[i] = i;
	Console.WriteLine(array);
}

void ReplaceIndices(object[] array)
{
	Console.WriteLine(array);
	for (int i = 0; i < array.Length; i++)
		array[i] = i;
	Console.WriteLine(array);
}

也可以選擇回傳修改的陣列,這個效果就跟 IEnumerable<T> 類似,對呼叫者來說是一份新的陣列。

void Main()
{
	var result = ReplaceIndicesParams("one", "two", "three");
	Console.WriteLine(result);
}

object[] ReplaceIndicesParams(params object[] array)
{
	for (int i = 0; i < array.Length; i++)
		array[i] = i;
	return array;
}

你也是可以傳入一般陣列但是同樣也會拋出 ArrayTypeMismatchException,這種寫法就不是由編譯器幫你產生參數,屬於呼叫者的問題。

void Main()
{
	string[] labels = new string[] { "one", "two", "three" };
	ReplaceIndicesParams(labels);
}

object[] ReplaceIndicesParams(params object[] array)
{
	for (int i = 0; i < array.Length; i++)
		array[i] = i;
	return array;
}

Summary

使用陣列做為參數要考慮 Covariance 和 Contravariance 會產生的問題,建議是修改設計使用 IEnumerable<T>Params Array ,避免這些潛在的問題。