コンテンツにスキップ

アノテーション

アノテーションは、ソースコードの特定の機能にメタデータを追加するために使用できます。型、メソッド、インスタンス変数、およびメソッド/マクロパラメータにアノテーションを付けることができます。標準ライブラリの 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

アプリケーション

アノテーションは、特定のインスタンス変数、型、またはメソッドに関するメタデータを格納し、マクロを使用してコンパイル時に読み取ることができるようにするために最適です。アノテーションの主な利点の1つは、インスタンス変数/メソッドに直接適用されるため、これらのプロパティ/メソッドを作成するために標準マクロが不要になるため、クラスがより自然に見えることです。

アノテーションのいくつかのアプリケーション

オブジェクトのシリアライズ

インスタンス変数に適用すると、そのインスタンス変数をシリアライズする必要があるかどうか、またはどのキーでシリアライズするかを決定するアノテーションを作成します。Crystalの JSON::SerializableYAML::Serializable はこの例です。

ORM

アノテーションを使用して、プロパティをORMカラムとして指定できます。インスタンス変数の名前と型は、アノテーションに加えて TypeNode から読み取ることができます。ORM固有のマクロは不要です。アノテーション自体を使用して、カラムに関するメタデータ(NULL許容かどうか、カラムの名前、主キーかどうかなど)を格納することもできます。

フィールド

データはアノテーション内に格納できます。

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>

位置引数

[] メソッドを使用して、コンパイル時に位置引数にアクセスできます。ただし、一度にアクセスできるインデックスは1つだけです。

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"

読み込み

アノテーションは、TypeNodeDefMetaVar、または Arg から .annotation(type : TypeNode) メソッドを使用して読み取ることができます。このメソッドは、指定された型の適用されたアノテーションを表す Annotation オブジェクトを返します。

注記

同じ型の複数のアノテーションが適用されている場合、.annotation メソッドは*最後*のものを返します。

@type 変数と @def 変数を使用して、.annotation メソッドを使用する TypeNode または Def オブジェクトを取得できます。ただし、TypeNode の他のメソッドを使用して TypeNode/Def 型を取得することもできます。たとえば、それぞれ TypeNode.all_subclasses または TypeNode.methods です。

ヒント

TypeNode を取得するより高度な方法については、parse_type メソッドを確認してください。

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"