函数
虚函数
所有的非静态非私有函数都是虚函数。这听起来也许低效,但是因为D编译器在生成代码时知道所有的类层次结构,所有未被重载的函数可以被优化为非虚函数。事实上,因为 C++ 程序员倾向于“在不确定时,声明它为虚函数”,D 采用的方法“声明为虚函数除非我们能够证明它可以是非虚函数”造成的结果是产生更多更直接的函数调用。由重载非虚函数造成的 bug 也减少了。拥有非D链接的函数不会是虚函数,因此也不能被重载。
标记为 final 的函数不可以在派生类中重载,除非它们也是 private 。例如:
class A { int def() { ... } final int foo() { ... } final private int bar() { ... } private int abc() { ... } } class B : A { int def() { ... } // ok,重载 A.def int foo() { ... } // 错误,A.foo 是 final 的 int bar() { ... } // ok,A.bar 是 final private 的,但不是 virtual 的 int abc() { ... } // ok,A.abc 不是 virtual 的,B.abc 是 virtual 的 } void test(A a) //参见下面的调用实参 { a.def(); // 调用 B.def a.foo(); // 调用 A.foo a.bar(); // 调用 A.bar a.abc(); // 调用 A.abc } void func() { B b = new B(); test(b); }支持 协变返回类型 ,这意味着派生类的重载函数可以返回从被重载的函数的返回类型派生的类型:
class A { } class B : A { } class Foo { A test() { return null; } } class Bar : Foo { B test() { return null; } // 重写了 test(),并且同 Foo.test() 具有协变返回类型 }
函数继承和重载
派生类中的函数将重载基类中同函数名同参数类型的函数:class A { int foo(int x) { ... } } class B : A { override int foo(int x) { ... } } void test() { B b = new B(); bar(b); } void bar(A a) { a.foo(); // 调用 B.foo(int) }但是,在进行重载解析的时候,不会考虑基类中的函数:
class A { int foo(int x) { ... } int foo(long y) { ... } } class B : A { override int foo(long x) { ... } } void test() { B b = new B(); bar(b); } void bar(A a) { a.foo(1); // 调用 A.foo(int) B b = new B(); b.foo(1); // 调用 B.foo(long),因为不会考虑 A.foo(int) }考虑重载解析过程中的基类函数,使用 别名声明 :
class A { int foo(int x) { ... } int foo(long y) { ... } } class B : A { alias A.foo foo; override int foo(long x) { ... } } void test() { B b = new B(); bar(b); } void bar(A a) { a.foo(1); // 调用 A.foo(int) B b = new B(); b.foo(1); // 调用 A.foo(int) }函数参数的默认值不会被继承:
class A { void foo(int x = 5) { ... } } class B : A { void foo(int x = 7) { ... } } class C : B { void foo(int x) { ... } } void test() { A a = new A(); a.foo(); // 调用 A.foo(5) B b = new B(); b.foo(); // 调用 B.foo(7) C c = new C(); c.foo(); // 错误,C.foo 需要一个参数 }
内联函数
D中没有 inline 关键字。编译器决定是否将一个函数内联,就像 register 关键字不再同编译器是否将变量存在寄存器中相关一样。(也没有 register 关键字。)函数重载
在 C++ 中,函数重载有很多复杂的级别,一些被定义为“更好的”匹配。如果代码编写者利用函数重载选择时更为细微的行为,代码就会变得难以维护。不仅 C++ 专家很难弄明白为什么选择这个函数而不选择哪个,不同的 C++ 编译器也可能会采用不同的方式实现这个充满技巧的特征,这造成了微妙而灾难性的结果。在 D 中,函数重载很简单。允许精确匹配,允许包含隐式转换的匹配,除此之外就不匹配。如果有多于一个匹配,就是错误。
非 D 链接的函数不允许重载。
函数参数
参数是 in、 out 或 inout 的。 in 是默认值; out 和 inout 参数工作起来像存储类。例如:int foo(int x, out int y, inout int z, int q);x 为 in、y 为 out、z 为 inout ,而 q 为 in 。
out 已经很少见了,inout 更少见,因此如果使用这两种特性就需要使用关键字,而将 in 作为默认值。它们出现在 D 中是因为:
- 函数声明清楚的表示了哪些是函数的输入,哪些是函数的输出。
- 不再需要单独使用一种叫 IDL 语言。
- 可以给编译器提供更多的信息,从而可以提供更好的错误检查并生成更好的代码。
- (也许)不需要引用(C++中的‘&’)声明。
void foo(out int bar) { } int bar = 3; foo(bar); // bar 现在是 0
可变函数参数
函数可以是可变的,就是说它们可以有任意个参数。一个可变的函数用必需参数后面的‘...’表示:int foo(int x, int y, ...); foo(3, 4); // ok foo(3, 4, 6.8); // ok,一个可变参数 foo(2); // 错误,y 是必需的参数带有非 D 链接的代码必须带有至少一个非可变参数。
int abc(...); // ok, D 链接 extern (C) def(...); // 错误,必须至少有一个参数可变函数声明了一个特殊的局部变量, _argptr ,它是指向第一个可变参数的 void* 类型的指针。如果要访问这些参数, _argptr 必须被转换为指向所希望的参数类型的指针:
foo(3, 4, 5); // 第一个可变参数是 5 int foo(int x, int y, ...) { int z; z = *cast(int*)_argptr; // z 被设为 5 }对于采用 D 链接的可变函数,有一个隐含的名为 _arguments 的参数,它的类型为 TypeInfo[] 。 _arguments 给函数提供了参数的个数,也提供了每个参数的类型,这样就可以编写类型安全的可变函数。
class FOO { } void foo(int x, ...) { printf("%d arguments/n", _arguments.length); for (int i = 0; i < _arguments.length; i++) { _arguments[i].print(); if (_arguments[i] == typeid(int)) { int j = *cast(int *)_argptr; _argptr += int.sizeof; printf("/t%d/n", j); } else if (_arguments[i] == typeid(long)) { long j = *cast(long *)_argptr; _argptr += long.sizeof; printf("/t%lld/n", j); } else if (_arguments[i] == typeid(double)) { double d = *cast(double *)_argptr; _argptr += double.sizeof; printf("/t%g/n", d); } else if (_arguments[i] == typeid(FOO)) { FOO f = *cast(FOO*)_argptr; _argptr += FOO.sizeof; printf("/t%p/n", f); } else assert(0); } } void main() { FOO f = new FOO(); printf("%p/n", f); foo(1, 2, 3L, 4.5, f); }将打印出:
00870FD0 4 arguments int 2 long 3 double 4.5 FOO 00870FD0为了避开不同 CPU 结构上的各种奇特的堆栈分布带来的麻烦,使用 std.stdarg 访问可变参数:
import std.stdarg; void foo(int x, ...) { printf("%d arguments/n", _arguments.length); for (int i = 0; i < _arguments.length; i++) { _arguments[i].print(); if (_arguments[i] == typeid(int)) { int j = va_arg!(int)(_argptr); printf("/t%d/n", j); } else if (_arguments[i] == typeid(long)) { long j = va_arg!(long)(_argptr); printf("/t%lld/n", j); } else if (_arguments[i] == typeid(double)) { double d = va_arg!(double)(_argptr); printf("/t%g/n", d); } else if (_arguments[i] == typeid(FOO)) { FOO f = va_arg!(FOO)(_argptr); printf("/t%p/n", f); } else assert(0); } }
局部变量
如果使用一个未被赋值过的局部变量,会被视为错误。编译器的实现未必总能够检测到这些情况。其他语言的编译器有时会为此发出警告,但是因为这种情况几乎总是意味着存在 bug ,所以应该把它处理为错误。如果声明一个变量但却从未使用,会被视为错误。死变量,如同过时的死代码一样,都会使维护者迷惑。
如果声明的一个变量掩盖了统一函数中的另一个变量,会被视为错误:
void func(int x) { int x; // 错误,掩盖了前面定义的 x double y; ... { char y; // 错误,掩盖了前面定义的 y int z; } { wchar z; // 合法,前面的 z 已经脱离作用域了 } }因为这种情况看起来不合理,在实践中出现这种情况时不是一个 bug 至少看起来也像一个 bug 。
如果返回一个局部变量的地址或引用,会被视为错误。(译注:有多少 C++ 书曾告诫你不要这么做?如果没有,你可以扔掉那本书了。在 D 中,你不再需要担心了,你所损失的,只是一点极为低级的能力。)
如果局部变量同标签同名,会被视为错误。
嵌套函数
函数可以嵌套在其他函数中:int bar(int a) { int foo(int b) { int abc() { return 1; } return b + abc(); } return foo(a); } void test() { int i = bar(3); // i 被赋值为 4 }嵌套函数只能由它外围的函数及同它有相同嵌套深度的嵌套函数访问:
int bar(int a) { int foo(int b) { return b + 1; } int abc(int b) { return foo(b); } // ok return foo(a); } void test() { int i = bar(3); // ok int j = bar.foo(3); // 错误,bar.foo 不可见 }嵌套函数可以访问它外围的函数的变量和其他的符号。在这里,“访问”意味着既可以读,也可以写。
int bar(int a) { int c = 3; int foo(int b) { b += c; // 4 被加到 b 上 c++; // bar.c 现在是 5 return b + c; // 返回 12 } c = 4; int i = foo(a); // i 被摄为 12 return i + c; // 返回 17 } void test() { int i = bar(3); // i 被赋值为 17 }这种访问能力能够跨越多重嵌套:
int bar(int a) { int c = 3; int foo(int b) { int abc() { return c; // 访问 bar.c } return b + c + abc(); } return foo(3); }静态嵌套函数不能访问外围函数的任何堆栈变量,但能访问静态变量。这种行为同静态成员函数类似。
int bar(int a) { int c; static int d; static int foo(int b) { b = d; // ok b = c; // 错误,foo() 不能访问 bar() 的堆栈帧 return b + 1; } return foo(a); }函数可以嵌套在成员函数内:
struct Foo { int a; int bar() { int c; int foo() { return c + a; } } }嵌套结构和嵌套类的成员函数不能访问外围函数的堆栈变量,但是能访问其他的符号:
void test() { int j; static int s; struct Foo { int a; int bar() { int c = s; // ok,s 是静态的 int d = j; // 错误,不能访问 test() 的堆栈帧 int foo() { int e = s; // ok,s 是静态的 int f = j; // 错误,不能访问 test() 的堆栈帧 return c + a; // ok,bar() 的堆栈帧是可访问的 // 通过指向 Foo.bar() 的 this 指针 // 可以访问 Foo 的成员 } } } }嵌套函数总是使用 D 函数链接类型。
同模块级的声明不同,函数作用域的声明会按照声明的顺序处理。这意味着连个嵌套函数不能互相调用:
void test() { void foo() { bar(); } // 错误,bar 未定义 void bar() { foo(); } // ok }解决的方法是使用委托:
void test() { void delegate() fp; void foo() { fp(); } void bar() { foo(); } fp = &bar; }未来的方向:这个限制可能会被删除。
委托、函数指针和动态闭包
函数指针可以指向一个静态嵌套函数:int function() fp; void test() { static int a = 7; static int foo() { return a + 3; } fp = &foo; } void bar() { test(); int i = fp(); // i 被设为 10 }委托可以用非静态嵌套函数赋值:
int delegate() dg; void test() { int a = 7; int foo() { return a + 3; } dg = &foo; int i = dg(); // i 被设为 10 }但是,一旦声明堆栈变量的函数退出了,堆栈变量就不再有效;同样的,指向堆栈变量的指针也不再有效:
int* bar()非静态嵌套函数的委托包括两块数据:指向外围函数堆栈帧的指针(叫做 帧指针 )和函数的地址。与此类似,结构/类的非静态成员函数委托由 this 指针和成员函数的地址组成。这两种形式的委托可以互相转换,实际上它们具有相同的类型:
{ int b;
test();
int i = dg(); // 错误,test.a 不再存在
return &b; // 错误,bar.b 在 bar() 退出后不再有效
}
struct Foo { int a = 7; int bar() { return a; } } int foo(int delegate() dg) { return dg() + 1; } void test() { int x = 27; int abc() { return x; } Foo f; int i; i = foo(&abc); // i 被设为 28 i = foo(&f.bar); // i 被设为 8 }环境和函数的结合被称作 动态闭包 。
未来的方向:函数指针和委托可能会合并到一个统一的语法中并且可以互相代替。