可选参数

来源:互联网 发布:淘宝苹果手机报价 编辑:程序博客网 时间:2024/06/11 15:41

Justification for Names and Optional Parameters

可选参数

By Bill Wagner

March 2012

 

不少开发人员问我为什么C#早期版本中不支持可选参数。可选参数在其它语言中展示了其很有益的特性。特别是当你写了一个有大量参数的方法,而该方法中的一部分参数有其合理的默认值的情况下.基于Office APICOM组件就是一个明显的例子。

C#直到4.0才添加该特性缘于其它方面需求更加迫切。此外,可选参数的益处也会导致一些问题。尤其是在你引入可选参数与方法过载间的交互后而愈加明显。你或许听说过来自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)

    )

// sample call:

ApplyFilters(new NameFilter());

没有出现含糊不清,决定调用方法的规则是“准确与调用参数匹配的方法要优先于使用可选参数的方法”。因此,前一个方法将被选中来执行方法调用。迄今为止,都还算简单。多数开发人员都还分得清调用哪个。但我们只看到了表面。接下来把这两个方法放到同一个类中,看看下面的调用结果:

// 假设NameFilter, CityFilter, 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.描述如下:出于过载方法选取的目的,任何调用中未指定值的可选参数都将从方法签名中移除。为了选择更适合的方法来调用,编译器一定会将两个过载方法看成如下的形式:

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());

这样就可以调用我们期望的方法了。可以这样做是因为在C#语言规格说明书中7.5.3.2中的另一点说明:参数会被重新排列,所以它们就会出现在参数列表中的相同位置。

也就是第二个调用一定会被转换成这样的形式:

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()给狗喂狗粮。c.Feed()给猫喂猫粮。thing.Feed()则给第二只狗喂了普通食物。我们来检察一下为何会这样.

对代码一个快速的检察结论是因为变量’thing’的静态类型(或称编译时类型)。’thing’的类型是Animal,因此方法Feed的可选参数默认值将从类Animal的方法声明中获得。的确如此,即使类Animal及方法Feed都是抽象的。

接下来,让我们来把情况搞得更复杂点。在类dog中添加一个方法Feed的新过载版本:

public void Feed(string dogfood = "dog chow", bool moist = false)

{

    Console.WriteLine("{0} {1}", moist ? "moist" : "dry", dogfood);

}

这下会怎样?或许你会惊奇的发现编译器这次选择了有额外可选参数的过载版本。这是由确定方法过载版本的规则决定的。来自较多继承层级类的候选项总是会优先于来自较少继承层级类的候选项来考虑。这也包括应用的任何可选参数。这样就使其与确定其它方法过载版本的规则相一致。另外一个虚Feed方法实际上是在类Animal中声明的而不是在类Dog中。也就是说只有第二个Feed方法是在扩展类中声明的,因此应选用第二个方法。相关原则参见C#语言规格说明书中7.6.5.1.

即使在我们考虑通过这些已经声明的方法来升级一个组件的分支前,仍有其它几个规则需要检察。现在让我们考虑一个有几个可选参数的虚方法,在类Cat上做如下修改:

public class Cat : Animal

{

    public override void Feed(string catfood) // parameter is not optional

    {

        Console.WriteLine(catfood);

    }

}

// usage:

var c = new Cat();

c.Feed();

上面的c.Feed()无法编译!上面调用的方法未包括任何参数的默认值。所以编译器不会把这些参数添加到正式参数列表中。然而,如果把变量类型转换成类Animal,则调用会是这样:

var c = new Cat();

((Animal)c).Feed();

当然,类Cat中的Feed方法版本会被调用,因为它是一个虚方法。

重写每个包含可选参数的虚方法时都应该重新声明这些可选参数,这样可以避免给调用者带来困惑。重新声明时,一定要确保同一个参数在每个重载中都使用同一个默认值。否则,上面两个调用中将使用不同的“食物”值。

接口方法中应用默认参数的规则与上面基类方法中应用的规则非常相似:如果创建的一个过载方法与某个接口中的某个方法同名,而你已经显式地实现了接口方法,那该过载方法更容易取代接口方法被调用。

发布新版本

既然我们已经介绍了简单情况,让我们来检查一下当有组件新版本交付时,会发生什么。记得c#的最初目标之一就是成为一个“面向组件语言”,也就是说写的不错的程序集应该可以在其中一个单独部分安全的更新。这些语言中的新特性已经增长了应对组件更新带来变化的潜能。由于这些变化,编译时或运行时代码行为可能有所变化。编译时中断只会在开发者使用组件构建一个最新版本时出现。而运行时中断则会出现在用户已经安装了新组件并运行应用程序时出现。有些变更可能会同时引起编译时与运行时中断。

我们来看个明显的例子:修改任意一个公共或保护方法上的参数名称,会引起一个编译时中断。不少C#开发人员不知道这样会引起多次中断。即使只在c#4.0中增加了对可选命名参数的支持,.net支持的其它语言也同时具有了该特性(最显示地是Visual Basic)。任何通过这些语言使用你的组件的,都将受到前面变化的影响。

当然,修改参数默认值创建一个中断变化。这个变化或是引起编译时中断或是引起运行时中断,这取决于你是如何修改代码的。可选参数的默认值由编译器插入到调用站中。运行时任何使用了前期组件版本编译的方法将继续使用前期插入到调用站中的老值。然后,如果你重新编译了调用者,那默认值将改变。这种情况真的不太可能作为一个中断变化来避免,你必须做出选择:让中断在运行时被发现还是编译时。更新前编译的方法调用将继续使用老的默认值;使用新的组件版本重新编译后的方法调用将使用新的默认值。你的方法如何解释可选参数的新值与旧值取决于哪个版本改变了行为。

前面我提到的所有这些问题,关于方法过载,基类中声明的可选参数方法,接口中声明可选参数方法所有这些均在此处适用。编辑任何参数默认值或是默认参数数量都会影响编译器对方法的选择。这不会引起运行时中断,但可能引发数量不定的编译时中断。当然调用不同的方法也会改变运行时行为。

只是警告,但不绝对禁止

上面介绍的内容不是说你的代码中决不要定义可选参数,或是决不要使用它。毕竟这已经是语言的一部分。但需要在有意义的地方使用这些新特性。尤其是要把这些特性的缺陷或可能引发的问题牢记于心,避免在自己的代码中出现相关问题。如果在你的方法中出现了一些随意放置的可选参数,这可能是个信号,提醒你应该在API的设计上再下些功夫。对可选参数的使用我持保守态度。使用时自问默认值是否会多次变更?或是方法只是可能需要多个过载而已?最重要的是避免在虚方法上或是接口方法中定义可选参数。


 

原创粉丝点击