More Effective C# 19.避免在基底類別中定義方法多載 More Effective C# 19.避免在基底類別中定義方法多載(Avoid Overloading Methods Defined in Base Classes)

這個做法建議不要在衍生類別多載基底類別存在的方法,因為會造成誤會以及使用上的困擾。

由於基底類別的開發者與衍生類別的開發者可能為不同人,可能會有某些熱門方法名稱大家都很愛用,導致在衍生類別寫出改變基底行為的多載方法, 這個問題跟以前提到的使用 new 宣告新方法類似,因為它是透過隱藏基底方法而不是覆寫方法,所以結果就是使用時可能會與預期的不同。

多載的解析規則是相當複雜的,它的目的是從多的候選方法中挑選出我們需要的那個方法,牽涉到的因素很多就有可能會挑選到錯誤的方法,可能的因素如以下:

  1. 方法是否存在於目標類別上
  2. 方法是否存在於目標類別的基底類別上
  3. 方法是否存在於目標類別的介面上
  4. 是否為類別擴充方法
  5. 是否為泛型方法
  6. 是否為泛型擴充方法
  7. 是否有可選引數的多載方法

由上面提到的因素來看,多載要解析出正確的方法已經很困難了,如果在加上一個本做法提到的因素只會讓結果更難預測也會混淆使用者,所以要解決 本做法提到的問題也不難,只要使用不同的名稱也就是避免與基底類別重名就好了。

假如我有以下類別,Animal 類別的 Foo 方法的參數為 Apple,但是衍生類別 Tiger 建立了多載方法並把它改成 Fruit 參數,

public class Fruit { }
public class Apple : Fruit { }

public class Animal
{
    public void Foo(Apple parm) =>
        Console.WriteLine("In Animal.Foo");
}
public class Tiger : Animal
{
    public void Foo(Fruit parm) =>
        Console.WriteLine("In Tiger.Foo");
}

當執行基底類別的方法時,結果很明顯是輸出 In Animal.Foo

var obj1 = new Animal();
obj1.Foo(new Apple());

當執行衍生類別的方法時,結果竟然都是輸出 In Tiger.Foo,有些人可能會認為第一個呼叫應該要輸出 In Animal.Foo 才對, 因為基底類別有提供 Apple 參數的版本,使用這個版本並不需要轉型才對,儘管基底類別有一個完全符合的方法, 但正確的規則是編譯器會認為最後一層的衍生類別提供的方法才是最佳解。

var obj2 = new Tiger();
obj2.Foo(new Apple());
obj2.Foo(new Fruit());

再來看看這個案例,雖然執行期型別為 Tiger 但是編譯時期型別為 Animal,所以會去調用基底類別的 Foo 方法,所以會輸出 In Animal.Foo

Animal obj3 = new Tiger();
obj3.Foo(new Apple());

由於方法解析規則以衍生類別為優先,基底類別方法可能會被隱藏起來。所以需要通過 casting 才能顯式呼叫基類的方法

var obj4 = new Tiger();
((Animal)obj4).Foo(new Apple());
obj4.Foo(new Fruit());

如果牽涉到泛型集合就又會有 Effective C# 22 提到的 covariancecontravariance 問題,新版的結果會輸出 In Tiger.Baz2。 由於在 C# 4 以前的版本並沒有這個概念,所有引數都是 Invariant,結果就會輸出 In Animal.Baz2

void Main()
{
	var sequence = new List<Apple> { new Apple(), new Apple() };
	var obj2 = new Tiger();
	obj2.Baz(sequence);
}

public class Fruit { }
public class Apple : Fruit { }

public class Animal
{
	public void Foo(Apple parm) => Console.WriteLine("In Animal.Foo");
	public void Bar(Fruit parm) => Console.WriteLine("In Animal.Bar");
	public void Baz(IEnumerable<Apple> parm) => Console.WriteLine("In Animal.Baz2");
}
public class Tiger : Animal
{
	public void Foo(Fruit parm) => Console.WriteLine("In Tiger.Foo");
	public void Bar(Fruit parm1, Fruit parm2 = null) => Console.WriteLine("In Tiger.Bar");
	public void Baz(IEnumerable<Fruit> parm) => Console.WriteLine("In Tiger.Baz2");
}

所以要避免誤會建議是直接避免在衍生類別使用相同的方法名稱,直接從根本解決這個問題。

public class Animal
{
    public void Foo(Apple parm) => Console.WriteLine("In Animal.Foo");
}
public class Tiger : Animal
{
    public void FooForFruit(Fruit parm) => Console.WriteLine("In Tiger.FooForFruit");
}

Summary

這個做法解釋了為何不要在衍生類別多載基底類別存在的方法,並且解釋多載解析的判斷因素,改用不一樣的名稱可以從根本杜絕這個問題。