Signature,顾名思义,是用来进行身份识别的一种手段。在编程语言的设计中,必须定义出识别两个不同实体的规则,这种规则也被称作签名规则。
比如,C通过函数名来区别两个函数,所以,不允许在同一个编译/链接单元内出现两个同名的函数。到了C++/Java,由于它们支持函数重载,一个函数名可以被赋予不同语义,提供不同的实现,所以它们对函数(方法)的签名规则扩展为:
o 函数名
o 参数数量
o 按照顺序,每一个参数的类型(包括类型修饰符)。
(请注意,在C++中,void foo()和void foo() const同样具备不同的签名,那是因为,C++把this也作为参数评估的一部分,假如上面的函数被定义在class Foo内,那么上面两个函数最终会被看作:void foo(Foo*)和void foo(const Foo*),所以仍然符合上面的签名规则.)
函数的签名规则之所以被这样定义,是因为这是这些函数被调用的方式所决定的,因为编译器必须能够根据一个函数的调用表达式识别出究竟是哪个函数被调用。例如:
class Foo
{
public static void foo() { ... }
public static void foo(int a) { ... }
};
Foo.foo(); //调用的是Foo::foo()
Foo.foo(10); // 调用的是Foo::foo(int)
由于一个函数在调用时,程序员并无必要保留其返回值,因此,编译器无从判断一个函数调用表达式的返回值类型。这也正是函数的返回值类型没有作为函数签名规则中的原因。
这样的函数签名规则极大的方便了程序员,它们可以更加方便的使用名字,更加直接的表现自己的意图。但,我们还可以做的更好。
我想你一定碰到过这样的问题,对于两个函数,其函数名、参数数量和参数类型列表都是一致的,但你事实上想表现不同的意图,也就是说它们的某些参数尽管其数据类型是一致的,但这些参数的含义却是不同的。对于这种问题,我们的解决手段往往是重新命名其中一个函数。且不说程序员在使用名字时不得不进行的妥协,因为这对于普通函数而言毕竟是一种选择,但对于我们无法重新命名的函数,比如构造函数,我们就无能为力了。比如:
class Multiplicity
{
private int minOccurs;
private int maxOccurs;
private boolean maxLimited;
...
}
在这个例子中,对于一个Multiplicity对象,client对其有两种构造方式:
o 指定最小值;其它成员值在这种情况下有一套自己的默认设置;
o 指定最大值;其它成员值在这种情况下有另外一套自己的默认设置;
我们想让client不需要知道这两种情况下的默认设置的规则。这样就可以做到信息隐藏。毕竟,那些设置是Multiplicity自身的规范和实现细节。client无需知道这些。
根据这种需求,最自然的方式是我们提供如下两个构造函数:
class Multiplicity
{
...
Multiplicity(int min)
{
minOccurs = min;
maxLimited = false;
}
Multiplicity(int max)
{
minOccurs = 0;
maxLimited = true;
maxOccurs = max;
}
}
但很不幸,这种规则在Java的签名规则下会出现名字冲突。但对于构造函数我们又没有办法重新命名,所以我们只好用其他手段来做。下面是一种解决方案:
class Multiplicity
{
...
Multiplicity(int min, int max, boolean limited)
{
minOccurs = min;
maxOccurs = max;
maxLimited = limited;
}
}
这样,client就可以使用一个构造函数对各种情况进行设置。但它最致命的缺点就是破坏了信息隐藏。client如果想正确的构造一个Multiplicity对象,它必须知道这些值被设置的规范。
对于这个简单例子,我们可以给出一个相对较好的解决方案:
class Multiplicity
{
...
Multiplicity(int value, boolean isMin)
{
if(isMin){
minOccurs = value;
maxLimited = false;
}
else {
minOccurs = 0;
maxLimited = true;
maxOccurs = value;
}
}
}
尽管对于这个简单例子,我们可以比较简单的解决问题。但这种解决办法绝对不是最直观的方式,而是不得已而为之。
所以,如果我们能够让程序员使用那种最直接的方式,我们就必须要解决调用时识别的问题。也就是说,面对着一个调用表达式,编译器如何能够识别到底是哪个函数被调用了。
解决方案就是:在调用时,给出参数的名字,例如:
Multiplicity m1 = new Multiplicity(min:10);
Multiplicity m2 = new Multiplicity(max:5);
当使用这种解决方案的时候,就等于说,参数名字也被放入函数的签名规则中。
将参数名放到调用列表中,并不是我的发明,一些语言早就采用了它。因为这样虽然麻烦了代码编写者,却有利于代码的阅读者和维护者(现有的编程语言都是为代码编写者设计的,为代码的维护者考虑甚少。这或许就是代码维护更加困难困难的原因之一吧)。我们在阅读别人代码的时候经常会看到一个类似的函数调用表达式:
who = query(20,"James",true);
当面对着这样的情况时,我们不得不去查看函数的原型,才可能知道每一个上面的20, "James", true到底代表什么样的意思。但如果上面的表达式变为:
who = query(age:20, name:"James", male:true);
代码就会直观的多。(当然这也许要代码编写者的配合,比如,在命名参数的时候,要尽量赋予其直观的含义)。
调用时指明参数的方式,还会带来一些其它好处。比如C++支持参数的默认值,但由于C++规定,参数必须按照声明顺序依次传递,所以我们不得不把一些带有默认值的参数都放在最后。即使这样,也仍然面临着一些问题。比如:
class Foo
{
public:
Foo(int a, bool b = true, bool c = false);
};
如果我们在构造一个Foo对象时,只想设置a和c,而让b使用其默认值。我们还是不得不去设置b,就像这样:
Foo* foo = new Foo(10, true, true);
但如果我们使用带有参数的形式,就可以更加便利,如下:
Foo* foo = new Foo(a:10, c:true);
我相信,你会同意这样的方案有利于代码理解和维护,也会带来一些其它的便利。但做为程序员,我也相信你一定会觉得这样对于代码编写者会带来更多的工作量。作为折衷,我们可以规定,只有在引起名字冲突的情况下,你才必须指定参数名;在其他情况下,你不必这么做。另外,即使这种规则是强制的,但随着现在的IDE环境越来越强大,这样的问题最终应该不会是一个问题。
你可以使用这个链接引用该篇文章 http://publishblog.blogchina.com/blog/tb.b?diaryID=650134