AutoTree对象属于特殊引用对象,在使用上它既不同于指针对象,也不同于值对象。它采取半自动内存管理政策,在对其进行赋值时请尤其注意它的引用特性。
注意:本文用到了AutoTree构造语法,这不是标准C的语法,而是为了更简单的实现特定功能创造的语法。
autotree.h采用特殊的内存区域来存储对象,这块内存区域您无法通过C中常见的指针操作来访问。我在设计AutoTree之初,就期望它可以像JavaScript一样自动管理内存,同时保持优良的对C互操作性。在您使用AutoTree时,大多数时候您可以把AutoTree对象看作是JavaScript中的Json对象来使用。
AutoTree实际上是对结构体__builtin_generic_AutoTreeNode的别称,其指代树中的一个节点,下文中使用AutoTree也代表节点。AutoTree可以是一个KV对集合,也可以是一个数组,也可以是叶子节点,如一个字符串或一个数字。例如,您有一棵树,只需要持有树的根节点即可完成对其所有节点的访问。所以,对AutoTree的@构造返回的是树的根节点。为了让树的所有节点可以在根节点被销毁时销毁,所以所有的AutoTree对象都包含一个引用计数来计算生命周期。您可以使用int tree_refcnt(AutoTree tree)
来获取节点的引用计数。当引用计数为0时该节点被销毁,使用@构造产生的树中,包含头节点在内的所有节点引用计数初始均为1。当该树不再使用时,您需要手动调用void tree_release(AutoTree tree);
来删除这棵树。实际上这个函数会将这个节点的引用计数减去一,当节点的引用计数为0时会删除自身,删除自身的同时会给其子节点引用计数减一,利用这个特性,只需要对头节点进行release操作,就可以像多米诺骨牌一样全部删除。
类似于Json,如果您需要构造一棵树,只需要进行:
AutoTree tree = @{
a: 1,
b: 2,
c: "hello"
};
这样,就诞生了一个有四个节点的树,tree是树的头节点,三个叶子节点分别表示三个值。请不要忘记@符号,这是对额外C语言拓展功能的标志。同时,@构造还可用于数组,数字,字符串以及C表达式。
xxxxxxxxxx
AutoTree tree = @[1, 2, "Hello", "World"];
// 创造一棵树,有1,2,Hello,World四个叶子节点和头节点tree
AutoTree node = @1;
// 创造一棵只有一个叶子节点的树,广义上单个节点也是树
AutoTree node = @"Hello";
// 创造一棵只有一个叶子节点"Hello"的树
AutoTree node = @c_func();
// 创造一棵只有一个叶子节点,值为C语言语句返回值的树。
当然,树是递归定义的,以上特性都可以递归整合在一起,整合的结果可以是如下:
xxxxxxxxxx
int v = 2;
AutoTree tree = @{
a: v+2*2,
b: 1,
c: ["main", {
f: 1
}],
d: {
e: 2
}
};
// 创造一棵复杂的树
在语义完备的部分情况下(字符串常量的后边),逗号可以省略。(在C Code Develop 3.2以后的版本中,由于为ccdui.h增加了多种AutoTree内置语义,所以不建议在任何地方省略逗号以免发生歧义)
xxxxxxxxxx
AutoTree json_tree(char* json) // 由Json字符串创建一棵树
int tree_json(AutoTree tree, char* jsonStr, int jsonStrMaxLength) // 将树对象转换为Json
xxxxxxxxxx
void tree_release(AutoTree) // 释放一次树对象,若引用计数为0则调用释放下一级树对象,可能递归的释放整棵树
void tree_retain(AutoTree) // 提升一次树对象,例如需要将AutoTree对象作为参数交给其它函数,且其它函数会释放时
void tree_refcnt(AutoTree) // 获取树对象的引用计数
AutoTree tree_clone(AutoTree) // 深拷贝一个树对象,注意它返回的Tree有1个引用计数,若直接赋值给另一个树的子树会又多一个引用计数,导致内存泄漏。正确做法是赋值后对其调用一次 tree_release。
xxxxxxxxxx
AutoTree tree_path(AutoTree tree, char *path) // 用path路径字符串取树的某个节点
// Example
int v = 2;
AutoTree tree = @{
a: v+2*2,
b: 1,
c: ["main", {
f: 1
}],
d: {
e: 2
}
};
AutoTree dNode = tree_path("d"); // 取得d子树
AutoTree fNode = tree_path("c[1].f"); // 取得f子树
AutoTree mainNode = tree_path("c[0]"); // 取得有main字符串的叶子元素
上面介绍的path表达式可以直接在C语言中使用,就像对结构体操作那样,例如:
xxxxxxxxxx
AutoTree fNode = tree.c[1].f; // 取得d子树
// 当然,复杂的表达式也可以潜入在path表达式中,例如
AutoTree fNode = tree.c[calculateIndex(i) + 1].f; // 就像数组那样,下标可以是一个表达式
每个AutoTree对象均有一个独特的地址,指向特殊的内存区域。如果有必要,您可以通过__at_handle字段来访问。
xxxxxxxxxx
int handle = tree.__at_handle;
有时候,我们可能希望两个树中的某些子树共享一份内存,所以可能存在两棵不同的树的某两个子树的__at_handle字段一样,例如A.a和B.a相同,当我们修改A.a时,B.a就同时被修改,例如JavaScript对象赋值。也有时候,我们不希望共享内存,让两个对象独立发展,所以赋值是一个必须十分小心的事。
浅拷贝(引用复制)
浅拷贝是默认的拷贝模式,当使用等号对一个AutoTree右值赋值时的行为是浅拷贝。
xxxxxxxxxx
tree.a = tree.b;
// 此时a和b子树共用一个内存空间,当改变b时a也会改变,例如
tree.b.a = 0;
// tree.a.a = 0
当使用浅拷贝时,AutoTree会自动的给浅拷贝增加引用计数。
深拷贝(值复制)
AutoTree暂时没有深拷贝语法,但是您可以通过tree_clone来实现深拷贝。
xxxxxxxxxx
AutoTree tree = tree_clone(originalTree);
如果您想把tree.b深拷贝到tree.a,请注意引用计数的问题。
xxxxxxxxxx
tree.a = tree_clone(tree.b); //内存泄漏!
// 当tree_clone创建一棵新树后,返回的临时对象具有1个引用计数,如果此时将临时对象赋值给tree.a,意味着将新树的引用赋值给tree.a,则对这个新树又增加了一个引用计数,此时若tree释放则tree.a还剩1个引用计数,并不会被释放。
// 正确的操作可以是
AutoTree cloned = tree_clone(tree.b);
tree.a = cloned;
tree_release(cloned);
// 当然,也可以是
tree.b = tree_clone(tree.a);
tree_release(tree.b);
赋值的具体情况种类较为复杂,但AutoTree在赋值的宗旨是尽可能的模拟JavaScript的行为,记住这点就可以了。
以下内容总结了AutoTree对象相关的所有赋值意义,可供查阅。为了支持引用复制,当您使用AutoTree c = tree.b; 时,c变量中会记录自己来自于tree变量的b字段,称c为字典来源。若使用AutoTree c = tree[0],c变量会记录自己来自于tree变量的第0个字段,称c为数组来源。字典来源和数组来源都属于有来源对象,而AutoTree c = @{}; 创建的新树c属于顶级对象,属于无来源对象。这些两组四种不同的特性标志了在赋值的时候具有不同含义,需要注意。
左侧类型 | 右侧类型 | 含义 |
---|---|---|
AutoTree 有来源空 | AutoTree 任意 | 右侧引用复制到左侧 |
AutoTree 无来源空 | AutoTree 任意 | 右侧地址复制到左侧 |
AutoTree 无来源非空 | AutoTree 任意 | 右侧第一层值复制到左侧 |
AutoTree 字典来源非空 | AutoTree 任意 | 右侧引用复制到左侧,支持新建字段 |
AutoTree 数组来源非空 | AutoTree 任意 | 右侧引用复制到左侧,不支持新建字段 |
AutoTree 字典来源空 | int | char * | 赋值(创建新字段) |
AutoTree 非空 | int | char * | 赋值 |
int | char * | AutoTree 任意 | 拷回C(对字符串创建新的C字符串) |