Effective C# 原則29:僅在對基類進行強制更新時才使用new修飾符
Item 29: Use the new Modifier Only When Base Class Updates Mandate It
你可以用new修飾符來重新定義一個從基類中繼承來的非虛成員。你可以這樣做,但並不意味著需要這樣做。重新定義非虛方法會導致方法含意的混亂。如果兩個相關的類是繼承關係,那麼很多開發人員可能會立即假設兩段代碼塊是做完全相同的事情,而且他們也會這麼認為:
object c = MakeObject( );
// Call through MyClass reference:
MyClass cl = c as MyClass;
cl.MagicMethod( );
// Call through MyOtherClass reference:
MyOtherClass cl2 = c as MyOtherClass;
cl2.MagicMethod( );
一但使用了new修飾符以後,問題就完全不一樣了:
public class MyClass
{
public void MagicMethod( )
{
// details elided.
}
}
public class MyOtherClass : MyClass
{
// Redefine MagicMethod for this class.
public new void MagicMethod( )
{
// details elided
}
}
這樣的實際操作會讓很多開發人員迷惑。因為當你在同一個對象上調用相同的函數時,一定希望它們執行同樣的代碼。但實際上是,一但你用不同的引用來調用同名的函數,它們的行為是不一樣的,這感覺非常糟糕。它們是不一致的。一個MyOtherClass類型的對象所表現的行為會因為你引用的方式不一樣而有所不同。這就是new修飾符用在非虛成員上的後果。其實這隻是讓你在類的名字空間中添加了一個不同的方法(雖然它們的函數名是相同的)。
非虛方法是靜態繫結的,不管哪裡的代碼,也不管在哪裡引用,MyClass.MagicMethod() 總是嚴格的調用類中所定義的函數。並不會在運行時在衍生類別中尋找不同的版本。另一方面,虛函數動態。運行時會根據不同的類型對象調用不同的版本。
建議大家避免使用new修飾符來重新定義非虛函數,這並不要太多的解釋,就像推薦大家在定義一個基類時應該用虛方法一樣。一個類庫的設計者應該按照合某種約設計虛函數。也就表示你期望任何衍生類別都應該修改虛函數的實現。虛函數的集合就相當於是定義了一個行為的集合,這些行為是希望在派生中重新實現的。設計預設的虛函數就是說派生可以修改類中的所有虛的行為。這確實是說你不想考慮所有衍生類別可能要修改行為的分歧問題。相反,你可以把時間花在考慮把什麼樣的方法以及屬性設計成多態的。當然,只有它們是虛行為的時候才能這樣做。不要考慮這樣會限制類的使用者。相反,應該認為這是給類型的使用者定義行為提供了一個入口嚮導。
有且只有一種情況要使用new修飾符,那就是把類整合到一個已經存在的基類上時,而這個基類中已經使用了存在的方法名,這時就要使用new了(譯註:就是說基類與衍生類別都已經存在了,是後來添加的繼承關係,結果在添加繼承關係時,發現兩個類中使用了同樣的方法名,那麼就可以在衍生類別中添加一個new來解決這個問題)。因為有些代碼已經依懶於類的方法名,或者已經有其它程式集在使用這個方法。例如你在庫中建立了下面的類,使用了在另一個庫中定義的BaseWidget:
public class MyWidget : BaseWidget
{
public void DoWidgetThings( )
{
// details elided.
}
}
你完成了你的widget, 而且使用者可以使用它。然而你卻發現BaseWidget公司發布了一個新的版本。而這正是你所渴望的,於是你立即購買並編譯你的MyWidget類。結果失敗了,因為BaseWidget的傢伙們已經添加了他們自己的DoWidgetThings 方法:
public class BaseWidget
{
public void DoWidgetThings()
{
// details elided.
}
}
這是個難題,你的基類中隱藏了一個方法,而這又是在你的類的名字空間中。有兩個方法解決這個問題,一個就是修改你的類中的方法名:
public class MyWidget : BaseWidget
{
public void DoMyWidgetThings( )
{
// details elided.
}
}
或者使用new修飾符:
public class MyWidget : BaseWidget
{
public new void DoWidgetThings( )
{
// details elided.
}
}
如果你可以拿到所有使用MyWidget類的原始碼,那麼你應該選擇修改方法名,因為這對於今後的運行會更簡單。然而,如果你已經向全世界的人發布了MyWidget類,這會迫使所有使用者來完成這個眾多的改變。這正是new修飾符容易解決的問題,你的使用者不用修改DoWidgetThings()方法而繼續使用它。沒有人會調用到BaseWidget.DoWidgetThings()方法,因為(對於衍生類別而言)它們根本不存在。在更新一個基類時,如果發現它與先前申明的成員發生了衝突,可以用new修飾符來解決這個問題。
當然,在某些時候,你的使用者可能想調用基類的Widget.DoWidgetThings()方法,這時你又回到了原來的問題上:兩個方法看上去是一樣的,但其實是不同的。考慮到new修飾長期存在的歧意問題,有時候,還是在短期上麻煩一下,修改方法名為上策。(譯註:長痛不如短痛。呵呵)
new修飾符必須小心謹慎的使用。如果它是有歧意的,你就在類上建立了個模糊的方法。這隻有在特殊情況下才使用,那就是升級基類時與你的類產生衝突時。即使在這種情況下,也應該小心的使用它。最重要的是,其它任何時候都不要用它。
==========================
Item 29: Use the new Modifier Only When Base Class Updates Mandate It
You use the new modifier on a class member to redefine a nonvirtual member inherited from a base class. Just because you can do something doesn't mean you should, though. Redefining nonvirtual methods creates ambiguous behavior. Most developers would look at these two blocks of code and immediately assume that they did exactly the same thing, if the two classes were related by inheritance:
object c = MakeObject( );
// Call through MyClass reference:
MyClass cl = c as MyClass;
cl.MagicMethod( );
// Call through MyOtherClass reference:
MyOtherClass cl2 = c as MyOtherClass;
cl2.MagicMethod( );
When the new modifier is involved, that just isn't the case:
public class MyClass
{
public void MagicMethod( )
{
// details elided.
}
}
public class MyOtherClass : MyClass
{
// Redefine MagicMethod for this class.
public new void MagicMethod( )
{
// details elided
}
}
This kind of practice leads to much developer confusion. If you call the same function on the same object, you expect the same code to execute. The fact that changing the reference, the label, that you use to call the function changes the behavior feels very wrong. It's inconsistent. A MyOtherClass object behaves differently in response to how you refer to it. The new modifier does not make a nonvirtual method into a virtual method after the fact. Instead, it lets you add a different method in your class's naming scope.
Nonvirtual methods are statically bound. Any source code anywhere that references MyClass.MagicMethod() calls exactly that function. Nothing in the runtime looks for a different version defined in any derived classes. Virtual functions, on the other hand, are dynamically bound. The runtime invokes the proper function based on the runtime type of the object.
The recommendation to avoid using the new modifier to redefine nonvirtual functions should not be interpreted as a recommendation to make everything virtual when you define base classes. A library designer makes a contract when making a function virtual. You indicate that any derived class is expected to change the implementation of virtual functions. The set of virtual functions defines all behaviors that derived classes are expected to change. The "virtual by default" design says that derived classes can modify all the behavior of your class. It really says that you didn't think through all the ramifications of which behaviors derived classes might want to modify. Instead, spend the time to think through what methods and properties are intended as polymorphic. Make thoseand only thosevirtual. Don't think of it as restricting the users of your class. Instead, think of it as providing guidance for the entry points you provided for customizing the behavior of your types.
There is one time, and one time only, when you want to use the new modifier. You add new to incorporate a new version of a base class that contains a method name that you already use. You've already got code that depends on the name of the method in your class. You might already have other assemblies in the field that use this method. You've created the following class in your library, using BaseWidget that is defined in another library:
public class MyWidget : BaseWidget
{
public void DoWidgetThings( )
{
// details elided.
}
}
You finish your widget, and customers are using it. Then you find that the BaseWidget company has released a new version. Eagerly awaiting new features, you immediately purchase it and try to build your MyWidget class. It fails because the BaseWidget folks have added their own DoWidgetThings method:
public class BaseWidget
{
public void DoWidgetThings()
{
// details elided.
}
}
This is a problem. Your base class snuck a method underneath your class's naming scope. There are two ways to fix this. You could change that name of your DoWidgetThings method:
public class MyWidget : BaseWidget
{
public void DoMyWidgetThings( )
{
// details elided.
}
}
Or, you could use the new modifier:
public class MyWidget : BaseWidget
{
public new void DoWidgetThings( )
{
// details elided.
}
}
If you have access to the source for all clients of the MyWidget class, you should change the method name because it's easier in the long run. However, if you have released your MyWidget class to the world, that would force all your users to make numerous changes. That's where the new modifier comes in handy. Your clients will continue to use your DoWidgetThings() method without changing. None of them would be calling BaseWidget.DoWidgetThings() because it did not exist. The new modifier handles the case in which an upgrade to a base class now collides with a member that you previously declared in your class.
Of course, over time, your users might begin wanting to use the Base Widget.DoWidgetThings() method. Then you are back to the original problem: two methods that look the same but are different. Think through all the long-term ramifications of the new modifier. Sometimes, the short-term inconvenience of changing your method is still better.
The new modifier must be used with caution. If you apply it indiscriminately, you create ambiguous method calls in your objects. It's for the special case in which upgrades in your base class cause collisions in your class. Even in that situation, think carefully before using it. Most importantly, don't use it in any other situations.