特性
在CLR via C#
和Professional C#
书中都有提到,在C#
编译器对特性进行编译时,会将其写入程序集的元数据中,所以特性的第一个显而易见的作用就是对代码的说明,相当于代码注解。但是,特性的功能远不止如此,这也是特性的第二个功能,特性结合反射,能够在程序运行时,读取特性的元数据,从而影响程序的运行。
C#
中的特性也是一个Class
,在自定义一个特性时,需要指出以下几点:
- 能够应用此特性的元素类型,可以使用
AttributeTargets
枚举来定义; - 对于同一个元素,能否多次应用这个特性,用
AllowMultiple
来指定; - 当特性应用于
Class
或者Interface
时,特性能否由子类继承,用`Inherited``指定;
例如有一个自定义特性如下:
using System;
namespace AttributeAndReflection
{
[AttributeUsage(AttributeTargets.Class,
AllowMultiple = false, Inherited = false)]
public class FieldNameAttribute: Attribute
{
private string _name;
public string Name => _name;
public FieldNameAttribute(string name)
{
_name = name;
}
}
}
反射:特性
接下来,我们使用反射来解析使用了上述特性的一个类。首先,在Program
上使用了上述自定义的一个特性,创建了FieldNameAttribute
的一个实例,并用“ClassTest”来初始化_name
。然后在Main
函数中,通过反射获取了这个特性的实例,最后输出了实例中的Name
属性。
using System.Reflection;
namespace AttributeAndReflection
{
[FieldName("ClassTest")]
internal class Program
{
public static void Main(string[] args)
{
Type programType = typeof(Program);
FieldNameAttribute attribute= (FieldNameAttribute)programType.GetTypeInfo().GetCustomAttribute(typeof(FieldNameAttribute));
Console.WriteLine(attribute.Name);
Console.ReadLine();
}
}
}
反射:动态加载程序集
除了可以动态解析特性,反射还可以动态加载程序集,实例化对象,接下来给出一个这样的例子,例子包括两个程序集。
- CalculatorLib.dll,该程序集定义了Calculator类型;
- ClientApp.exe,该控制台应用程序动态使用Calculator的方法。
// Calculator类型定义
public class Calculator
{
public double Add(double x, double y) => x + y;
public double Subtract(double x, double y) => x - y;
}
Program
程序是一个命令行程序,程序会有一个启动参数,用来表示Calculator.dll
程序集所在的位置。
为了动态使用CalculatorLib.Calculator.Add
方法,ClientApp.Program.Main
方法中首先得加载CalculatorLib
程序集,其次创建出Calculator
实例,然后使用这个实例的calc.GetType().GetMethod()
方法获取Add
方法对应的MethodInfo
对象,从而调用其Invoke
方法,该方法的第一个参数是Add
方法所属的实例,第二个参数是一个object[]
数组,用来传递参数到Add
方法中,具体代码如下。
using Microsoft.CSharp.RuntimeBinder;
using System;
using System.Reflection;
namespace ClientApp
{
class Program
{
private const string CalculatorTypeName = "CalculatorLib.Calculator";
static void Main(string[] args)
{
if (args.Length != 1)
{
ShowUsage();
return;
}
UsingReflection(args[0]);
}
private static void ShowUsage()
{
Console.WriteLine($"Usage: {nameof(ClientApp)} path");
Console.WriteLine();
Console.WriteLine("Copy CalculatorLib.dll to an addin directory");
Console.WriteLine("and pass the absolute path of this directory when starting the application to load the library");
}
private static void UsingReflection(string addinPath)
{
double x = 3;
double y = 4;
object calc = GetCalculator(addinPath);
object result = calc.GetType().GetMethod("Add").Invoke(calc, new object[] {
x, y });
Console.WriteLine($"the result of {x} and {y} is {result}");
}
private static object GetCalculator(string addinPath)
{
Assembly assembly = Assembly.LoadFile(addinPath);
return assembly.CreateInstance(CalculatorTypeName);
}
}
}
dynamic:动态使用程序集
当然,C#
提供了一种更加简化的方式来动态加载一个程序集,使用其中定义的类型,这就是dynamic
关键字。通过dynamic
关键字定义的Calculator
实例,可以直接使用其方法,完成calc.Add(x, y)
的调用,这段程序在VS或者Rider编辑器中是没有智能提示的。dynamic
关键字定义的类型会跳过编译时的检查,因此需要在运行时处理其可能发生的错误,下面代码中使用了Calculator
中没有定义的Multiply
方法,在运行时,我们可以捕获到RuntimeBinderException
。
using Microsoft.CSharp.RuntimeBinder;
using System;
using System.Reflection;
namespace ClientApp
{
class Program
{
private const string CalculatorTypeName = "CalculatorLib.Calculator";
static void Main(string[] args)
{
if (args.Length != 1)
{
ShowUsage();
return;
}
UsingReflectionWithDynamic(args[0]);
}
private static void ShowUsage()
{
Console.WriteLine($"Usage: {nameof(ClientApp)} path");
Console.WriteLine();
Console.WriteLine("Copy CalculatorLib.dll to an addin directory");
Console.WriteLine("and pass the absolute path of this directory when starting the application to load the library");
}
private static void UsingReflectionWithDynamic(string addinPath)
{
double x = 3;
double y = 4;
dynamic calc = GetCalculator(addinPath);
double result = calc.Add(x, y);
Console.WriteLine($"the result of {x} and {y} is {result}");
try
{
result = calc.Multiply(x, y);
}
catch (RuntimeBinderException ex)
{
Console.WriteLine(ex);
}
}
private static object GetCalculator(string addinPath)
{
Assembly assembly = Assembly.LoadFile(addinPath);
return assembly.CreateInstance(CalculatorTypeName);
}
}
}