很多開發者問為什麼命名參數和可選參數直到現在才加入到C#語言中。從其它語言中的使用情況看,它們都是很好的特性. 特別是當方法有一大堆的參數,但是很多都是可以使用默認值的時候。Office COM APIs就是很明顯的例子。
C#直到4才加入這個特性的是因為有其它更迫切的事情要做。另外,命名參數和可選參數,的確是很好用,但是也會導致很多問題. 當遇到可選參數和重載的時候,這些問題更加明顯. 你可能聽過C#團隊說過每個新的語言特性開始都是得到負面的評價,然後必須足夠吸引開發者,從而被接受. 這篇文章中,我將從多個方面討論,命名參數和可選參數限制了以後的組件發布和使得代碼更加難讀. 你將學到如何在自己的代碼中避免這些陷阱和一些為什麼這些特性這麼晚才出現C#的原因.
方法底層如何運行的
定義一個方法:
public static IEnumerable<Record> ApplyFilters(
NameFilter nameFilter = default(NameFilter),
CityFilter cityFilter = default(CityFilter),
AgeFilter ageFilter = default(AgeFilter)
)
這個方法看起來實現了很多的方法重載. 你可能認為,很簡單就能夠實現對一個方法的重載. 在你的想法裡,你認為它已經實現了下面這些方法:
public static IEnumerable<Record> ApplyFilters(
// no parms means no filter.
)
public static IEnumerable<Record> ApplyFilters(
NameFilter nameFilter = default(NameFilter)
)
public static IEnumerable<Record> ApplyFilters(
CityFilter cityFilter = default(CityFilter)
)
public static IEnumerable<Record> ApplyFilters(
AgeFilter ageFilter = default(AgeFilter)
)
public static IEnumerable<Record> ApplyFilters(
NameFilter nameFilter = default(NameFilter),
CityFilter cityFilter = default(CityFilter),
)
public static IEnumerable<Record> ApplyFilters(
NameFilter nameFilter = default(NameFilter),
AgeFilter ageFilter = default(AgeFilter)
)
public static IEnumerable<Record> ApplyFilters(
CityFilter cityFilter = default(CityFilter),
AgeFilter ageFilter = default(AgeFilter)
)
public static IEnumerable<Record> ApplyFilters(
NameFilter nameFilter = default(NameFilter),
CityFilter cityFilter = default(CityFilter),
AgeFilter ageFilter = default(AgeFilter)
)
事實上卻不是上面的方式. 編譯器相反,意識到ApplyFilters方法中有可選參數, 並且在調用方法的時候,會給可選參數賦值. 這就是為什麼下面的例子不會產生歧義 (因為第一個方法,不會為後面可選參數設置默認值)
public static IEnumerable<Record> ApplyFilters(
NameFilter nameFilter
)
public static IEnumerable<Record> ApplyFilters(
NameFilter nameFilter = default(NameFilter),
CityFilter cityFilter = default(CityFilter),
AgeFilter ageFilter = default(AgeFilter)
)
// 函數調用:
ApplyFilters(new NameFilter());
這個例子說明有確定參數的函數比有可選參數的函數更加有匹配的優先級. 所以, 上面調用的是第一個方法. 到現在為止,一切看起來很簡單,這個很多開發人員都知道. 但是我們還只是掀開面紗, 現在, 想想下面這個例子會發生什麼:
// NameFilter, CityFilter, and AgeFilter 都是繼承自抽象類Filter
public static IEnumerable<Record> ApplyFilters(
NameFilter nameFilter = default(NameFilter),
CityFilter cityFilter = default(CityFilter),
AgeFilter ageFilter = default(AgeFilter)
)
{
Console.WriteLine("First version");
return null;
}
public static IEnumerable<Record> ApplyFilters(
Filter filter
)
{
Console.WriteLine("second version");
return null;
}
// sample call:
ApplyFilters(new NameFilter());
ApplyFilters(new CityFilter());
調用函數的參數並不完全匹配第二個方法. 第一個調用中, 在第一個調用中, 參數倒是完全匹配一個方法中的參數. 編譯器是怎麼搞定上面的情況的呢? 第一個調用匹配第一個方法, 第二個調用匹配第二個方法. 這個已經在C#規范7.5.3.2 (第四版)中說明了.
規范裡面講,為了解決重載的問題, 任何可選參數,只要是沒有具體值,就會從方法標記中刪除. 所以編譯器會認為上面的兩個方法,可以解釋成下面這種:(當後2個可選參數沒有值的時候)
public static IEnumerable<Record> ApplyFilters(
NameFilter nameFilter = default(NameFilter),
)
public static IEnumerable<Record> ApplyFilters(
Filter filter
)
上面兩個方法中, 第一個更加匹配第一個調用. 如果調用參數的更加符合一個函數的可選參數
如果你想強制編譯器調用其它的方法, 你需要使用命名參數:
// NameFilter, CityFilter, and AgeFilter all derive from
// an abstract Filter class.
public static IEnumerable<Record> ApplyFilters(
NameFilter nameFilter = default(NameFilter),
CityFilter cityFilter = default(CityFilter),
AgeFilter ageFilter = default(AgeFilter)
)
public static IEnumerable<Record> ApplyFilters(
Filter filter
)
ApplyFilters(filter: new NameFilter());
ApplyFilters(cityFilter: new CityFilter());
第二次調用會等價於:
• ApplyFilters(default(NameFilter), cityFilter: new CityFilter());
當有繼承關系和虛方法的情況下, 使用命名參數和可選參數會變得更加復雜:
public abstract class Animal
{
public abstract void Feed(string food = "chow");
}
public class Cat : Animal
{
public override void Feed(string catfood = "cat chow")
{
Console.WriteLine(catfood);
}
}
public class Dog : Animal
{
public override void Feed(string dogfood = "dog chow")
{
Console.WriteLine(dogfood);
}
}
下面調用這些方法:
var d = new Dog();
d.Feed();
var c = new Cat();
c.Feed();
Animal thing = new Dog();
thing.Feed();
d.Feed() 輸出“dog chow”, c.Feed() 輸出 “cat chow”. thing.Feed() 輸出 “chow”. 解釋一下為什麼:
一個簡單的解釋是一位thing是Animal類型, 所以Feed()方法來自Animal類eclaration in the Animal class.
現在, 看看更加復雜的情況. 在dog類中添加一個新的方法
public void Feed(string dogfood = "dog chow", bool moist = false)
{
Console.WriteLine("{0} {1}", moist ? "moist" : "dry", dogfood);
}
奇跡發生了, 居然調用了這個新方法. 因為這個規則: 優先使用繼承子類中的方法.
There are still several other rules that must be examined before we even consider the ramifications of upgrading a component with these methods declared. Now, let’s consider a virtual method with some optional parameters. Consider this change to the Cat class:
public class Cat : Animal
{
public override void Feed(string catfood) // parameter is not optional
{
Console.WriteLine(catfood);
}
}
// usage:
var c = new Cat();
c.Feed();
這個Feed()方法的調用不能編譯通過. 因為類Cat中沒有無參數的方法.如果轉換成類型Animal, 就能工作:
• var c = new Cat();
• ((Animal)c).Feed();
當前調用的Cat類中的Feed方法, 因為這是一個虛方法.
You should re-declare any of the optional parameters on each override of a virtual method to avoid confusing client code this way. When you do that, it’s critically important that you ensure the default value of that parameter is the same for each override. Otherwise, these two calls could use different values for the food parameter.
The rules that cover default parameters in interface methods are very similar to the rules for base class methods. That carries all the way through: If you create an overloaded method with the same name as a method in an interface, the overloaded method will be preferred to the interface method, but only if you’ve implemented the interface method explicitly.
Releasing new Versions
Now that we’ve covered the simple case, let’s examine what happens when new versions of components are delivered. Remember that one of C#’s original goals was to be a ‘component oriented language’, meaning that well-written C# assemblies should be safely upgradeable on an individual basis. These new features in the C# language have increased the potential for breaking changes when you deliver upgraded components. With each of these changes, code behavior may change at compile time or runtime. Compile time breaks will show up only when developers using the component build an updated version. Runtime breaks will show up when users have the new component installed and run the application. Some changes can cause both.
Let’s start with the obvious: changing the name of any parameter on a public, or protected method is a compile time breaking change. What many C# developers don’t know is that this has been a breaking change for some time. Even though C# only added support for named and optional parameters in C# 4, other languages on the .NET Framework, have had this features for some time (most obviously, Visual Basic). Anyone using your component from one those languages would be affected by your changes earlier.
Next, of course, changing the values of default parameters creates a breaking change. This change could cause breaks at either compile time, or runtime, depending on how you code the change. The default value of an optional parameter is inserted by the compiler at the callsite. At runtime, any methods that were compiled with the previous version of the component will continue to have the previous value inserted at the callsite. However, when you recompile any caller, the default value will change. It’s really impossible to avoid this as a breaking change, you must pick whether the break is discovered at compile time, or runtime. Method calls compiled before the update will continue to use the previous default value; method calls recompiled with the new version of the component will use the new default value. How your method interprets the old and new values of the optional parameter determines which version has changed behavior.
All those issues I brought up earlier regarding method overloads, methods declared in base classes, and methods declared in interface methods all apply here as well. Modifying the default values of any parameter or the number of default parameters will affect the better method choice made by the compiler. That won’t have any runtime breaking changes, but could potentially introduce any number of breaking compile time changes. Of course, calling a different method could change the runtime behavior as well.
需要注意的地方
並不是說你不應該使用可選參數. 你必須記住和避免這些陷阱. 如果你發現你在方法中弄了很多的可選參數, 也許說明你考慮一下你的方法設計. 我建議謹慎的使用可選參數.最重要的,不要在虛方法和接口中使用可選參數.
摘自 JustRun1983