第14章 属性とメタデータ
14.1 概要
| 言語 | 機能名 | 構文 |
|---|---|---|
| PHP | Attributes | #[AttributeName] |
| TypeScript | Decorators | @decoratorName |
| VB.NET | Attributes | <AttributeName> |
14.2 PHP Attributes(PHP 8.0+)
基本的な使用法
<?php
// 組み込み属性
#[Deprecated("Use newMethod instead")]
function oldMethod(): void {}
// クラスに属性を付与
#[Entity]
#[Table(name: "users")]
class User {
#[Column(type: "integer", primary: true)]
public int $id;
#[Column(type: "string", length: 255)]
public string $name;
#[Route("/users/{id}", methods: ["GET"])]
public function show(int $id): Response {}
}
カスタム属性の定義
<?php
#[Attribute(Attribute::TARGET_CLASS)]
class Entity {
public function __construct(
public ?string $table = null
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Column {
public function __construct(
public string $type,
public int $length = 255,
public bool $primary = false
) {}
}
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Route {
public function __construct(
public string $path,
public array $methods = ["GET"]
) {}
}
属性の読み取り
<?php
$reflection = new ReflectionClass(User::class);
// クラス属性
$attributes = $reflection->getAttributes(Entity::class);
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
echo $instance->table;
}
// プロパティ属性
foreach ($reflection->getProperties() as $property) {
$attrs = $property->getAttributes(Column::class);
foreach ($attrs as $attr) {
$column = $attr->newInstance();
echo $column->type;
}
}
// メソッド属性
foreach ($reflection->getMethods() as $method) {
$routes = $method->getAttributes(Route::class);
foreach ($routes as $route) {
$instance = $route->newInstance();
echo $instance->path;
}
}
14.3 TypeScript Decorators
基本的な使用法(実験的機能)
// tsconfig.json で有効化
// "experimentalDecorators": true
// クラスデコレータ
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@readonly
greet() {
return "Hello, " + this.greeting;
}
}
// プロパティデコレータ
class User {
@required
name: string = "";
@range(0, 150)
age: number = 0;
}
// パラメータデコレータ
class UserService {
greet(@validate name: string): string {
return `Hello, ${name}`;
}
}
デコレータの定義
// クラスデコレータ
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
// デコレータファクトリ
function entity(tableName: string) {
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
tableName = tableName;
};
};
}
// メソッドデコレータ
function readonly(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.writable = false;
return descriptor;
}
// プロパティデコレータ
function required(target: any, propertyKey: string) {
// メタデータに記録
Reflect.defineMetadata("required", true, target, propertyKey);
}
// パラメータデコレータ
function validate(
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) {
const existingValidations: number[] =
Reflect.getMetadata("validate", target, propertyKey) || [];
existingValidations.push(parameterIndex);
Reflect.defineMetadata("validate", existingValidations, target, propertyKey);
}
// ログデコレータ(実用例)
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
TC39 Stage 3 Decorators(新構文)
// 新しいデコレータ構文(TypeScript 5.0+)
// tsconfig.json で "experimentalDecorators": false
function logged<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const methodName = String(context.name);
return function (this: This, ...args: Args): Return {
console.log(`Calling ${methodName}`);
return target.call(this, ...args);
};
}
class Example {
@logged
greet(name: string) {
return `Hello, ${name}`;
}
}
14.4 VB.NET Attributes
基本的な使用法
' 組み込み属性
<Obsolete("Use NewMethod instead")>
Public Sub OldMethod()
End Sub
' 複数の属性
<Serializable>
<DataContract>
Public Class User
<DataMember>
<Required>
Public Property Name As String
<DataMember>
<Range(0, 150)>
Public Property Age As Integer
End Class
' 属性のパラメータ
<DllImport("user32.dll", CharSet:=CharSet.Auto)>
Public Shared Function MessageBox(hWnd As IntPtr,
text As String,
caption As String,
type As UInteger) As Integer
End Function
カスタム属性の定義
' 属性クラス
<AttributeUsage(AttributeTargets.Class Or AttributeTargets.Struct)>
Public Class EntityAttribute
Inherits Attribute
Public Property TableName As String
Public Sub New()
End Sub
Public Sub New(tableName As String)
Me.TableName = tableName
End Sub
End Class
<AttributeUsage(AttributeTargets.Property, AllowMultiple:=False)>
Public Class ColumnAttribute
Inherits Attribute
Public Property Name As String
Public Property Type As String
Public Property Length As Integer = 255
Public Property IsPrimaryKey As Boolean = False
Public Sub New(name As String)
Me.Name = name
End Sub
End Class
<AttributeUsage(AttributeTargets.Method, AllowMultiple:=True)>
Public Class RouteAttribute
Inherits Attribute
Public Property Path As String
Public Property Methods As String() = {"GET"}
Public Sub New(path As String)
Me.Path = path
End Sub
End Class
' 使用
<Entity("users")>
Public Class User
<Column("user_id", IsPrimaryKey:=True)>
Public Property Id As Integer
<Column("user_name", Length:=100)>
Public Property Name As String
End Class
属性の読み取り
Dim type = GetType(User)
' クラス属性
Dim entityAttr = type.GetCustomAttribute(Of EntityAttribute)()
If entityAttr IsNot Nothing Then
Console.WriteLine($"Table: {entityAttr.TableName}")
End If
' プロパティ属性
For Each prop In type.GetProperties()
Dim columnAttr = prop.GetCustomAttribute(Of ColumnAttribute)()
If columnAttr IsNot Nothing Then
Console.WriteLine($"Column: {columnAttr.Name}, Type: {columnAttr.Type}")
End If
Next
' すべての属性を取得
Dim allAttributes = type.GetCustomAttributes(True)
For Each attr In allAttributes
Console.WriteLine(attr.GetType().Name)
Next
' 属性の存在確認
If Attribute.IsDefined(type, GetType(SerializableAttribute)) Then
Console.WriteLine("Class is serializable")
End If
よく使用される組み込み属性
| 属性 | 用途 |
|---|---|
<Obsolete> |
非推奨マーク |
<Serializable> |
シリアライズ可能 |
<DataContract> / <DataMember>
|
WCF/JSON シリアライズ |
<Required> |
必須フィールド |
<Range> |
値の範囲検証 |
<StringLength> |
文字列長制限 |
<RegularExpression> |
正規表現検証 |
<Conditional> |
条件付きコンパイル |
<DllImport> |
P/Invoke |
<DebuggerDisplay> |
デバッガ表示 |
<CallerMemberName> |
呼び出し元メンバー名 |
' 実用例
Public Class ValidationExample
<Required(ErrorMessage:="Name is required")>
<StringLength(100, MinimumLength:=1)>
Public Property Name As String
<Range(0, 150, ErrorMessage:="Age must be between 0 and 150")>
Public Property Age As Integer
<RegularExpression("^[\w\.-]+@[\w\.-]+\.\w+$")>
Public Property Email As String
End Class
' デバッガ表示
<DebuggerDisplay("User: {Name}, Age: {Age}")>
Public Class DebugUser
Public Property Name As String
Public Property Age As Integer
End Class
' 条件付きコンパイル
Public Class Logger
<Conditional("DEBUG")>
Public Shared Sub DebugLog(message As String)
Console.WriteLine($"[DEBUG] {message}")
End Sub
End Class
' 呼び出し元情報
Public Sub Log(message As String,
<CallerMemberName> Optional memberName As String = "",
<CallerFilePath> Optional filePath As String = "",
<CallerLineNumber> Optional lineNumber As Integer = 0)
Console.WriteLine($"{filePath}:{lineNumber} [{memberName}] {message}")
End Sub
14.5 メタプログラミングの比較
| 機能 | PHP | TypeScript | VB.NET |
|---|---|---|---|
| 実行時の型情報 | Reflection API | 制限的(実行時に消える) | Reflection API |
| コンパイル時処理 | なし | デコレータ | なし |
| コード生成 | eval、include | なし | Reflection.Emit、Source Generators |
| 動的メソッド呼び出し | $obj->$method() |
obj[method]() |
CallByName、Reflection |