目录
引言
在C#编程的世界里,特性(Attribute)和反射(Reflection)是两个强大的概念,它们为开发者提供了极大的灵活性和编程的便捷性。特性允许我们在不修改程序逻辑的前提下,为代码添加额外的信息,而反射则提供了在运行时查看和操作程序元素的能力。本文将结合实际经验和提供的文档内容,深入浅出地探讨这两个概念,并展示如何在实际开发中应用它们。
特性(Attribute)
特性是一种特殊的修饰符,以方括号[]
包裹,附着在C#程序的各种元素(如类、方法、属性、字段等)之上,为它们添加额外的元数据。这些元数据不仅可以在编译时影响编译器的行为,更重要的是,它们能在运行时通过反射被查询和利用,为程序提供丰富的自描述信息和动态行为指导。例如,我们可能会在代码中标注“这个类是我负责编写的”或者“这个函数在过去的开发周期中曾经出现过性能问题”。这样的信息对于团队协作和代码维护都是非常有价值的。
特性和注释的区别
特性与注释虽然都用于提供额外信息,但存在本质区别:
- 注释:仅面向人类阅读,编译器编译时忽略,不影响程序行为。
- 特性:既是源代码一部分,也被编译进程序集元数据,可在运行时通过反射访问,直接影响程序行为。
创建和使用特性
特性的基本语法包括将特性片段放置在目标声明之前,用方括号[]
包裹特性名及其参数(如果有的话)。
// 无参数特性 [Serializable]
public class MyClass { // 类的实现 }
// 带参数特性 [MyAttribute("first", "second", "finally")]
public class MyClass { // 类的实现 }
一个声明可以同时应用多个特性,特性之间可以用逗号分隔,或者分别声明。这为代码提供了灵活的修饰方式。
//多个独立特性片段
[Serializable]
[MyAttribute("first", "second", "finally")]
public class MyClass { // 类的实现 }
// 特性通过逗号分隔
[MyAttribute("first", "second", "finally"), Serializable]
public class MyClass { // 类的实现 }
此外,.NET框架允许某些特性被多次应用到同一个程序元素上。例如,Conditional
特性就可以根据预定义的条件来控制方法的执行。
[Conditional("DEBUG"), Conditional("TEST1")]
public void TraceMethod() { // 方法实现 }
在这个例子中,TraceMethod
只有在预处理器指令DEBUG
或TEST1
被定义时才会被执行,这为条件性地执行代码提供了一种灵活的机制。
常用的预定义的特性
.NET框架为我们提供了一些预定义的特性,如AttributeUsage
、Conditional
和Obsolete
等。这些特性有着特定的用途和行为。
AttributeUsage
:定义了自定义特性的使用规则,比如它可以应用到哪些类型的声明上,是否可以多次应用等。Conditional
:用于条件编译,只有在定义了特定的预处理指令时,标记为Conditional
的方法才会被执行。Obsolete
:标记不再推荐使用的程序元素,可以提供替代方案,并指定是生成警告还是错误。
AttributeUsage
预定义特性 AttributeUsage 描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型。规定该特性的语法如下:
[AttributeUsage(
validon,
AllowMultiple=allowmultiple,
Inherited=inherited
)]
其中:
参数 validon 规定特性可被放置的语言元素。它是枚举器 AttributeTargets 的值的组合。默认值是 AttributeTargets.All。
参数 allowmultiple(可选的)为该特性的 AllowMultiple 属性(property)提供一个布尔值。如果为 true,则该特性是多用的。默认值是 false(单用的)。
参数 inherited(可选的)为该特性的 Inherited 属性(property)提供一个布尔值。如果为 true,则该特性可被派生类继承。默认值是 false(不被继承)。
例如:
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]
Conditional
这个预定义特性标记了一个条件方法,其执行依赖于指定的预处理标识符。
它会引起方法调用的条件编译,取决于指定的值,比如 Debug 或 Trace。例如,当调试代码时显示变量的值。
//#define CONDITINA
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
///Conditional
namespace _01_特性
{
internal class Program
{
static void Main(string[] args)
{
Test1();
Test2();
}
[Conditional("CONDITINA")]
public static void Test1()
{
Console.WriteLine("Test1");
}
public static void Test2()
{
Console.WriteLine("Test2");
}
}
}
Obsolete
这个预定义特性标记了不应被使用的程序实体。它可以让您通知编译器丢弃某个特定的目标元素。例如,当一个新方法被用在一个类中,但是您仍然想要保持类中的旧方法,您可以通过显示一个应该使用新方法,而不是旧方法的消息,来把它标记为 obsolete(过时的)。
规定该特性的语法如下:
[Obsolete(
message
)]
[Obsolete(
message,
iserror
)]
其中:
参数 message,是一个字符串,描述项目为什么过时以及该替代使用什么。
参数 iserror,是一个布尔值。如果该值为 true,编译器应把该项目的使用当作一个错误。默认值是 false(编译器生成一个警告)。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _02_特性
{
internal class Program
{
static void Main(string[] args)
{
OldMethod();
NewMethod();
}
[Obsolete("这个方法过时了,使用NewMethod来代替",true)]
public static void OldMethod()
{
Console.WriteLine("旧方法");
}
public static void NewMethod()
{
Console.WriteLine("新方法");
}
}
}
反射(Reflection)
反射是.NET提供的一个强大的库,它允许我们在运行时取得任何类型的信息,并能创建类型实例、调用方法、获取或设置属性等。
使用反射读取特性
下面的代码演示了如何使用反射来检索我们前面定义的DeveloperAttribute
。
public static void DisplayDeveloperInfo(Type type)
{
// 获取应用在类型上的DeveloperAttribute
DeveloperAttribute devAttr = (DeveloperAttribute)Attribute.GetCustomAttribute(type, typeof(DeveloperAttribute));
if (devAttr != null)
{
Console.WriteLine($"{type.Name} was developed by {devAttr.Name}, level: {devAttr.Level}");
}
}
public static void Main()
{
DisplayDeveloperInfo(typeof(MyAmazingClass));
}
当运行这段代码时,你将看到类MyAmazingClass
的元数据被打印出来,这是因为我们使用了反射来检索类装饰的特性信息。下面是另一个例子,更加详细的写法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _03_自定义特性
{
internal class Program
{
static void Main(string[] args)
{
//4.通过反射访问特性
//Type获取有关加载的程序集中其中定义的类型信息
//typeof用于获取类型的System.Type对象
Type type = typeof(Test);
//获取自定义属性
var someAttributes = type.GetCustomAttributes(typeof(Some), true);
var somethingAttributes = type.GetCustomAttributes(typeof(Something), true);
foreach (var a in someAttributes.Concat(somethingAttributes))
{
Console.WriteLine(((dynamic)a).Name + ((dynamic)a).Date);
}
}
//1.声明自定义特性
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
//2.构建自定义特性
public class Something : Attribute
{
public string Name { get; set; }
public string Date { get; set; }
public Something(string Name, string Date)
{
this.Name = Name;
this.Date = Date;
}
}
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
public class Some : Attribute
{
public string Name { get; set; }
public string Date { get; set; }
public Some(string Name, string Date)
{
this.Name = Name;
this.Date = Date;
}
}
//3.在目标程序元素上应用自定义特性
[Something("刘某", "2020-11-12")]
[Something("万某", "2018-01-12")]
[Some("周某", "2018-05-12")]
[Some("末某", "2018-03-12")]
class Test
{
}
}
}
更深入地使用反射
反射不仅可以读取元数据,它还能让我们实例化对象、调用方法或访问字段和属性。这使得C#成为一种非常动态的语言,可以在运行时做出许多编译时无法确定的决定。
让我们看看如何使用反射来调用一个方法。
public class ReflectionDemoClass
{
private int _x;
public ReflectionDemoClass(int x)
{
_x = x;
}
public int AddToX(int y)
{
return _x + y;
}
}
public static void Main()
{
// 创建ReflectionDemoClass的实例
Type demoType = typeof(ReflectionDemoClass);
object demoInstance = Activator.CreateInstance(demoType, new object[] { 10 });
// 调用AddToX方法
MethodInfo addMethod = demoType.GetMethod("AddToX");
int result = (int)addMethod.Invoke(demoInstance, new object[] { 5 });
Console.WriteLine(result); // 输出15
}
在这个例子中,我们��建了ReflectionDemoClass
的实例并调用了它的AddToX
方法,尽管我们在编译时并不知道这个类或它的方法。这给我们编写通用代码或框架提供了强大的灵活性。
反射的常见用途
- 单元测试: 反射通常用于单元测试框架中,来自动发现并运行测试方法。
- 动态加载: 通过反射,程序可以在运行时加载和使用外部库,即便这些库在编译时不可用。
- 依赖注入: 反射是实现依赖注入框架的关键,例如.NET Core中的依赖注入容器。
反射的优缺点
优点:
- 反射提高了程序的灵活性和扩展性。
- 降低耦合性,提高自适应能力。
- 它允许程序创建和控制任何类的对象,无需提前硬编码目标类。
缺点:
- 性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。
- 使用反射会模糊程序内部逻辑;程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因而会带来维护的问题,反射代码比相应的直接代码更复杂。
如何优雅的使用反射
反射非常强大但同时也可能增加出错的风险,因为它依赖于字符串(例如方法名),这可能会在运行时出现问题。我们可以通过多种方式来减少风险:
- 使用
nameof
运算符来获取变量、类型或方法的名称 - 捕获和处理
Reflection
可能抛出的异常。 - 使用接口或委托定义明确的契约,而不是直接使用反射调用方法。
总结
特性和反射,是C#语言中两个极其有用的功能。特性提供了一种注解代码的方法,允许我们在编译时和运行时访问这些注解。而反射,则提供了一种强大的工具,它让我们可以查询和调用代码的结构和行为,即使在编写代码的时候我们不知道那些结构的细节。
通过以上内容的学习,希望你已经对C#中的特性和反射有了基本的认识,欢迎你在实际项目中尝试使用这些强大的工具,来提升你的代码的灵活性和强大功能。
觉得本篇文章写的还不错可以点赞,收藏,关注。主页有21天速通C#教程欢迎订阅!!!