【C#】【Unity】カスタム属性の基礎知識コンプリートガイド

# 最初に

本記事はC#言語機能における`カスタム属性`について調べたものとなります。
サンプルコードの実況環境に関しては**Mono 5.16.0(C# 7.3サポート)**となっております。

# 属性とは？

属性とは`private`や`public`のような追加情報をクラスやメンバに与える機能を指します。

# カスタム属性とは？

**`System.Attribute`**を継承するクラスを作成することで、`private`や`public`のような属性を用意できるC#の機能です。カスタム属性はクラスやメンバに対して自由に追加することができます。また、この機能は、リフレクションと組み合わせて使うことで非常に強力な機能を生み出すことが可能です。

自分でカスタム属性を作成することもできます。定義するためには、**`System.Attribute`**を継承したpublicな属性クラスを作成する必要があります。定義時には、クラス名の最後に「Attribute」をつけることが推奨されています(実際既存のカスタムクラス群はそのような名前付けがされています)。ただし使用時には、`Attribute`という**サフィックスを省略**することができます。

カスタム属性を使用することで以下のようなメリットがあります。

- 条件付きコンパイルのような、**コンパイラへの指示を行うことができる**。
- クラス利用者へ開発者が伝えたい**情報をメタデータとしてプログラムに埋め込むことができる**。
- `リフレクション`と組み合わせて、**プログラム実行時に属性情報を取得し使用する**。

カスタム属性によっては**パラメータを渡すことが可能**なものもあります(後述)。属性自体が引数を取らない場合は**括弧の省略が可能**です。

ただし以下のような注意点もあります。

- **属性はメタデータ**のため動的な設定は不可能。
- `リフレクション`を使用することで**パフォーマンスが少々ボトルネック**になるかもしれません。
- **カスタム属性がインスタンス化されるタイミングは**CLRによって決まり、**ユーザーが管理することはできません**。


# 属性の基本的な使い方

カスタム属性は以下のような記述して使用することができます。

```
[属性名(位置指定パラメーター)]
クラスやメソッドの定義
```
**位置指定パラメーターとは、指定した属性クラスのコンストラクタに渡される引数のこと**です。特定の順序で値を指定する必要があり、省略はできません。

以下の場合はOldMethodを使用した場合に、「Old version. Don't use.」という警告が表示されるよう属性をメソッドに付与しています。

```C#
// メソッドを対象に属性付与
// Obsoleteの説明は後述
[Obsolete("Old version. Don't use.")] 
public void OldMethod()
{
    // 何らかの処理
}
```

<br>
複数の属性を指定することも可能です。その場合は`,`で区切ります。

```
[属性名(位置指定パラメータ), 属性名(位置指定パラメーター)]
クラスやメソッドの定義
```

```C#
// Conditionalの説明も後述
[Conditional("DEBUG"), Obsolete("Old version. Don't use.")] 
public void OldMethod()
{
    // 何らかの処理
}
```
<br>
属性クラスによっては、クラス内で`public`と指定されているフィールドやプロパティに対して値を設定することができます。

```
[属性名(位置指定パラメータ, 名前付きパラメーター = 値)]
クラスやメソッドの定義
```
この値を設定するためにフィールドやプロパティ名を指定する箇所を**名前付きパラメーター**と呼びます。

```C#
// StringLengthの説明も後述
[StringLength(15, ErrorMessage = "会社名は15文字以内で記述してください。")]
    public String OfficeName
```

## 標準ライブラリから提供される基本的なカスタム属性の種類

コンパイラやVisual Studioに対して指示を行える便利な属性がいくつか標準ライブラリによって提供されています。これらは名前の後に`Attribute`というサフィックスが入っていますが、**使用時には省略する**ことができます。

|属性名|概要|
|:----|:----|
|[AttributeUsage]|属性クラスを自作した場合に使用します(後述)。属性の用途を指定します。|
|[Conditional]| **指定のシンボルが定義されている場合のみ**属性対象のメソッド(又は属性)を呼び出すように指定します。|
|[CLSCompliant]|CLSへの準拠を検証するかどうか指定します。|
|[Obsolete]|属性対象のメソッド等が古いバージョンのため使用が推奨されないことを表します(使用した場合警告が発せられます)。|
<br>

バリデーションに使用する属性として有用なものは`System.ComponentModel.DataAnnotations`下に存在しており、`Validator`クラスを使用することで、**対象データの妥当性を検証する**ことができます。

(バリデーションってなんだ？という方は[「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典「バリデーション(validation)」](https://wa3.i-3-i.info/word11610.html)へ）

|属性名|概要|
|:----|:----|
| [Required] | 属性対象のデータに対してnullや空文字を許可しないことを示します。 |
| [Range] | 属性対象のデータの値の範囲を指定します。 |
| [StringLength] | 属性対象の文字列データに対して最大長/最小長を指定します。 |

```C#
[Required(ErrorMessage="入力必須項目です。"), StringLength(20, ErrorMessage="名前は20桁以内で入力してください")]
    String OfficeName;
```

## Unityから提供される基本的なカスタム属性の種類

### エディタ拡張系

|属性名|概要|
|:----|:----|
|[SerializeField]|属性対象がシリアライズ可能であることを表します。|
|[NonSerialized]|対象がシリアライズ不可能であることを表します。|
|[Tooltip]|マウスカーソルがフィールドの上に置かれた場合に位置指定パラメーターとして渡された値を表示します。|
|[Space]|フィールド間に位置指定パラメーターとして渡された値分スペースを設定します。|
|[Header]|フィールドの上に位置指定パラメーターとして渡された文字をタイトルとして表示します。|
|[Multiline]|位置指定パラメーターとして渡された値分の行数を入力することができるテキストフィールドを設定します。|
|[TextArea]|複数行の入力が可能なテキストフィールドを設定します。位置指定パラメーターで、行数のMIN値・MAX値が設定可能です。|
|[HideInInspector]|public等のシリアライズするフィールドをInspectorに表示しないように設定できます。|
|[FormerlySerializedAs]|変数名を変更した際に変数が持っている情報を変数名変更後の変数に引き継がれ、保持するよう設定できます。|

### コンポーネント系

|属性名|概要|
|:----|:----|
|[RequireComponent]|位置指定パラメーターとして渡された(例えば`typeof(Rigidbody)`)コンポーネントがアタッチされていなければ追加します。|
|[DisallowMultipleComponent]|対象のオブジェクトに対して複数のコンポーネントの追加を設定不可能にします。|
|[ExecuteInEditMode]|コンポーネントのStartやUpdateメソッドをゲームスタートしていない状態でも確認できるようになります。|


### C++との連携系

|属性名|概要|
|:----|:----|
|MonoPInvokeCallback|C#のメソッドをC++から呼び出せるようにします。|
|DLLImport|C++のメソッドをC#から呼び出せるようにします。|

## 属性対象の指定

基本的に属性の対象となるのは、その直後に続くクラスやメソッド、フィールドとなります。しかし、**明示的に属性の対象を変更**することができます。

```
[属性対象: 属性名(位置指定パラメーター等...)]
```

属性対象としては以下のものを指定できます。

- assembly
- module
- type (クラス,構造体,列挙型,デリゲート等の型が対象)
- method
- property
- field
- param
- event
- return

特にプロパティやイベントでは主に`method`や`param`、`field`辺りを使うこととなります。

```C#
class Test
{
    public int Prop
    {
        // getメソッドに対応
        [method: CustomAttribute]
        // getの返却値に対応
        [return: Required]
        get => 10;

        // setメソッドに対応
        [method: CustomAttribute] 
        // setが受け取るvalue引数に対応
        [param: Required]
        set { }
    }

    // バッキングフィールドを対象とする
    [field: CustomAttribute]
    public int AutoProp { get; }
}
```


# 独自のカスタムクラス

標準ライブラリで提供されているような属性は`System.Attribute`を継承したクラス、又はその派生クラスを継承したクラスです。つまり同様に継承を行うことで**自分自身でも独自の属性クラスを作成することができます**。

作成する属性クラスにプロパティが含まれている場合は**プロパティは読み取り/書き込み可能**にしておく必要があります。


ここで重要となるのは、**作成する属性クラスの振る舞いを決める`AttributeUsage`属性**です。この属性の位置指定パラメーターである`AttributeTargets`と名前付きパラメーターである`AllowMultiple`と`Inherited`を確認してみましょう。

**「AttributeTargets」**

**属性を適用できる範囲を指定できます**。

例えばAttributeTargets.Classを設定することで、クラスに対して属性を設定可能、とできます。AttributeTargets.Allであれば全ての要素に対して適用が可能です。また、複数のAttributeTargetsの値を渡すことも可能です。

**「AllowMultiple」**

作成した属性を**ターゲットに対して複数回適用が可能か**どうかをbool値で指定します。`false`であれば一度のみとなります。デフォルト値は`false`です。

**「Inherited」**

作成した属性の**ターゲットが継承された後も、そのターゲットに設定された属性が一緒に継承されるか**どうかをbool値で指定します。デフォルト値は`true`です。

上記を踏まえてサンプルコードを見てみましょう

```C#
// この場合はクラスと構造体に対し属性の設定が可能で、複数回付与でき、属性のターゲットが継承時には属性は引き継がれません。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, 
    AllowMultiple = true, Inherited = false)]
public class DeveloperAttribute : System.Attribute
{
    // 作者名
    private string Name;
    // 対象が動作するバージョン
    public float Version;
    public AuthorAttribute(string name) { Name = name; }
}
```

# 最後に

如何でしたでしょうか。一口に属性と言っても調べてみるととても奥が深い機能であるということが伝わったかと思います。属性機能をどんどん活用して楽しいC#ライフを送りましょう。

# 参考

- [カスタム属性の作成 (C#)](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/concepts/attributes/creating-custom-attributes)
- [属性 ++C++](https://ufcpp.net/study/csharp/sp_attribute.html)
- [カスタム属性の記述](https://docs.microsoft.com/ja-jp/dotnet/standard/attributes/writing-custom-attributes)
- [属性 (Attribute)](https://so-zou.jp/software/tech/programming/c-sharp/attribute/)
- [[c#] C＃でカスタム属性を作成する方法](https://code.i-harness.com/ja/q/4a74a1)