类 class 2.014

D语言面向对象特性详解
本文深入探讨D语言的面向对象编程特性,包括类、构造函数、析构函数、静态构造函数、静态析构函数、类不变量、单元测试、类分配器、类释放器、域类、最终类、嵌套类、匿名嵌套类等内容。
D 的面向对象的特性都来源于类。类层次里的顶层是 Object 类。Object 定义了每个派生类拥有的最小功能集合,并为这些功能提供了默认的实现。

类是程序员定义的类型。作为面向对象语言,D支持对象,支持封装、继承和多态。D 类支
持单继承编程范式,并且支持接口。类对象只能通过引用具现化。

类可以被导出,这意味着它的名字和非私有成员将会暴露给外部的 DLL 或者 EXE 。

类声明的定义:

类声明:
class 标识符 基类列表可选的 类过程体

基类列表:
: 父类
: 父类 多个接口类
: 单个接口类

父类:
标识符
保护属性 标识符

多个接口类:
单个接口类
单个接口类 多个接口类

单个接口类:
标识符
保护属性 标识符

保护属性:
private
package
public
export

类过程体:
{ }
{ 多个类过程体声明 }

多个类过程体声明:
单个类过程体声明
单个类过程体声明 多个类过程体声明

单个类过程体声明:
单个声明

构造函数
析构函数

静态构造函数
静态析构函数

不变量
单元测试

类分配器
类释放器


类的组成:
父类
接口

动态域
静态域

类型
函数

静态函数
动态函数

构造函数
析构函数

静态构造函数
静态析构函数

不变量
单元测试

分配器
释放器


一个类被定义成:
class Foo
{
... 成员 ...
}

注意在标志类定义结束的‘ }’之后没有‘;’。同样不能像下面这样声明变量:
class Foo { } var;

正确方法是:
class Foo { }
Foo var;


12.1 域

类成员总是通过‘.’运算符访问。并没有像 C++ 里的 ::或者 -> 操作符

D 编译器有权利重新排列类中各个域的顺序,这样就允许编译器按照实现定义的方式将它们
压缩以优化程序。考虑函数中的局部变量的域——编译器将其中一些分配到寄存器中,其他
的按照最理想的分布被保存到堆栈帧中。这就给了代码设计者重排代码以加强可读性的自
由,而不必强迫代码设计者依据机器的优化规则排列相关的域。结构/联合提供了显式控制
域分布的能力,但这不是类的分内之事。


12.2 域特性

特性 .offsetof 给出的是起始自类实例的域的字节偏移量。.offsetof 仅能用于带在完整类类型的域,而不能用于那些产生域类型自己的表达式:

class Foo
{
int x;
}
...
void test(Foo foo)
{
size_t o;
o = Foo.x.offsetof; // 计算得 8
o = foo.x.offsetof; // 出错,.offsetof 是一个 int 类型
}


12.3 类特性

特性 .tupleof 会返回一个类里所有域的 ±í´ïʽ元组,不过隐藏域和基类里的域除外。

class Foo { int x; long y; }
void test(Foo foo)
{
foo.tupleof[0] = 1; // 把 foo.x 设置为 1
foo.tupleof[1] = 2; // 把 foo.y 设置为 2
foreach (x; foo.tupleof)
writef(x); // 输出 12
}


12.4 父类

所有的类都从父类继承。如果没有指定,它就从 Object 继承。Object 是 D 类层次体系的最顶层。


12.5 构造函数

构造函数:
this 参数 函数体

所有的成员都被初始化为它对应类型的默认初始值,通常整数被初始化为 0 ,浮点数被初
始化为 NAN。这就防止了因为在某个构造函数中忽略了初始化某个成员而造成那已发现的
错误。在类定义中,可以使用静态的初始值代替默认值:

class Abc
{
int a; // a 的默认初始值为 0
long b = 7; // b 的默认初始值为 7
float f; // f 的默认初始值为 NAN
}

静态初始化在调用构造函数之前完成。
构造函数是名为 this 的函数,它没有返回值:

class Foo
{
this(int x) // 声明 Foo 的构造函数
{ ...
}
this()
{ ...
}
}

基类的构造通过用 super 调用基类的构造函数来完成:

class A { this(int y) { } }

class B : A
{
int j;
this()
{
...
super(3); // 调用基类构造函数 A.this(3)
...
}
}
构造函数也能调用同一个类中的其他构造函数以共享通用的初始化代码:
class C
{
int j;
this()
{
...
}
this(int i)
{
this();
j = i;
}
}
如果构造函数中没有通过 this 或 super 调用构造函数,并且基类有构造函数,编译器将在
构造函数的开始处自动插入一个 super()。
如果类没有构造函数,但是基类有构造函数,那么默认的构造函数的形式为:

this() { }

这会由编译器隐式生成。

类对象的构造十分灵活,但是有一些限制:

1. 构造函数互相调用是非法的:
this() { this(1); }
this(int i) { this(); } // 非法,构造函数循环调用

2. 如果一个构造函数中调用了构造函数,那么这个构造函数的任何执行路径中都只能调
用一次构造函数:
this() { a || super(); } // 非法
this() { (a) ? this(1) : super(); } // 正确
this()
{
for (...)
{
super(); // 非法,位于循环内
}
}

3. 在构造函数出现之前显式或者隐式引用 this 都是非法的。

4. 不能在标号后调用构造函数(这样做的目的是使检查 goto 的前导条件容易完成)。
类对象的实例使用 New表达式 创建:
A a = new A(3);


在这个过程中按照下面的步骤执行:
1. 为对象分配存储空间。如果失败,不会返回 null,会抛出一个
OutOfMemoryException 异常。因此,不再需要编写冗长而乏味的 null 引用防卫代
码。

2. 使用类定义中的值静态初始化“原始数据”。指向 vtbl[](指向虚函数的指针数组)
的指针被赋值。这保证了调用构造函数的类是已经完全成型的。这个操作等价于使用
memcpy() 将对象的静态版本拷贝到新分配的对象的空间,但是更高级的编译器将会
对这种方法进行优化。

3. 如果为类定义了构造函数,匹配调用参数列表的构造函数被调用。

4. 如果打开了类不变量的检查,在构造函数调用后调用类不变量。


12.6 析构函数

析构函数:
~this() 函数体

当对象被垃圾回收器删除时将调用析构函数。语法如下:

class Foo
{
~this() // Foo 的析构函数
{
}
}

每个类只能有一个析构函数,析构函数没有参数,没有特征。它总是虚函数。

此构函数的作用是释放对象持有的任何资源。

程序可以显式的通知垃圾回收程序不在引用一个对象(使用 delete 表达式),然后垃圾回收器会立即调用析构函数,并将对象占用的内存放回自由存储区。析构函数决不会被调用两
次。

当析构函数运行结束时会自动调用父类的析构函数。不能显式调用父类的析构函数。

当垃圾回收程序调用一个对象的析构函数时,并且这个对象含有对垃圾回收对象的引用,那
么这些引用都会变得无效。这意味着析构函数不能引用子对象。这是因为垃圾回收器并不是
以一种确定的顺序来回收对象的,因此,当垃圾回收器在运行某个对象的析构函数时,就不
能保证指向其它垃圾回收器的指针或引用总是存在。这条件规则并不适用于自动(auto)对象
或那些使用 Delete表达式 删除的对象,原因就是,它们的析构函数不是由垃圾回收器来运
行的,即是说所有引用都是有效的。

垃圾回收程序不能保证对所有的无引用对象调用析构函数。而且垃圾回收器也不能保证调用
的相对顺序。

从数据段中引用的对象不会被 gc 回收。


12.7 静态构造函数

静态构造函数:
static this() 函数体

静态构造函数在 main() 函数获得控制前执行初始化。静态构造函数用来初始化其值不能
再编译时求出的静态类成员。

其他语言中的静态构造函数被设计为可以使用成员进行初始化。这样做的问题是无法精确控
制代码执行的顺序,

例如:
class Foo
{
static int a = b + 1;
static int b = a * 2;
}

最终 a 和 b 都是什么值?初始化按照什么顺序执行?在初始化前 a 和 b 都是什么值?这是一个编译是错误吗?抑或它是一个运行是错误?还有一种不那么明显的令人迷惑的情况是单
独一个初始化是静态的还是动态的。

D 让这一切变得简单。所有的成员初始化都必须在编译时可确定,因此就没有求值顺序依赖
问题,也不可能读取一个未被初始化的值。动态初始化由静态构造函数执行,采用语法
static this() 实现。

class Foo
{
static int a; // 默认初始化为 0
static int b = 1;
static int c = b + a; // 错误,不是常量初始化
static this() // 静态构造函数
{
a = b + 1; // a 被设置为 2
b = a * 2; // b 被设置为 4
}
}

static this() 会由启动代码在调用 main() 之前调用。如果它正常返回(没有抛出异
常),静态析构函数就会被加到会在程序终止时被调用的函数列表中。静态构造函数的参数
列表为空。
模块里的静态构造函数会以它们出现在词法里的顺序执行。直接或间接导入的模块的所有静
态构造函数则在为些导入者的静态构造函数之前执行。
静态构造函数里的 static 并不是属性,因此它必须紧挨在 this 前面:

class Foo
{
static this() { ...} // 一个静态构造函数
static private this() { ...} // 不是静态构造函数
static
{
this() { ...} // 不是静态构造函数
}
static:
this() { ...} // 不是静态构造函数
}


12.8 静态析构函数

静态析构函数:
static ~this() 函数体
静态析构函数定义为具有语法形式 static ~this() 的特殊静态函数。

class Foo
{
static ~this() // 静态析构函数
{
}
}

静态析构函数在程序终止的时候被调用,但这只发生在静态构造函数成功执行完成时。静态
析构函数的参数列表为空。静态析构函数按照静态构造函数调用的逆顺序调用。
静态析构函数里的 static 并不是属性,因此它必须紧挨在 ~this 前面:

class Foo
{
static ~this() { ...} // 静态析构函数
static private ~this() { ...} // 不是静态析构函数
static
{
~this() { ...} // 不是静态析构函数
}
static:
~this() { ...} // 不是静态析构函数
}


12.9 类不变量(Class Invariants)

不变量:
invariant() 块语句
类不变量(class invariants)用来指定类的必须总是为真的特征(除了在执行成员函数时)。例如,代表日期的类可能有一个不变量:day 必须位于 1..31 之间,hour 必须位于 0..23 之间:

class Date
{
int day;
int hour;
invariant()
{
assert(1 <= day && day <= 31);
assert(0 <= hour && hour < 24);
}
}

类不变量就是一种契约,是必须为真的断言。当类的构造函数执行完成时、在类的析构函数
开始执行时、在 public 或 exported 成员函数执行之前,或者在 public 或 exported 成员函数完成时,将检查不变量。

不变量中的代码不应该调用任何非静态公有类成员函数,无论直接还是间接。如果那样做的
话就会造成堆栈溢出,因为不变量将被无限递归调用。

由于不变量是在公共(public)成员或导出(exported)成员的开始部分被调用的,因此,这些成员不应该从构造函数里进行调用。

class Foo
{
public void f() { }
private void g() { }
invariant()
{
f(); // 错误,不能在 invariant(不变量)中调用公有成员函数
g(); // 正确,g() 不是 public(公有的)
}
}

可以使用 assert() 检查类对象的不变量,例如:
Date mydate;
...


assert(mydate); // 检查类 Date 的 invariant(不变量) 的内容
如果不变量检查失败,将抛出一个 AssertError 异常。类不变量会被继承,也就是,任
何类的不变量都隐式地包含其基类的不变量。

每个类只能有一个 不变量。

当编译生成发布版时,不会生成不变量检查代码,这样程序就会以最高速度运行。


12.10 单元测试(Unit Tests)

单元测试:
unittest 函数体
单元测试(Unit Tests)是用来测试一个类是否正常工作的一系列测试用例。理想情况下,单元测试应该在每次编译时运行一遍。保证单元测试得到运行,并且随同类代码一起维护的最好
的方法就是:将测试代码同类的实现代码放置到一起。]

类可以有一个特殊的成员函数,叫做:
unittest
{
...测试代码...
}

编译器开关,如 -unittest(对于 dmd),会使得单元测试代码被编译并被合并到最后的可
执行文件中去。在静态初始化运行完成之后并且在调用 main() 函数之前,单元测试代码
会被运行。

例如,假定一个类 Sum 用来计算两个值得和:
class Sum
{
int add(int x, int y) { return x + y; }

unittest
{
Sum sum = new Sum;
assert(sum.add(3,4) == 7);
assert(sum.add(-2,0) == -2);
}
}


12.11 类分配器(Class Allocators)

类分配器:
new 参数 函数体

具有下面形式的类成员函数:
new(uint size)
{
...
}

叫做类分配器。如果第一个参数的类型是 uint ,类分配器可以有任意数量的参数。可以为
类定义多个类分配器,通过通用的函数重载解析规则选择合适的函数。
当执行一个 new 表达式:

new Foo;

并且当 Foo 是拥有分配器的类时,分配器将被调用,第一个参数被设置为分配一个实例所
需的以字节为单位的内存大小。分配器必须分配内存并返回一个 void* 指针。如果分配失
败,它不必返回一个 null,但是必须抛出一个异常。如果分配器有多于一个的参数,余下
的参数将在 new(在 New表达式里)之后的括号中出现。

class Foo
{
this(char[] a) { ... }
new(uint size, int x, int y)
{
...
}
}
...

new(1,2) Foo(a); // 调用 new(Foo.sizeof,1,2)

如果没有指定类分配器,派生类将继承基类的类分配器。

如果实例是创建在堆栈上,则类分配器不会被调用。

另见 显式类实例分配。


12.12 类释放器(Class Deallocators)

类释放器:
delete 形式参数 函数体
具有下面形式的类成员函数:

delete(void *p)
{
...
}

叫做类释放器。类释放器有且仅有一个类型为 void* 的参数。一个类只能有一个类释放
器。当执行一个 delete 表达式:

delete f;
且 f 是拥有释放器的一个类的实例时,如果类有析构函数,会调用析构函数,然后释放器被
调用,

一个指向类实例的指针被传递给释放器。释放内存是释放器的责任。

如果不特别指定,派生类会继承基类所有的释放器。
如果实例是创建在堆栈上,则类分配器不会被调用。

另见 显式类实例分配。


12.13 域类(Scope Classes)

一个 scope 类指的是带有 scope 属性的类,
像这样:

scope class Foo { ... }

scope 特征是继承的,这样任何从一个 scope 类导出得到的类也带有 scope。
一个 scope 类引用仅能做为一个函数局部变量。它必须声明成 scope:
scope class Foo { ... }
void func()
{
Foo f; // 错误,scope 类的引用必须是 scope
scope Foo g = new Foo(); // 正确
}
当一个 scope 类引用超出作用域时,它的析构函数(如有的话)会被自动调用。即使通过抛
出的异常退出了该域,这条原则也成立。


12.14 最终类(Final Classes)

最终类(Final classes)不能分出子类:
final class A { }
class B :A { } // 错误,类 A 是最终的(final)


12.15 嵌套类(Nested Classes)

嵌套类指的是声明在一个函数或另一个类作用域里的类。嵌套类可以访问嵌套它的类和函数
里的变量和其它符号:
class Outer
{
int m;
class Inner
{
int foo()
{
return m; // 正确访问 Outer 的成员
}
}
}

void func()
{ int m;
class Inner
{
int foo()
{
return m; // 正确访问 func() 的局部变量 m
}
}
}

如果嵌套类有 static 属性,那么它就不能访问封闭域里的变量——它们对于堆栈是局部的或者需要的是 this:

class Outer
{
int m;
static int n;
static class Inner
{
int foo()
{
return m; // 错误,Inner 是静态的,并且 m 需要 this
return n; // 正确,n 是静态的
}
}
}
void func()
{ int m;
static int n;
static class Inner
{
int foo()
{
return m; // 错误,Inner 是静态的,并且 m 对于堆栈是局部的
return n; // 正确,n 是静态的
}
}
}

非静态见嵌套类的通过包含一个额外的隐藏成员(叫做上下文相关指针)发挥作用,而隐藏
成员指的是:封闭函数的帧指针(如果它是嵌套在一个函数内部的话);封闭的类实例的
this 指针(如果它是嵌套在一个类内部的话)。

当非静态嵌套类被实例化时,上下文相关指针会在该类的构造函数被调用之前会分配,因此
构造函数可以完全访问那些封闭变量。非静态嵌套类只有在必需的上下文相关指针信息可用
时才会被实例化:

class Outer
{
class Inner { }
static class SInner { }
}
void func()
{
class Nested { }
Outer o = new Outer; // 正确
Outer.Inner oi = new Outer.Inner; // 错误,对于 Outer 没有'this'
Outer.SInner os = new Outer.SInner; // 正确
Nested n = new Nested; // 正确
}

由于非静态嵌套类可以访问它的封闭函数的栈变量,因此一旦封闭函数退出,则该访问也就
变得无效。

class Base
{
int foo() { return 1; }
}
Base func()
{ int m = 3;
class Nested : Base
{
int foo() { return m; }
}
Base b = new Nested;
assert(b.foo() == 3); // 正确,func() 还存在
return b;
}
int test()
{
Base b = func();

return b.foo(); // 错误,func().m 未定义
}

如果需要的这种功能话,则让其工作的方式就是为嵌套类的构造函数里的所需要的变量制做
一个复本:

class Base
{
int foo() { return 1; }
}
Base func()
{ int m = 3;
class Nested : Base
{ int m_;
this() { m_ = m; }
int foo() { return m_; }
}
Base b = new Nested;
assert(b.foo() == 3); // 正确,func() 还存在
return b;
}
int test()
{
Base b = func();
return b.foo(); // 正确,正在使用缓存的 func().m 复本
}
this 可以被用于内层类实例的创建,方式就是在它的 New表达式 前面添加前缀:
class Outer
{ int a;
class Inner
{
int foo()
{
return a;
}
}
}
int bar()
{
Outer o = new Outer;
o.a = 3;
Outer.Inner oi = o.new Inner;
return oi.foo(); // 返回 3
}
这里 o 将 this 提供给 Outer 的外层类实例。

嵌套类里使用的 .outer 特性会给出指向它的封闭类的 this 指针。如果封闭的上下文不是一个类,则 .outer 给出出指向它的指针,而类型为 void*。

class Outer
{
class Inner
{
Outer foo()
{
return this.outer;
}
}
void bar()
{
Inner i = new Inner;
assert(this == i.foo());
}
}
void test()
{
Outer o = new Outer;
o.bar();
}


12.15.1 匿名嵌套类(Anonymous Nested Classes)

匿名嵌套类要使用 New匿名类表达式 定义和实例化:

New 匿名类表达式:
new (参数列表)可选的
class (参数列表)可选的
父类
可选的
多个接口类
可选的
类过程体


这个等同于:
class 标识符 :父类 接口类
类过程体
new (参数列表) 标识符 (参数列表);
这里 标识符 指的是为匿名嵌套类生成的名字。

[color=red]2.014
Const and Invariant Classes

If a ClassDeclaration has a const or invariant storage class, then it is as if each member of the class was declared with that storage class. If a base class is const or invariant, then all classes derived from it are also const or invariant. [/color]


。。。。。。。。。。。。。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值