C#高级篇 反射和属性详解【代码之美系列】
🎀🎀🎀代码之美系列目录🎀🎀🎀
一、C# 命名规则规范
二、C# 代码约定规范
三、C# 参数类型约束
四、浅析 B/S 应用程序体系结构原则
五、浅析 C# Async 和 Await
六、浅析 ASP.NET Core SignalR 双工通信
七、浅析 ASP.NET Core 和 MongoDB 创建 Web API
八、浅析 ASP.NET Web UI 框架 Razor Pages/MVC/Web API/Blazor
九、如何使用 MiniProfiler WebAPI 分析工具
十、浅析 .NET Core 中各种 Filter
十一、C#.Net筑基-类型系统
十二、C#.Net 筑基-运算符
十三、C#.Net筑基-解密委托与事件
十四、C#.Net筑基-集合知识大全
十五、C#.Net筑基 - 常见类型
十六、C#.NET体系图文概述—2024最全总结
十七、C# 强大无匹的模式匹配,让代码更加优雅
十八、C# 中的记录类型简介
十九、C# 异步编程模型【代码之美系列】
二十、C#高级篇 反射和属性详解【代码之美系列】
文章目录
- 🎀🎀🎀代码之美系列目录🎀🎀🎀
- 一、属性
- 1.1 使用属性
- 1.2 属性参数
- 1.3 属性目标
- 1.4 属性的常见用途
- 1.5 反射概述
- 二、创建自定义特性
- 三、使用反射访问特性
- 四、如何使用特性 (C#) 创建 C/C++ 联合
- 五、泛型和特性
- 六、如何使用反射查询程序集的元数据 (LINQ)
- 七、泛型和反射
一、属性
特性 提供了一种将 元数据
或 声明性
信息与代码(程序集、类型、方法、属性等)相关联的强大方法。将属性与程序实体关联后,可以在 运行时
使用称为反射的技术查询该属性。
属性具有以下属性:
- 属性将元数据添加到程序中。元数据是有关程序中定义的类型的信息。所有 .NET 程序集都包含一组指定的元数据,用于描述程序集中定义的类型和类型成员。您可以添加自定义属性以指定所需的任何其他信息。
- 您可以将一个或多个属性应用于整个程序集、模块或较小的程序元素,例如类和属性。
Attributes
可以像methods
和properties
一样接受参数。- 您的程序可以使用反射检查自己的元数据或其他程序中的元数据。
反射 提供描述程序集、模块和类型的对象(类型 Type
)。您可以使用反射动态创建类型的实例,将类型绑定到现有对象,或者从现有对象获取类型并调用其方法或访问其字段和属性。如果您在代码中使用属性,则反射使您能够访问它们。
下面是一个使用 GetType()
方法(由基类中的所有类型继承)获取变量类型的反射的简单示例:Object
确保在
.cs
文件的顶部添加and
。using System;using System.Reflection;
// Using GetType to obtain type information:
int i = 42;
Type type = i.GetType();
Console.WriteLine(type);
输出为: .System.Int32
下面的示例使用反射来获取加载的程序集的全名。
// Using Reflection to get information of an Assembly:
Assembly info = typeof(int).Assembly;
Console.WriteLine(info);
C# 关键字 和 在中间语言 (IL) 中没有意义,也不用于反射 API。IL 中的相应术语是 Family 和 Assembly。若要使用反射标识方法,请使用
IsAssembly
属性。若要标识方法,请使用 IsFamilyOrAssembly。protectedinternalinternalprotected internal
1.1 使用属性
属性几乎可以放置在任何声明上,尽管特定属性可能会限制它有效的声明类型。在 C# 中,可以通过将用方括号 () 括起来的特性名称放在应用该特性的实体的声明上方来指定特性。[]
在此示例中,SerializableAttribute
属性用于将特定特征应用于类:
[Serializable]
public class SampleClass
{
// Objects of this type can be serialized.
}
具有 DllImportAttribute
属性的方法的声明类似于以下示例:
[System.Runtime.InteropServices.DllImport("user32.dll")]
extern static void SampleMethod();
可以在声明上放置多个属性,如下例所示:
void MethodA([In][Out] ref double x) { }
void MethodB([Out][In] ref double x) { }
void MethodC([In, Out] ref double x) { }
可以为给定实体多次指定某些属性。此类 multiuse
属性的一个示例是 ConditionalAttribute
:
[Conditional("DEBUG"), Conditional("TEST1")]
void TraceMethod()
{
// ...
}
按照约定,所有 属性名称 都以单词
“Attribute”
结尾,以便将它们与 .NET 库中的其他项区分开来。但是,在代码中使用属性时,无需指定属性后缀。例如,等效于 ,但 是 .NET 类库中属性的实际名称。[DllImport][DllImportAttribute]DllImportAttribute
1.2 属性参数
许多属性都有参数,这些参数可以是 positional、unnamed 或 named
。任何位置参数都必须按特定顺序指定,并且不能省略。命名参数是可选的,可以按任意顺序指定。首先指定位置参数。例如,这三个属性是等效的:
[DllImport("user32.dll")]
[DllImport("user32.dll", SetLastError=false, ExactSpelling=false)]
[DllImport("user32.dll", ExactSpelling=false, SetLastError=false)]
第一个参数(DLL
名称)是位置参数,始终排在最前面;其他的都被命名了。在这种情况下,两个命名参数都默认为 false
,因此可以省略它们。位置参数对应于属性构造函数的参数。命名参数或可选参数对应于属性的属性或字段。有关默认参数值的信息,请参阅单个属性的文档。
有关允许的参数类型的更多信息,请参见 C# 语言规范的 Attributes
部分
1.3 属性目标
属性的目标是属性应用到的实体。例如,特性可能应用于类、特定方法或整个程序集。默认情况下,属性适用于其后面的元素。但是,您也可以显式标识,例如,属性是应用于方法、参数还是返回值。
要显式标识属性目标,请使用以下语法:
[target : attribute-list]
下表显示了可能的值列表。target
您可以指定目标值,以将属性应用于为自动实现的属性创建的支持字段。field
下面的示例演示如何将属性应用于程序集和模块。有关更多信息,请参见 通用属性 (C#)。
using System;
using System.Reflection;
[assembly: AssemblyTitleAttribute("Production assembly 4")]
[module: CLSCompliant(true)]
下面的示例演示如何在 C#
中将属性应用于方法、方法参数和方法返回值。
// default: applies to method
[ValidatedContract]
int Method1() { return 0; }
// applies to method
[method: ValidatedContract]
int Method2() { return 0; }
// applies to parameter
int Method3([ValidatedContract] string contract) { return 0; }
// applies to return value
[return: ValidatedContract]
int Method4() { return 0; }
无论定义有效的目标是什么,都必须指定目标,即使目标定义为仅应用于返回值也是如此。换句话说,编译器不会使用信息来解决不明确的属性目标。有关更多信息,请参阅
AttributeUsage
。ValidatedContractreturnValidatedContractAttributeUsage
1.4 属性的常见用途
以下列表包括代码中 attribute
的一些常见用法:
- 在 Web 服务中使用该属性标记方法,以指示该方法应可通过
SOAP
协议调用。有关更多信息,请参阅 WebMethodAttribute。WebMethod
- 描述在与本机代码互操作时如何封送方法参数。有关详细信息,请参阅
MarshalAsAttribute
。 - 描述类、方法和接口的
COM
属性。 - 使用
DllImportAttribute
类调用非托管代码。 - 根据标题、版本、描述或商标来描述程序集。
- 描述要序列化的类的哪些成员以实现持久性。
- 描述如何在类成员和
XML
节点之间进行映射以进行XML
序列化。 - 描述方法的安全要求。
- 指定用于强制实施安全性的特征。
- 通过即时 (
JIT
) 编译器控制优化,使代码易于调试。 - 获取有关方法的调用方的信息。
1.5 反射概述
反射 在以下情况下非常有用:
- 当您必须访问程序元数据中的属性时。有关更多信息,请参阅检索存储在属性中的信息。
- 用于检查和实例化程序集中的类型。
- 用于在运行时构建新类型。使用
System.Reflection.Emit
中的类。 - 为了执行后期绑定,访问在运行时创建的类型的方法。请参阅动态加载和使用类型一文。
二、创建自定义特性
可通过定义特性类创建自己的自定义特性,特性类是直接或间接派生自 Attribute
的类,可快速轻松地识别元数据中的特性定义。 假设希望使用编写类型的程序员的姓名来标记该类型。 可能需要定义一个自定义 Author
特性类:
[System.AttributeUsage(System.AttributeTargets.Class |
System.AttributeTargets.Struct)
]
public class AuthorAttribute : System.Attribute
{
private string Name;
public double Version;
public AuthorAttribute(string name)
{
Name = name;
Version = 1.0;
}
}
类名 AuthorAttribute
是该特性的名称,即 Author 加上 Attribute
后缀。 由于该类派生自 System.Attribute
,因此它是一个自定义特性类。 构造函数的参数是自定义特性的位置参数。 在此示例中,name
是位置参数。 所有公共读写字段或属性都是命名参数。 在本例中,version
是唯一的命名参数。 请注意,使用 AttributeUsage
特性可使 Author 特性仅对类和 struct
声明有效。
可按如下方式使用这一新特性:
[Author("P. Ackerman", Version = 1.1)]
class SampleClass
{
// P. Ackerman's code goes here...
}
AttributeUsage
有一个命名参数 AllowMultiple
,通过此命名参数可一次或多次使用自定义特性。 下面的代码示例创建了一个多用特性。
[System.AttributeUsage(System.AttributeTargets.Class |
System.AttributeTargets.Struct,
AllowMultiple = true) // Multiuse attribute.
]
public class AuthorAttribute : System.Attribute
{
string Name;
public double Version;
public AuthorAttribute(string name)
{
Name = name;
// Default value.
Version = 1.0;
}
public string GetName() => Name;
}
在下面的代码示例中,某个类应用了同一类型的多个特性。
[Author("P. Ackerman"), Author("R. Koch", Version = 2.0)]
public class ThirdClass
{
// ...
}
三、使用反射访问特性
你可以定义自定义特性并将其放入源代码中这一事实,在没有检索该信息并对其进行操作的方法的情况下将没有任何价值。 通过使用反射,可以检索通过自定义特性定义的信息。 主要方法是 GetCustomAttributes
,它返回对象数组,这些对象在运行时等效于源代码特性。 此方法有许多重载版本。 有关详细信息,请参阅 Attribute
。
特性规范,例如:
[Author("P. Ackerman", Version = 1.1)]
class SampleClass { }
在概念上等效于以下代码:
var anonymousAuthorObject = new Author("P. Ackerman")
{
Version = 1.1
};
但是,在为特性查询 SampleClass
之前,代码将不会执行。 对 SampleClass
调用 GetCustomAttributes
会导致构造并初始化一个 Author 对象。 如果该类具有其他特性,则将以类似方式构造其他特性对象。 然后 GetCustomAttributes
会以数组形式返回 Author 对象和任何其他特性对象。 之后你便可以循环访问此数组,根据每个数组元素的类型确定所应用的特性,并从特性对象中提取信息。
下面是完整的示例。 定义自定义特性、将其应用于多个实体,并通过反射对其进行检索。
// Multiuse attribute.
[System.AttributeUsage(System.AttributeTargets.Class |
System.AttributeTargets.Struct,
AllowMultiple = true) // Multiuse attribute.
]
public class AuthorAttribute : System.Attribute
{
string Name;
public double Version;
public AuthorAttribute(string name)
{
Name = name;
// Default value.
Version = 1.0;
}
public string GetName() => Name;
}
// Class with the Author attribute.
[Author("P. Ackerman")]
public class FirstClass
{
// ...
}
// Class without the Author attribute.
public class SecondClass
{
// ...
}
// Class with multiple Author attributes.
[Author("P. Ackerman"), Author("R. Koch", Version = 2.0)]
public class ThirdClass
{
// ...
}
class TestAuthorAttribute
{
public static void Test()
{
PrintAuthorInfo(typeof(FirstClass));
PrintAuthorInfo(typeof(SecondClass));
PrintAuthorInfo(typeof(ThirdClass));
}
private static void PrintAuthorInfo(System.Type t)
{
System.Console.WriteLine($"Author information for {t}");
// Using reflection.
System.Attribute[] attrs = System.Attribute.GetCustomAttributes(t); // Reflection.
// Displaying output.
foreach (System.Attribute attr in attrs)
{
if (attr is AuthorAttribute a)
{
System.Console.WriteLine($" {a.GetName()}, version {a.Version:f}");
}
}
}
}
/* Output:
Author information for FirstClass
P. Ackerman, version 1.00
Author information for SecondClass
Author information for ThirdClass
R. Koch, version 2.00
P. Ackerman, version 1.00
*/
四、如何使用特性 (C#) 创建 C/C++ 联合
通过使用特性,可自定义结构在内存中的布局方式。 例如,可使用 StructLayout(LayoutKind.Explicit) 和 FieldOffset
特性在 C/C++ 中创建所谓的联合。
在此代码段中,TestUnion
的所有字段均从内存中的同一位置开始。
[System.Runtime.InteropServices.StructLayout(LayoutKind.Explicit)]
struct TestUnion
{
[System.Runtime.InteropServices.FieldOffset(0)]
public int i;
[System.Runtime.InteropServices.FieldOffset(0)]
public double d;
[System.Runtime.InteropServices.FieldOffset(0)]
public char c;
[System.Runtime.InteropServices.FieldOffset(0)]
public byte b;
}
以下代码是另一个示例,其中的字段从不同的显式设置位置开始。
[System.Runtime.InteropServices.StructLayout(LayoutKind.Explicit)]
struct TestExplicit
{
[System.Runtime.InteropServices.FieldOffset(0)]
public long lg;
[System.Runtime.InteropServices.FieldOffset(0)]
public int i1;
[System.Runtime.InteropServices.FieldOffset(4)]
public int i2;
[System.Runtime.InteropServices.FieldOffset(8)]
public double d;
[System.Runtime.InteropServices.FieldOffset(12)]
public char c;
[System.Runtime.InteropServices.FieldOffset(14)]
public byte b;
}
组合的两个整数字段 i1
和 i2
与 lg
共享相同的内存位置。 lg
使用前 8
个字节,或 i1
使用前 4
个字节且 i2
使用后 4
个字节。 使用平台调用时,这种对结构布局的控制很有用。
五、泛型和特性
特性可按与非泛型类型相同的方式应用到泛型类型。 但是,只能将特性应用于开放式泛型类型和封闭式构造泛型类型,而不能应用于部分构造泛型类型。 开放式泛型类型是未指定任何类型参数的类型,例如 Dictionary<TKey, TValue>
;封闭式构造泛型类型指定所有类型参数,例如 Dictionary<string, object>
。 部分构造泛型类型指定一些(而非全部)类型参数。 示例为 Dictionary<string, TValue>
。 未绑定泛型类型是省略类型参数的泛型类型,例如Dictionary<,>。
以下示例使用此自定义属性:
class CustomAttribute : Attribute
{
public object? info;
}
属性可以引用未绑定的泛型类型:
public class GenericClass1<T> { }
[CustomAttribute(info = typeof(GenericClass1<>))]
class ClassA { }
通过使用适当数量的逗号指定多个类型参数。 在此示例中,GenericClass2 具有两个类型参数:
public class GenericClass2<T, U> { }
[CustomAttribute(info = typeof(GenericClass2<,>))]
class ClassB { }
属性可引用封闭式构造泛型类型:
public class GenericClass3<T, U, V> { }
[CustomAttribute(info = typeof(GenericClass3<int, double, string>))]
class ClassC { }
引用泛型类型参数的特性导致一个编译时错误:
[CustomAttribute(info = typeof(GenericClass3<int, T, string>))] //Error CS0416
class ClassD<T> { }
从 C# 11 开始,泛型类型可以从 Attribute 继承:
public class CustomGenericAttribute<T> : Attribute { } //Requires C# 11
若要在运行时获取有关泛型类型或类型参数的信息,可使用 System.Reflection
方法。 有关详细信息,请参阅泛型和反射。
六、如何使用反射查询程序集的元数据 (LINQ)
使用 .NET 反射 API 检查 .NET 程序集中的元数据,并创建位于该程序集中的类型、类型成员和参数的集合。 因为这些集合支持泛型 IEnumerable<T>
接口,所以可以使用 LINQ 查询它们。
下面的示例演示了如何将 LINQ 与反射配合使用以检索有关与指定搜索条件匹配的方法的特定元数据。 在这种情况下,该查询在返回数组等可枚举类型的程序集中查找所有方法的名称。
Assembly assembly = Assembly.Load("System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e");
var pubTypesQuery = from type in assembly.GetTypes()
where type.IsPublic
from method in type.GetMethods()
where method.ReturnType.IsArray == true
|| (method.ReturnType.GetInterface(
typeof(System.Collections.Generic.IEnumerable<>).FullName!) != null
&& method.ReturnType.FullName != "System.String")
group method.ToString() by type.ToString();
foreach (var groupOfMethods in pubTypesQuery)
{
Console.WriteLine("Type: {0}", groupOfMethods.Key);
foreach (var method in groupOfMethods)
{
Console.WriteLine(" {0}", method);
}
}
该示例使用 Assembly.GetTypes
方法返回指定程序集中的类型的数组。 将应用 where
筛选器,以便仅返回公共类型。 对于每个公共类型,子查询使用从 Type.GetMethods
调用返回的 MethodInfo
数组生成。 筛选这些结果,以仅返回其返回类型为数组或实现 IEnumerable<T>
的其他类型的方法。 最后,通过使用类型名称作为键来对这些结果进行分组。
七、泛型和反射
因为公共语言运行时 (CLR) 能够在运行时访问泛型类型信息,所以可以使用反射获取关于泛型类型的信息,方法与用于非泛型类型的方法相同。 有关详细信息,请参阅运行时中的泛型。
System.Reflection.Emit
命名空间还包含支持泛型的新成员。 请参阅如何:使用反射发出定义泛型类型。
有关泛型反射中使用的术语的固定条件列表,请参阅 IsGenericType
属性注解:
- IsGenericType:如果类型是泛型,则返回
true
。 - GetGenericArguments:返回
Type
对象的数组,这些对象表示为构造类型提供的类型实参或泛型类型定义的类型形参。 - GetGenericTypeDefinition:返回当前构造类型的基础泛型类型定义。
- GetGenericParameterConstraints:返回表示当前泛型类型参数约束的
Type
对象的数组。 - ContainsGenericParameters:如果类型或任何其封闭类型或方法包含未提供特定类型的类型参数,则返回 true。
- GenericParameterAttributes:获取描述当前泛型类型参数的特殊约束的 * GenericParameterAttributes 标志组合。
- GenericParameterPosition:对于表示类型参数的
Type
对象,获取类型参数在声明其类型参数的泛型类型定义或泛型方法定义的类型参数列表中的位置。 - IsGenericParameter:获取一个值,该值指示当前
Type
是否表示泛型类型或方法定义中的类型参数。 - IsGenericTypeDefinition:获取一个值,该值指示当前
Type
是否表示可以用来构造其他泛型类型的泛型类型定义。 如果该类型表示泛型类型的定义,则返回true
。 - DeclaringMethod:返回定义当前泛型类型参数的泛型方法,如果类型参数未由泛型方法定义,则返回
null
。 - MakeGenericType:替代由当前泛型类型定义的类型参数组成的类型数组的元素,并返回表示结果构造类型的
Type
对象。
此外,MethodInfo 类的成员还为泛型方法启用运行时信息。 有关用于反射泛型方法的术语的固定条件列表,请参阅 IsGenericMethod
属性注解:
- IsGenericMethod:如果方法是泛型,则返回 true。
- GetGenericArguments:返回类型对象的数组,这些对象表示构造泛型方法的类型实参或泛型方法定义的类型形参。
- GetGenericMethodDefinition:返回当前构造方法的基础泛型方法定义。
- ContainsGenericParameters:如果方法或任何其封闭类型包含未提供特定类型的任何类型参数,则返回 true。
- IsGenericMethodDefinition:如果当前
MethodInfo
表示泛型方法的定义,则返回true
。 - MakeGenericMethod:用类型数组的元素替代当前泛型方法定义的类型参数,并返回表示结果构造方法的
MethodInfo
对象。