本文节选自《LPC语言基础教程:从零学习游戏开发》,版权归@mudren,欢迎转载,但必须注明来源(mud.ren)。
在第二章数据类型中我们简单的介绍了LPC语言的结构体,这种数据类型可以像C++一样使用class关键字代替struct,而且也使用new关键字初始化变量,但是不能和C++的类一样定义成员函数,本质只是struct数据类型,而不像C++是名副其实的类(class),但类本来也是一自定义数据类型,所以本教程把class一样翻译为「类」。
因为 mixed 类型数组和映射的存在,让LPC语言中对数据处理上很少使用结构体,但不代表结构体没什么用,和C语言一样,结构体最大的作用是封装一系列有内在联系的不同数据类型的变量成为一个整体。
// 结构体class
class example {
string name;
int age;
string *lover;
}
#define Example class example
Example test(string name, int age, string *lover)
{
Example test = new (Example, name:name, age:age, lover:lover);
return test;
}
int main(object me, string arg)
{
// 变量
Example instance;
instance = test("mudren", 24, ({"a", "b", "c", "d", "e"}));
// 取值
printf("name : %s,age : %d, lover : %@-7s\n", instance->name, instance->age, instance->lover);
return 1;
}
而且在LPC语言中,存在大量结构体相关的efun。
名称
classp() - 检测指定变量是否是 class/struct 类型
语法
int classp( mixed arg );
描述
如果变量 `arg` 是 class/struct 类型返回 1。
名称
assemble_class
语法
mixed assemble_class( mixed *elements );
描述
用数组 `elements` 的元素实例化一个结构体。
名称
disassemble_class
语法
mixed *disassemble_class( mixed instantiated_class );
描述
返回一个包含已实例化的结构体 `instantiated_class` 所有成员的值的 mixed 类型的数组。
名称
fetch_class_member
语法
mixed fetch_class_member( mixed instantiated_class, int index );
描述
返回实例化的类 `instantiated_class` 的索引为 `index` 的成员的值,这对数据成员没有标签(变量名)的匿名类非常有用。
名称
store_class_member
语法
mixed store_class_member(mixed instantiated_class, int class_element, mixed value);
描述
保存结构体 `instantiated_class` 的第 `class_element` 个成员的值为 `value`,返回更新后的结构体。
名称
classes() - 返回指定对象中的 class 变量信息
语法
mixed *classes( object ob );
mixed *classes( object ob, int flag );
描述
返回对象 `ob` 中的所有 class 类型变量数组,如果指定参数 `flag` 值为 1,还会返回成员变量名和变量类型。
名称
num_classes() - 返回指定对象中的 class 变量数量
语法
int num_classes(object ob);
描述
此外部函数返回对象 `ob` 中定义的 class 类型变量的数量。
通过这些 efun 我们可以很方便的使用结构体来开发游戏功能…………
class 和 struct 本质上是相同的概念
在 FluffOS 的 LPC 语境中,class 和 struct 本质上是相同的概念,两者在语法和功能上没有区别,只是名称不同,可视为同义词。以下是具体说明:
1. 文档与语法中的一致性
-
官方文档中,
class和struct常被并列提及(如classp函数的描述明确提到“检测变量是否是 class/struct 类型”),且示例中两者可互换使用。例如:// 用 class 定义 class Person { string name; int age; } // 用 struct 定义(功能完全相同) struct Person { string name; int age; } -
实例化、成员访问的语法对两者完全一致:
class Person p = new(class Person, name: "Alice"); struct Person s = new(struct Person, age: 30); p->name = "Bob"; // 点运算符 (.) 也可使用 s.age = 25;
2. 实现层面的统一性
- 从代码实现来看(如
src/vm/internal/base/class.h和class.cc),LPC 对class和struct的处理逻辑完全一致:- 两者均通过
class_def_t结构体存储定义信息(类名、成员数量等)。 - 实例均以
array_t形式存储,成员通过固定偏移量访问,内存管理(引用计数、分配/释放)机制相同。
- 两者均通过
- 编译器层面(
src/compiler/internal/compiler.cc),class和struct的解析、成员校验、代码生成流程完全共享同一套逻辑,无特殊区分。
3. 历史与命名习惯
- 这种“同义性”可能源于 LPC 对 C 语言的借鉴(C 中
struct是原生结构,C++ 引入class并增加访问控制),但 LPC 简化了这一设计,未区分两者的语义或功能。 - 实际使用中,
class和struct的选择仅取决于代码风格,例如:- 用
struct表示简单数据聚合(如坐标struct Point { int x; int y; })。 - 用
class表示更复杂的对象(如角色属性class Character { ... })。
- 用
总结
在 FluffOS 的 LPC 中,class 和 struct 是完全等价的,没有功能、语法或实现上的区别,仅名称不同,可根据代码风格自由选择使用。
在 FluffOS 的 LPC 中,class 和 struct 虽然功能完全相同,但实际开发中通常会根据代码风格和语义侧重来选择使用,形成一些约定俗成的场景偏好:
1. 用 struct 的场景:简单数据聚合(“纯数据容器”)
适合表示无复杂行为的简单数据结构,强调“数据的组合”而非“对象的抽象”。
典型场景:
- 基础数据结构:如坐标、矩形区域、时间戳等纯粹存储值的结构。
struct Point { int x; int y; }; // 仅存储坐标信息 struct Time { int hour; int min; int sec; }; // 存储时间分量 - 临时数据载体:如函数返回的多值结果、网络协议的数据包结构。
struct AttackResult { int damage; string msg; }; // 攻击结果的多值封装 - 轻量信息组:成员少且无关联行为,仅用于集中存储相关数据。
2. 用 class 的场景:复杂对象建模(“带语义的实体”)
适合表示具有明确语义或潜在行为的实体,强调“对象的抽象”而非“单纯的数据组合”。
典型场景:
- 核心游戏实体:如角色、物品、技能等具有复杂属性和潜在行为的对象。
class Player { string name; int hp; int mp; mapping skills; // 包含技能等复杂属性 }; // 代表“玩家”这一实体,有明确的业务语义 - 业务逻辑载体:虽然 LPC 的
class不直接支持成员函数,但可结合外部函数实现“数据+行为”的绑定(类似面向对象)。class Item { string id; int weight; string desc; }; // 与 Item 相关的行为函数(模拟成员函数) void item_use(class Item item, object user) { ... } - 跨模块共享的复杂结构:当结构需要在多个模块中传递,且具有明确的业务含义时,用
class更易体现其抽象性。
核心区别:语义侧重而非功能
两者的选择本质是代码可读性和团队约定的问题:
struct更像“数据打包”,适合简单、无歧义的纯数据场景。class更像“实体抽象”,适合具有业务含义、可能扩展行为的复杂场景。
实际开发中,只要团队内部保持一致即可,无需严格区分。例如:小项目可能全程用 struct,而大型项目可能用 class 表示核心实体,用 struct 表示辅助数据结构。
在 FluffOS 的 LPC 开发中,class 和 mapping 是两种核心数据类型,各自适用于不同场景,其核心差异和适用边界可总结如下:
一、本质与设计目的
| 类型 | 本质 | 设计目的 | 核心特征 |
|---|---|---|---|
class |
预定义结构的自定义类型 | 封装固定结构的数据,强调类型安全和结构化 | 编译期确定成员(名称、类型、数量) |
mapping |
动态键值对集合(哈希表实现) | 存储灵活可变的关联数据,强调动态性 | 运行时可自由增删键值对 |
二、核心特性对比
| 特性 | class |
mapping |
|---|---|---|
| 结构确定性 | 固定结构(编译期定义,不可动态修改) | 动态结构(运行时可增删键值对) |
| 访问方式 | 通过成员名访问(c->hp 或 c.hp) |
通过键访问(m["hp"] 或 m[123]) |
| 类型安全 | 编译期校验(访问不存在的成员会报错) | 无编译期校验(错误键仅在运行时返回 0) |
| 性能 | 高效(直接通过偏移量访问) | 中等(需计算哈希、处理冲突) |
| 成员类型 | 声明时固定(如 int hp; string name) |
键和值可混合多种类型(字符串、数字等) |
| 代码可读性 | 自文档化(成员明确可见) | 依赖注释(需记忆键的含义) |
三、适用场景划分
优先使用 class 的场景
-
数据结构固定且高频访问
如角色基础属性(hp/mp/name)、物品核心信息(id/weight/damage)等,结构稳定且需频繁读写。
示例:class Player { string name; int hp; int mp; object weapon; }; // 结构固定,适合用 class 封装 -
需要类型安全和编译期校验
如协议数据、配置模板等,需避免因键名拼写错误(如m["hpp"]误写m["hp"])导致的隐蔽 bug。 -
表示具有明确语义的实体
如class NPC、class Skill等,通过结构定义直观体现业务含义,提升代码可读性。
优先使用 mapping 的场景
-
数据结构动态可变
如玩家临时状态(buff、任务进度)、动态配置表(不同场景下字段不同)等,需要灵活增删字段。
示例:mapping temp_status = ([ "poisoned": 1, // 临时中毒状态 "blessed": 30, // 祝福持续30秒 "quest_step": 2 // 任务进度动态变化 ]); // 字段随状态动态增减,适合用 mapping -
键值关联查询
如物品 ID 到物品对象的映射(([1001: obj_sword, 1002: obj_potion])),需通过键快速查找值。 -
临时数据聚合
如函数返回多值结果、临时存储中间数据,无需长期维护结构。
四、总结:互补而非替代
class是“静态骨架”:适合封装固定结构、高频访问、有明确语义的数据,提供类型安全和效率。mapping是“动态容器”:适合存储结构可变、临时或需要键值查询的数据,提供灵活性。
实际开发中两者常结合使用:例如用 class 定义角色基础属性,用 mapping 存储其临时状态(如 buff、任务进度),兼顾结构稳定性和动态扩展性。
要在多个代码文件中使用同一个类(也称为结构体),理想的做法是为类定义创建一个可共享的继承文件。不要使用include语句。
定义类的结构
class Person {
string name; // 字符串类型的name成员
int level; // 整数类型的level成员
}
创建类的实例
创建一个空的Person类实例:
class Person npc = new(class Person);
创建带有初始值的Person类实例:
class Person npc = new(class Person, name: "姓名", level: 15);
访问成员值
使用点运算符访问成员值:
npc.name = "姓名"; // 给name成员赋值
npc.level = 15; // 给level成员赋值
使用箭头运算符访问成员值:
npc->name = "姓名"; // 给name成员赋值
npc->level = 15; // 给level成员赋值
类的类型转换
function (mixed p, string name) {
((class Person)p)->name = name; // 将p转换为Person类并访问其name成员
}