前回は、外部アセンブリを読み込み、実行時に型やメソッドを呼び出す方法を見ました。
今回は 属性 を扱います。
属性は、型、メソッド、プロパティ、アセンブリなどに追加できるメタデータです。
属性を使うと、コードに「追加の意味」を与えられます。
そしてリフレクションでその意味を読み取ることで、フレームワークやアプリケーションの動作を変えられます。
属性とは
属性は、コード要素に付与する説明情報です。
たとえば、古くなったメソッドへ Obsolete 属性を付けると、呼び出し側に警告を出せます。
public class FileImporter
{
[Obsolete("ImportOld は廃止予定です。Import を使ってください。")]
public void ImportOld()
{
}
public void Import()
{
}
}
ImportOld() を呼び出すと、コンパイル時に警告が表示されます。
属性がコンパイラーや実行時の仕組みに意味を伝えている例です。
属性名の短い書き方
属性クラスの名前は、通常 Attribute で終わります。
public sealed class FeatureAttribute : Attribute
{
}
使うときは、末尾の Attribute を省略できます。
[Feature]
public class CsvImportFeature
{
}
もちろん、正式名で書くこともできます。
[FeatureAttribute]
public class JsonImportFeature
{
}
一般的には短い書き方を使います。
属性にコンストラクター引数を渡す
属性にはコンストラクターを定義できます。
[AttributeUsage(AttributeTargets.Class)]
public sealed class FeatureAttribute : Attribute
{
public FeatureAttribute(string name)
{
Name = name;
}
public string Name { get; }
}
使用例です。
[Feature("CSV 取り込み")]
public class CsvImportFeature
{
}
コンストラクター引数は、属性を付けるときに必ず指定する値に向いています。
名前付きプロパティを使う
任意指定の値は、プロパティとして用意できます。
[AttributeUsage(AttributeTargets.Class)]
public sealed class FeatureAttribute : Attribute
{
public FeatureAttribute(string name)
{
Name = name;
}
public string Name { get; }
public string Category { get; set; } = "General";
public int Order { get; set; }
}
使用例です。
[Feature("CSV 取り込み", Category = "Import", Order = 10)]
public class CsvImportFeature
{
}
必須値はコンストラクター、任意値はプロパティにすると、属性の意味が読みやすくなります。
属性の適用先を制限する
AttributeUsage を使うと、属性をどこに付けられるかを制限できます。
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = false,
Inherited = false)]
public sealed class FeatureAttribute : Attribute
{
public FeatureAttribute(string name)
{
Name = name;
}
public string Name { get; }
}
この例では、クラスとメソッドにだけ付けられます。
同じ場所に複数付けることはできず、派生クラスには継承されません。
属性の用途が明確なら、適用先を制限しておくと誤用を防げます。
属性をリフレクションで読む
属性は、リフレクションで読み取れます。
using System.Reflection;
Type type = typeof(CsvImportFeature);
FeatureAttribute? attribute = type
.GetCustomAttribute<FeatureAttribute>();
if (attribute != null)
{
Console.WriteLine(attribute.Name);
Console.WriteLine(attribute.Category);
Console.WriteLine(attribute.Order);
}
複数取得したい場合は GetCustomAttributes<T>() を使います。
foreach (FeatureAttribute attribute in type.GetCustomAttributes<FeatureAttribute>())
{
Console.WriteLine(attribute.Name);
}
型を直接参照せずに属性を読む
プラグインでは、アプリ本体が属性型を直接参照できる設計にすることが多いです。
ただし、診断ツールのように「未知の属性も表示したい」場合は、属性型を文字列で見ることもあります。
using System.Reflection;
Type type = typeof(CsvImportFeature);
foreach (CustomAttributeData data in type.CustomAttributes)
{
Console.WriteLine(data.AttributeType.FullName);
foreach (CustomAttributeTypedArgument argument in data.ConstructorArguments)
{
Console.WriteLine($" 引数: {argument.Value}");
}
foreach (CustomAttributeNamedArgument argument in data.NamedArguments)
{
Console.WriteLine($" {argument.MemberName}: {argument.TypedValue.Value}");
}
}
CustomAttributeData は、属性のインスタンスを作らずにメタデータとして読むときに便利です。
アセンブリに属性を付ける
属性は、型やメソッドだけでなく、アセンブリにも付けられます。
using System.Reflection;
[assembly: AssemblyTitle("Sample Plugin")]
[assembly: AssemblyDescription("CSV import plugin")]
別ファイルにまとめることもできます。
// AssemblyInfo.cs
using System.Reflection;
[assembly: AssemblyCompany("MyCompany")]
[assembly: AssemblyProduct("Plugin Samples")]
SDK スタイルのプロジェクトでは、プロジェクトファイルから指定できる属性も多くあります。
<PropertyGroup>
<AssemblyTitle>Sample Plugin</AssemblyTitle>
<Company>MyCompany</Company>
<Product>Plugin Samples</Product>
</PropertyGroup>
プロジェクトファイルで管理できる情報は、ビルド設定と一緒に置けるため扱いやすいです。
属性で機能を発見する
属性は、拡張機能を発見するためにも使えます。
[AttributeUsage(AttributeTargets.Class)]
public sealed class PluginAttribute : Attribute
{
public PluginAttribute(string displayName)
{
DisplayName = displayName;
}
public string DisplayName { get; }
public string Category { get; set; } = "General";
}
[Plugin("CSV 取り込み", Category = "Import")]
public class CsvImporter
{
public void Run()
{
Console.WriteLine("CSV を取り込みます。");
}
}
探索側です。
using System.Reflection;
Assembly assembly = typeof(CsvImporter).Assembly;
foreach (Type type in assembly.GetTypes())
{
PluginAttribute? plugin = type.GetCustomAttribute<PluginAttribute>();
if (plugin == null)
{
continue;
}
Console.WriteLine($"{plugin.DisplayName} ({plugin.Category})");
Console.WriteLine($" 型: {type.FullName}");
}
このパターンを使うと、クラス名ではなく属性に基づいて機能を発見できます。
まとめ
属性は、コードにメタデータを付与する仕組みです。
コンパイラーに意味を伝える場合もあれば、実行時にリフレクションで読み取ってアプリケーションの動作を変える場合もあります。
特に、プラグインや拡張機能を作る場合、属性は「この型は何の機能か」「表示名は何か」「どのカテゴリか」を表現するのに向いています。
次回は、リフレクション、遅延バインディング、属性を組み合わせて、拡張可能なアプリケーションの構造を作ります。