Attribute


Attribute

Attribute是C#里一个非常强大的元编程机制——它让代码可以携带额外的“说明性数据”,这些数据可以再运行时或编译时被读取,用来改变程序的行为、提供元信息或做自动化处理
可以把它理解为:给类型、方法、字段等打标签,然后通过反射读取这些标签

Attribute是什么

在.NET世界里,Attribute是继承自System-Attribute的类,用来给程序元素(类、方法、属性、参数、程序集等)附加元数据(metadata)

这些元数据在编译后会被写入到程序集的metadata字段里,在运行时可以通过反射(System.Reflection)读取

系统内置的常见Attribute

| 名称 | 用途 | | [Obsolete] | 标记成员已废弃。编译器会警告或报错 | | [Serializable] | 标记类可以被序列化 | | [NoSerialized] | 标记字段在序列化时被跳过 | | [DllImport] | 用于调用非托管代码(P/Invoke)| | [Conditional] | 根据编译条件决定是否调用方法 | | [AttributeUsage] | 限定自定义Attribute的使用范围 |

[Obsolete]

告诉编译器“这个方法、类或属性已经过时”,鼓励使用新API

定义 (.NET Framework源码)

[AttributeUsage(AttributeTargets.All, Inherited = false)]
public sealed class ObsoleteAttribute : Attribute
{
    public string Message { get; }
    public bool IsError { get; }

    public ObsoleteAttribute(string message, bool error = false) { ... }
}

使用

public class Game
{
    [Obsolete("Use InitGame instead", true)] // 第二个参数为true会报错
    public void StartGame() { }

    public void InitGame() { }
}

调用

var g = new Game();
// g.StartGame(); // CS0619
  • 第一个参数是提示消息
  • 第二个参数true表示编译错误,false只警告
  • 这是编译器特性,不会影响运行时

用法场景:在API版本升级时非常常见,比如Unity的老API或.NET的旧接口

[Serializable]

告诉.NET序列化器(如BinaryFormatter、XmlSerializer)这个类的对象可以被序列化(即转成字节流保存或传输)

[Serializable]
public class Player
{
    public string Name;
    public int Level;

    [NonSerialized]
    public int TempScore;
}

序列化

var palyer = new Player { Name = "LJF", Level = 10, TempScore = 99 };

using (var fs new FileStream("player.dat", FileMode.Create))
{
    var formatter = new BinaryFormatter();
    formatter.Serialize(fs, player);
}

反序列化

using (var fs = new FileStream("player.dat", FileMode.Open))
{
    var formatter = new BinaryFormatter();
    var restore = (Player)formatter.Deserialize(fs);
    Console.WriteLine(rstored.Name); // 输出LJF
    Console.WriteLine(restored.TempScore); // 输出0(被跳过了)
}

[Serializable]告诉运行时在反射扫描时,将该类型字段纳入序列化流程

[NonSerialized]

配合[Serializable]使用,表示该字段在序列化时被忽略

[Serializable]
public class Player
{
    public string Name;
    [NonSerialized]
    public string Password;
}

Password在序列化时不会被写入数据流
常用于跳过缓存字段、临时数据或敏感信息
只对字段(field)生效,不作用于属性(property)

二进制序列化已经被废弃,取而代之的是Json序列化

[DllImport]

用于声明外部函数(通常是C/C++ 动态库函数),告诉CLR这是个“外部实现”的方法
定义(位于System.Runtime.InteropServices

[AttributeUsage(AttributeTargets.Method)]
public sealed class DllImportAttribute : Attribute
{
    public DllImportAttribute(string dllName) { ... }
    public CallingConvention CallingConvention { get; set; }
    public string EntryPoint { get; set; }
}

使用例子

using System.Runtime.InteropServices;

public class NativeMethods
{
    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    public static extern int MessageBox(IntPtr hWnd, string text, string caption, int type);
}

NativeMethods.MessageBox(IntPtr.Zero, "你好", "提示", 0)

这就是P/Invoke
C#会在运行时加载user32.dll并调用其中的MessageBox函数

用法场景: Unity、.NET Core、Mono都广泛使用它来桥接C/C++层,比如

  • 调用OpenGL/Vulkan/Win32 API
  • 调用C写的音频或图像库
  • 嵌入系统底层调用

[Conditional]

让某个方法在特定条件下才会被编译调用

using System.Diagnostics;

public class Logger
{
    [Conditional("DEBUG")] // VS在Debug模式下,会自动#define DEBUG,因此会生效
    public static void Log(string msg) => Console.WriteLine(msg);
}

使用

Logger.Log("Game started!");

编译

  • 如果定义了宏DEBUG(在项目属性或#define DEBUG),这行代码会保留
  • 否则整行会被编译器直接忽略(像从源代码中删掉一样)

注意

  • 仅影响调用,不影响方法定义
  • 只能修饰返回类型为void的方法
  • 可用多个条件,如[Conditional("DEBUG"), Conditional("TRACE")]

这在日志系统、调试开关、断言机制中非常有用

[AttributeUsage]

控制自定义Attriute的适用位置、可重复性、继承性

[AttributeUsage(
    AttributeTargets.Class | AttributeTargets.Method,
    AllowMultiple = true,
    Inherited = false)]
public class AuthorAttribute : Attribute
{
    public string Name { get; }
    public AuthorAttribute(string name) { Name = name; }
}
  • AtrributeTargets: 限定能贴在哪些元素上(类、方法、属性、字段、参数等)
  • AllowMultiple:是否能多次使用同一个特性
  • Inherited:是否能被子类继承

示例

[Author("ljf12825")]
[Author("system", AllowMultiple = true)]
public class GameEngine { }

反射读取

var attrs = typeof(GameEngine).GetCustomAttributes(typeof(AuthorAttribute), false);
foreach (AuthorAttribute attr in attrs)
    Console.WriteLine(attr.Name);

小结

这六种基本Attribute代表了Attribute的“六种哲学”

特性领域核心思想
[Obsolete]编译期提示可攻至版本演化
[Serializable]/[NonSerialized]运行时序列化数据状态的持久化控制
[DllImport]系统互操作扩展托管代码边界
[Conditional]条件编译动态编译控制
[AttributeUsage]自定义约束元编程的元规则

这些特性合起来体现了C#的一个哲学:“元数据驱动的行为控制”
它不是仅仅写死逻辑,而是把“信息”挂在结构上,再由编译器、运行时或框架去解释执行
这正式Unity、ASP.NET、Entity Framework、XUnit等框架的底层机制所在

自定义Attribute

自定义Attribute只需要继承自System.Attribute即可

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
    public string Name { get; }
    public int Version { get; set; }

    public AuthorAttribute(string name) => Name = name;
}

使用

[Author("ljf12825", Version = 1)]
public class GameEngine
{
    [Author("tester", Version = 2)]
    public void Innit() { }
}

读取Attribute(反射)

可以在运行时读取类或方法上的特性,并作出相应逻辑

Type type = typeof(GameEngine);
var attrs = type.GetCustomAttributes(typeof(AuthorAttriute), false)

foreach (AuthorAttribute attr in attrs)
{
    Console.WriteLine($"Author: {attr.Name}, Version: {attr.Version}");
}

也可以这样查方法

var method = type.GetMethod("Init");
var attr = (AuthorAttribute)method.GetCustomAttribute(typeof(AuthorAttribute));
Console.WriteLine($"Method Author: {attr.Name}");