Skip to Content

特性(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。

校验面

标注在编译期全量校验,贴错当场报错,不存在「贴了没生效」的静默面:

诊断消息原文
MS4050Attribute 'Mark' is not allowed on this kind of declaration.
MS4051Attribute 'Tag' cannot be applied more than once.
MS4052Undeclared attribute 'Nope'.
MS4094Attribute '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 2

Args() 的契约有三条:

  • 位置实参按声明参名升名:读侧只有一套命名键,@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。
最后更新于 2026年10月10日