跳至內容

註解

註解可以用來將元數據添加到原始碼中的某些功能。類型、方法、實例變數和方法/巨集參數可以被註解。使用者定義的註解,例如標準函式庫的 JSON::Field,是使用 annotation 關鍵字定義的。編譯器提供了一些 內建註解

使用者可以使用 annotation 關鍵字定義自己的註解,其工作方式類似於定義 classstruct

annotation MyAnnotation
end

然後,註解可以應用於各種項目,包括

  • 實例和類別方法
  • 實例變數
  • 類別、結構、列舉和模組
  • 方法和巨集參數(儘管後者目前無法存取)
annotation MyAnnotation
end

@[MyAnnotation]
def foo
  "foo"
end

@[MyAnnotation]
class Klass
end

@[MyAnnotation]
module MyModule
end

def method1(@[MyAnnotation] foo)
end

def method2(
  @[MyAnnotation]
  bar
)
end

def method3(@[MyAnnotation] & : String ->)
end

應用

註解最適合用於儲存關於給定實例變數、類型或方法的元數據,以便可以在編譯時使用巨集讀取。註解的主要好處之一是它們直接應用於實例變數/方法,這使得類別看起來更自然,因為不需要標準巨集來建立這些屬性/方法。

註解的一些應用

物件序列化

有一個註解,當應用於實例變數時,決定該實例變數是否應該序列化,或者使用哪個鍵。Crystal 的 JSON::SerializableYAML::Serializable 就是這方面的例子。

ORMs

可以使用註解來將屬性指定為 ORM 欄位。除了註解之外,還可以從 TypeNode 中讀取實例變數的名稱和類型;無需任何 ORM 特定的巨集。註解本身也可以用來儲存關於欄位的元數據,例如是否可為空值、欄位的名稱或是否為主要鍵。

欄位

資料可以儲存在註解中。

annotation MyAnnotaion
end

# The fields can either be a key/value pair
@[MyAnnotation(key: "value", value: 123)]

# Or positional
@[MyAnnotation("foo", 123, false)]

鍵/值

註解鍵/值對的值可以在編譯時透過 [] 方法存取。

annotation MyAnnotation
end

@[MyAnnotation(value: 2)]
def annotation_value
  # The name can be a `String`, `Symbol`, or `MacroId`
  {{ @def.annotation(MyAnnotation)[:value] }}
end

annotation_value # => 2

named_args 方法可以用來讀取註解上的所有鍵/值對,作為 NamedTupleLiteral。這個方法預設定義在所有註解上,並且對於每個應用的註解都是唯一的。

annotation MyAnnotation
end

@[MyAnnotation(value: 2, name: "Jim")]
def annotation_named_args
  {{ @def.annotation(MyAnnotation).named_args }}
end

annotation_named_args # => {value: 2, name: "Jim"}

由於這個方法會回傳一個 NamedTupleLiteral,因此該類型上的所有 方法 都可以使用。特別是 #double_splat,它可以輕鬆地將註解參數傳遞給方法。

annotation MyAnnotation
end

class SomeClass
  def initialize(@value : Int32, @name : String); end
end

@[MyAnnotation(value: 2, name: "Jim")]
def new_test
  {% begin %}
    SomeClass.new {{ @def.annotation(MyAnnotation).named_args.double_splat }}
  {% end %}
end

new_test # => #<SomeClass:0x5621a19ddf00 @name="Jim", @value=2>

位置

位置值可以在編譯時透過 [] 方法存取;但是,一次只能存取一個索引。

annotation MyAnnotation
end

@[MyAnnotation(1, 2, 3, 4)]
def annotation_read
  {% for idx in [0, 1, 2, 3, 4] %}
    {% value = @def.annotation(MyAnnotation)[idx] %}
    pp "{{ idx }} = {{ value }}"
  {% end %}
end

annotation_read

# Which would print
"0 = 1"
"1 = 2"
"2 = 3"
"3 = 4"
"4 = nil"

args 方法可以用來讀取註解上的所有位置參數,作為 TupleLiteral。這個方法預設定義在所有註解上,並且對於每個應用的註解都是唯一的。

annotation MyAnnotation
end

@[MyAnnotation(1, 2, 3, 4)]
def annotation_args
  {{ @def.annotation(MyAnnotation).args }}
end

annotation_args # => {1, 2, 3, 4}

由於 TupleLiteral 的回傳類型是可迭代的,我們可以以更好的方式重寫先前的範例。此外,TupleLiteral 上的所有 方法 也可以使用。

annotation MyAnnotation
end

@[MyAnnotation(1, "foo", true, 17.0)]
def annotation_read
  {% for value, idx in @def.annotation(MyAnnotation).args %}
    pp "{{ idx }} = #{{{ value }}}"
  {% end %}
end

annotation_read

# Which would print
"0 = 1"
"1 = foo"
"2 = true"
"3 = 17.0"

讀取

可以使用 .annotation(type : TypeNode) 方法從 TypeNodeDefMetaVarArg 中讀取註解。這個方法會回傳一個 Annotation 物件,表示所提供類型的應用註解。

注意

如果應用了相同類型的多個註解,則 .annotation 方法會回傳最後一個註解。

可以使用 @type@def 變數來取得 TypeNodeDef 物件,以便使用 .annotation 方法。但是,也可以使用 TypeNode 上的其他方法來取得 TypeNode/Def 類型。例如,分別是 TypeNode.all_subclassesTypeNode.methods

提示

請查看 parse_type 方法,以取得更進階的方式來取得 TypeNode

可以使用 TypeNode.instance_vars 來取得實例變數 MetaVar 物件的陣列,以便讀取在這些實例變數上定義的註解。

注意

TypeNode.instance_vars 目前僅在實例/類別方法的內容中有效。

annotation MyClass
end

annotation MyMethod
end

annotation MyIvar
end

annotation MyParameter
end

@[MyClass]
class Foo
  pp {{ @type.annotation(MyClass).stringify }}

  @[MyIvar]
  @num : Int32 = 1

  @[MyIvar]
  property name : String = "jim"

  def properties
    {% for ivar in @type.instance_vars %}
      pp {{ ivar.annotation(MyIvar).stringify }}
    {% end %}
  end
end

@[MyMethod]
def my_method
  pp {{ @def.annotation(MyMethod).stringify }}
end

def method_params(
  @[MyParameter(index: 0)]
  value : Int32,
  @[MyParameter(index: 1)] metadata,
  @[MyParameter(index: 2)] & : -> String
)
  pp {{ @def.args[0].annotation(MyParameter).stringify }}
  pp {{ @def.args[1].annotation(MyParameter).stringify }}
  pp {{ @def.block_arg.annotation(MyParameter).stringify }}
end

Foo.new.properties
my_method
method_params 10, false do
  "foo"
end
pp {{ Foo.annotation(MyClass).stringify }}

# Which would print
"@[MyClass]"
"@[MyIvar]"
"@[MyIvar]"
"@[MyMethod]"
"@[MyParameter(index: 0)]"
"@[MyParameter(index: 1)]"
"@[MyParameter(index: 2)]"
"@[MyClass]"

警告

只能從類型化的區塊參數讀取註解。請參閱 https://github.com/crystal-lang/crystal/issues/5334

讀取多個註解

#annotations 方法會回傳類型上所有註解的 ArrayLiteral。您可以選擇使用 #annotations(type : TypeNode) 方法並加上 TypeNode 參數,只過濾出所提供的類型的註解。

annotation MyAnnotation; end
annotation OtherAnnotation; end

@[MyAnnotation("foo")]
@[MyAnnotation(123)]
@[OtherAnnotation(456)]
def annotation_read
  {% for ann in @def.annotations(MyAnnotation) %}
    pp "{{ann.name}}: {{ ann[0].id }}"
  {% end %}

  puts

  {% for ann in @def.annotations %}
    pp "{{ann.name}}: {{ ann[0].id }}"
  {% end %}
end

annotation_read

# Which would print:
"MyAnnotation: foo"
"MyAnnotation: 123"

"MyAnnotation: foo"
"MyAnnotation: 123"
"OtherAnnotation: 456"