特性(Attribute)
attribute 声明一种元数据标注,@名字 把它贴到声明头上。特性不给声明附加任何行为:它是写给「读代码的人以外的读者」的数据,编辑器面板、序列化标记、热更新边界、宿主注入面都靠它表达。声明与应用在编译期全量校验,读取在运行期走反射。
声明
attribute Reviewed(reviewer: string) targets method;
attribute Range(min: int, max: int) targets field, property allow multiple;
attribute Mark targets type;全形是 attribute 名字(参数表) targets 原子列表 allow multiple;,子句顺序固定:参数表、targets、allow multiple。要点:
- 参数表复用函数形参形态:
名字: 类型,可带默认值(weight: int = 1);实参必须是编译期常量; targets白名单九个原子:type | field | property | event-prop | method | constructor | func | global | module。event-prop是唯一多词原子;类的方法用method、顶层函数用func,两个原子分立;allow multiple声明可重复应用,缺省single;- 省写
targets的极简形attribute Mark;语法合法,但 targets 是空集:贴到任何声明都报 MS4050。声明特性时把 targets 写全。
应用
@名字 贴在声明头上,可逐个堆叠,也可数组形一次贴一组:
enum Mode { Fast, Slow }
attribute Tag(name: string, weight: int = 1) targets type, field;
attribute Note(text: string) targets type allow multiple;
attribute Pick(m: Mode) targets type;
@Tag("英雄", weight: 5)
@Pick(Mode.Fast)
@Note("第一条")
@Note("第二条")
class Hero
{
@Tag("血量", weight: 2)
public var hp: int = 0;
}- 实参表复用调用形态:位置实参与
name: 实参命名实参可混用(@Tag("英雄", weight: 5)); - 带默认值的参数可以省,位置形省尾参、命名形按名省都行;省了的参数不进读取面的
Args(); - 枚举实参三形放行:限定名(
Mode.Fast)、类型定向裸名(Fast)与命名实参,读侧存裸变体名字符串; single特性重复应用报 MS4051。
校验面
标注在编译期全量校验,贴错当场报错,不存在「贴了没生效」的静默面:
| 诊断 | 消息原文 |
|---|---|
| MS4050 | Attribute 'Mark' is not allowed on this kind of declaration. |
| MS4051 | Attribute 'Tag' cannot be applied more than once. |
| MS4052 | Undeclared attribute 'Nope'. |
| MS4094 | Attribute 'T' expects 2 argument(s) (got 1). |
| MS4095 | 实参类型不符(含变体属于另一个枚举) |
| MS4096 | 实参须编译期常量;payload 变体与 flags 成员不可作实参 |
读取:四级 Attrs()
特性在读侧有四级入口:t.Attrs()(类型)、f.Attrs()(字段)、p.Attrs()(属性)、m.Attrs()(方法)。Attr 句柄出 .Name 与 .Args() -> map<string, any>,Attrs() 恒按应用序排列:
func main() -> int
{
val t = typeof(Hero);
print(t.Attrs().Count);
val tag = t.Attrs()[0];
print(tag.Name, tag.Args()["name"], tag.Args()["weight"]);
print(tag.Args().Contains("weight"), tag.Args().Contains("nope"));
for a in t.Attrs()
{
if a.Name == "Note"
{
print(a.Args()["text"]);
}
}
print(t.Fields()[0].Attrs()[0].Name, t.Fields()[0].Attrs()[0].Args()["weight"]);
return 0;
}4
Tag 英雄 5
True False
第一条
第二条
Tag 2Args() 的契约有三条:
- 位置实参按声明参名升名:读侧只有一套命名键,
@Tag("英雄", 5)读出的也是"name"与"weight"; - 只含显式实参:带默认值的参数省略时
Args()里没有该键,判存用Contains,别直接下标; - 枚举实参存裸变体名字符串:
@Pick(Mode.Fast)读出"Fast",与枚举型本身的比较用字符串判。
Args() 是普通 map,读取遵循二形词表:键保证存在用 Args()[键](缺键硬错),不确定先 Contains 探测或 try? Args().Get(键) ?? 默认。
特性驱动分派
特性的典型消费是「扫出贴了标注的类型,建注册表」:Reflect.Types() 给出全部已加载类型,配 Attrs() 过滤就是一遍声明序稳定的扫描。
attribute Panel(title: string) targets type;
@Panel("背包")
class Inventory
{
public var slots: int = 24;
}
@Panel("状态")
class Status
{
public var hp: int = 100;
}
class Plain { }
func main() -> int
{
for t in Reflect.Types()
{
for a in t.Attrs()
{
if a.Name == "Panel"
{
print(a.Args()["title"], t.Name);
}
}
}
return 0;
}背包 Main.Inventory
状态 Main.Status没贴 @Panel 的 Plain 自然落选,扫描顺序与声明序一致。编辑器面板注册、序列化出向、按标注分派的工具链都是这一套形状。
与宿主特性的关系
std 的 Runtime 模块(extern module 声明面)提供宿主消费的内置特性:@Expose(id) 标记字段对宿主可见(Inspector 可编辑与 UI 序列化),@HotReload(id) 标记热重载时按 identity 保值迁移。内置特性与用户特性语法一致,差别只在消费者:用户特性的读者是外部工具与脚本自身,内置特性的读者是宿主,后者已在读取。
注意
- 特性只贴元数据,不给声明附加行为;要行为注入用协议、混入或扩展;
- 类型级特性不沿继承链:基类贴的
@Tag在派生类的Attrs()里不出现; - 构造器的特性经
Methods()读取(构造器在方法枚举内);global/module级特性本期可贴不可读,是反射面的后续候选; - 属性前缀可堆叠多组(
@A @B),也可数组形@[A, B]; - 本页诊断码集中于 MS4050 / MS4051 / MS4052 / MS4094 / MS4095 / MS4096,语义速查见《语法参考》§3.24。
