一、概念
- 反射(Reflection)在C#中是一种非常重要的特性,它为开发者提供了在运行时获取和操作关于类型、成员、属性、方法等的详细信息的能力。通过反射,开发者可以在程序运行期间动态地创建对象、调用方法、设置属性值以及进行其他多种操作,而不需要事先在代码中硬编码这些操作。
- C#中的反射API主要集中在System.Reflection命名空间下,该命名空间包含了多个用于反射的类和接口。
1.1常用的反射类型:
- Assembly(程序集):Assembly类用于表示一个程序集,可以加载和卸载程序集,以及获取程序集中的类型、资源等信息。
- Type(类型):Type类表示程序集中的任意类型,可以是类、接口、枚举、结构等。通过Type类可以获取类型的成员信息,如方法和属性等。
- MethodInfo(方法信息):MethodInfo类提供了关于特定方法的信息,包括方法的名称、参数、返回类型等,还可以调用该方法。
- PropertyInfo(属性信息):PropertyInfo类用于表示属性,可以获取或设置属性的值。
- FieldInfo(字段信息):FieldInfo类用于表示字段,可以获取或设置字段的值。
1.2常用场景
- 动态加载程序集:在运行时根据需要加载.dll或.exe文件,并访问其中的类型和成员。
- 晚期绑定和早期绑定:晚期绑定是指在程序运行时才确定要调用的方法或访问的属性,而早期绑定是在编译时就确定。反射可以实现晚期绑定。
- 对象序列化和反序列化:在序列化和反序列化过程中,反射可以动态地读取和写入对象的各个属性。
- 实现插件系统:通过反射可以加载和调用插件中的功能,而无需预先知道插件的具体实现细节。
- 单元测试和ORM(对象关系映射)技术:在单元测试中,反射可用于动态调用待测试的方法;在ORM技术中,反射则可用于动态访问和映射对象的属性至数据库表的列。
1.3程序集概念
- 程序集(Assembly)是.NET框架下代码编译的一个逻辑单元,用于组合相关的代码和类型,最终生成PE文件。
- 程序集是.NET应用程序的基本组成单元,它以可执行文件(.exe)或 动态链接库文件(.dll)的形式存在。一个程序集可以包含一个或多个模块,每个模块又可以包括多个类型(如类、接口、枚举等)。程序集不仅包含编译后的IL(中间语言)代码 ,还包含描述程序集自身的元数据、资源文件等信息。
- 在软件开发过程中,一个项目通常对应一个程序集。例如,在Visual Studio中,一个解决方案可以包含多个项目,每个项目编译后都会生成一个程序集(一般是一个.dll文件)。这些程序集可以被其他项目引用,从而实现功能的复用和模块化开发。
二、Type类
2.1、概念
Type类是C#反射中的一个核心类,用于表示所有.NET类型的公共基类。通过Type类,可以访问类型的元数据,获取类型的成员信息,并进行实例化等操作。以下将详细探讨Type类的各个方面:
定义:Type类位于System.Reflection命名空间下,它提供了很多方法来获取类型信息、创建实例、执行方法等。
2.2、功能
通过Type类,可以检查类型是否为泛型类型、接口、委托等。同时,还可以获取类型的属性、方法、事件等成员信息。
2.3、使用
- 获取Type实例
静态方法:可以使用Type的静态方法如GetType、GetTypes等来获取类型信息。例如,typeof(int)返回表示int类型的Type对象。
动态实例:通过对象实例调用GetType()方法获取其类型。例如,一个字符串实例myString.GetType()将返回String类型的Type对象。 - 获取类型信息
属性和方法:使用Type对象的GetProperties和GetMethods方法可以分别获取类型的所有属性和方法。这些返回的PropertyInfo和MethodInfo对象提供了更详细的成员信息。
基类和接口:可以通过Type对象的BaseType属性获取其基类的类型,IsClass属性判断是否为类,Implements接口方法检查实现的接口。 - 创建类型实例
使用Activator类:Activator类提供了基于Type创建实例的方法,如CreateInstance。这允许在运行时动态创建对象。
使用ConstructorInfo:通过Type对象的GetConstructors方法获取构造函数,然后使用ConstructorInfo对象的Invoke方法创建实例。 - 调用类型方法
MethodInfo对象:通过Type对象的GetMethods获取方法列表,然后用MethodInfo对象的Invoke方法动态调用这些方法。
参数处理:MethodInfo的Invoke方法需要参数数组。如果方法需要的参数类型与传递的参数类型不匹配,需要进行适当的类型转换。 - 类型的兼容性和转换
类型兼容检查:IsAssignableFrom方法可以用来检查一个类型是否可以从另一个类型派生。例如,typeof(object).IsAssignableFrom(typeof(string))将返回true。
类型转换:Type类提供了几个方法来检查和执行类型转换,如IsInstanceOfType方法和ChangeType方法。
2.4、代码实例
csharp
// See https://aka.ms/new-console-template for more information
//Console.WriteLine("Hello, World!");
//反射
//Type类实例化
using System.Reflection;
Type type = typeof(Person);
Console.WriteLine("--------------------获取属性信息------------------------------");
Console.WriteLine(type.ToString());
//01、获取类中属性信息GetProperties(),返回的是PropertyInfo[]类型数组
PropertyInfo[] propertyInfos=type.GetProperties();
foreach (var item in propertyInfos)
{
Console.WriteLine(item.Name);
Console.WriteLine(item.Attributes);//还有其他特性可以获取
}
Console.WriteLine("-----------------------获取字段信息---------------------------");
//02、获取所有字段,只能获取共有字段,属性的第二种定义方式也属于私有字段
FieldInfo[] fieldInfos=type.GetFields();
foreach (var field in fieldInfos) {
Console.WriteLine(field.Name);
Console.WriteLine(field.FieldType);
}
/*结果:
iD
System.Int32
name
System.String*/
Console.WriteLine("-----------------------获取类的信息---------------------------");
//直接使用type调用相关方法
Console.WriteLine(type.Name);
Console.WriteLine(type.Attributes);
Console.WriteLine("-----------------------获取构造方法的信息---------------------------");
ConstructorInfo[] methodInfo =type.GetConstructors();
foreach (var item in methodInfo)
{
Console.WriteLine(item);//返回的是构造方法的签名,返回值与参数
}
/*
Void.ctor()
Void.ctor(Int32, System.String)*/
Console.WriteLine("-----------------------获取方法的信息---------------------------");
MethodInfo[] info = type.GetMethods();
foreach (var item in info)
{
Console.WriteLine(item.Name);
}
/// <summary>
/// 定义反射测试类
/// </summary>
class Person{
//01、定义公有字段
public int iD;
public string name;
//02、定义私有字段
private string Sex;
//03、定义属性,属性有两种定义方式
//方式1
public int Age { get; set; }
//方式2
private string address;
//04无参构造
public Person()
{
Console.WriteLine("无参构造");
}
//05有参构造
public Person(int iD, string name)
{
this.iD = iD;
this.name = name;
}
public string Address {
get { return this.address; }
set { this.address = value; }
}
}
三、Assembly类
3.1概念及使用
Assembly类在C#反射中是用于操作程序集的一个重要工具,它能够加载、枚举、和操纵程序集。以下是对Assembly类的详细解析:
- 加载程序集
- 静态加载方法 :Assembly.Load方法可以加载一个程序集,通常用于加载同一目录下或全局程序集中的其它程序集[^1^]。例如,
Assembly assembly = Assembly.Load("AssemblyName");
。 - 动态加载方法 :Assembly.LoadFrom和Assembly.LoadFile方法可以指定文件路径来加载程序集。例如,
Assembly assembly = Assembly.LoadFrom("包含程序集清单的文件的名称或路径");
[^2^]。
- 静态加载方法 :Assembly.Load方法可以加载一个程序集,通常用于加载同一目录下或全局程序集中的其它程序集[^1^]。例如,
- 枚举程序集中的类型
- 获取类型信息 :通过Assembly实例的GetTypes方法可以得到程序集中定义的所有类型的Type对象数组[^3^]。例如,
Type[] types = assembly.GetTypes();
。 - 创建类型实例 :结合Activator.CreateInstance方法,可以用枚举到的Type动态创建类型的实例。例如,
object obj = Activator.CreateInstance(type);
[^4^]。
- 获取类型信息 :通过Assembly实例的GetTypes方法可以得到程序集中定义的所有类型的Type对象数组[^3^]。例如,
- 调用方法与访问属性
- 调用方法 :MethodInfo类可以用于获取方法和动态调用方法。例如,使用
MethodInfo method = type.GetMethod("MethodName");
来获取方法信息,然后使用method.Invoke(obj, parameters);
来调用它[^4^]。 - 访问属性 :PropertyInfo类可以用来获取属性信息和动态设置或获取属性值。例如,
PropertyInfo property = type.GetProperty("PropertyName");
用来获取属性信息,然后property.SetValue(obj, value, index);
用于设置属性值[^4^]。
- 调用方法 :MethodInfo类可以用于获取方法和动态调用方法。例如,使用
- 获取程序集信息
- 获取程序集名称和版本 :Assembly实例的GetName方法可以获取程序集的完整名称、版本等信息。例如,
AssemblyName name = assembly.GetName();
[^4^]。 - 获取程序集元数据 :可以使用Assembly实例的其他方法如GetCustomAttributes来检查程序集的特性和元数据[^4^]。
- 获取程序集名称和版本 :Assembly实例的GetName方法可以获取程序集的完整名称、版本等信息。例如,
- 操纵程序集
- 创建新实例 :除了加载已存在的程序集外,Assembly类还提供了CreateInstance方法用于创建程序集中特定类型的新实例[^3^]。例如,
object instance = assembly.CreateInstance("TypeName");
。 - 获取当前程序集 :Assembly.GetExecutingAssembly方法可以获取当前正在执行的程序集,这在需要访问当前程序集中的资源或类型时非常有用[^2^]。例如,
Assembly currentAssembly = Assembly.GetExecutingAssembly();
。
- 创建新实例 :除了加载已存在的程序集外,Assembly类还提供了CreateInstance方法用于创建程序集中特定类型的新实例[^3^]。例如,
3.2应用-Assembly拿到成员-Type操作成员
3.2.1、Assembly获取程序集中的类/接口
- VS中定义类库文件:demo49反射_数据,作为程序集文件
- 定义控制台应用程序:demo49反射_Assembly使用,通过反射 去获取指定程序集里面的程序 操作成员(注意:此文将并没有对程序集文件添加依赖,不可直接操作程序集文件中内容,需要使用反射相关技术获取程序集文件 )
- 通过LoadFile获取程序集文件:通过loadFile+程序集的绝对路径的方式 获取到了要反射的程序集
csharp
//路径不知道的情况下:如何获取路径
//获取当前路径的完全限定路径
Console.WriteLine("当前文件夹路径:" + Environment.CurrentDirectory);
//找到net7.0文件夹,自己手动进入找到.dll文件
Assembly ass= Assembly.LoadFile(@"E:\c#体系课\04c#高级\代码\demo49反射_数据\bin\Debug\net7.0\demo49反射_数据.dll");
- 通过Assembly对象 自己程序的GetType获取程序集中指定的类型 成员 GetType()+类型全名称 [命名空间.类型名字]
csharp
//通过GetTypes() 获取程序集中 所以的成员 获取到的是 pubic + internal
Type type = ass.GetType("demo49反射_数据.Person");//获取单个
Type[] types = ass.GetTypes();//获取所有
但是有时候我们并不希望获取内部internal成员
csharp
//只获取程序集中被public修饰的成员,不要internal成员 Exported:出口
Type[] types = ass.GetExportedTypes();
foreach (var item in types)
{
Console.WriteLine(item.Name);
}
3.2.2、Type获取类中方法
- 添加数据,对Person类中添加需要测试的属性,方法等数据
csharp
public class Person
{
//01、定义属性
public string Name { get; set; }
public int Age { get; set; }
public char Gender { get; set; }
//02、定义方法
public static void SayHello()
{
Console.WriteLine("我是静态方法");
}
public void Sayhi()
{
Console.WriteLine("我是实例方法");
}
public int Add(int n1, int n2) {
return n1 + n2;
}
public Person()
{
}
//03、有参构造
public Person(string name,int age,char gender)
{
this.Name = name;
this.Age = age;
this.Gender = gender;
}
public void Test() {
Console.WriteLine("我是无参方法");
}
public void Test(string name) {
Console.WriteLine(name);
}
public void Test(int n1,int n2)
{
Console.WriteLine(n1+n2);
}
}
- 获取类中方法
csharp
//01、Assembly操作
Assembly ass= Assembly.LoadFile(@"D:\C#_Project\05C#高级代码\demo49反射_数据\bin\Debug\net7.0\demo49反射_数据.dll");
//02、Type操作
Type type = ass.GetType("demo49反射_数据.Person");
//获取所有方法
MethodInfo[] methodInfos = type.GetMethods();
foreach (var item in methodInfos)
{
Console.WriteLine(item.Name);
}
添加限定条件,只要静态方法
csharp
//如果我们要用BindingFlags.Static这个枚举 需要再加上一个public 枚举进行配合
MethodInfo[] methodInfos = type.GetMethods(BindingFlags.Static | BindingFlags.Public);
foreach (var item in methodInfos)
{
Console.WriteLine(item.Name);
}
//如果只要其中一个方法就可以用下面这种方式
MethodInfo method = type.GetMethod("SayHello");
3.2.3、执行获取的方法
如果执行静态方法 注意:不可以像调用委托或者事件一样通过加括号直接调用
间接调用 通过 invoke调用
//第一个参数 在调用成员的是很好 是否需要传入对象
//1.静态方法:类.
//2.实例方法:对象.
//第二个参数 调用方法的时候是否需要传入参数 如果需要就传入参数 不需要传入null就可以了
csharp
method.Invoke(null,null);
3.2.4、Activator创建对象(方法一)
对象创建唯一途径就是构造方法被调用
csharp
//01、获取程序集
Assembly ass= Assembly.LoadFile(@"D:\C#_Project\05C#高级代码\demo49反射_数据\bin\Debug\net7.0\demo49反射_数据.dll");
//02、获取程序集中需要的类
Type type = ass.GetType("demo49反射_数据.Person");
//03、创建person对象实例 在反射中 一般有2中方式创建对象
//1.调用构造函数
//2.通过Activator 动态创建实例对象
//Activator的底层 就是调用无参构造函数
object person = Activator.CreateInstance(type);
Console.WriteLine(person.ToString());
//调用实例方法,参数1:实例对象
method.Invoke(person,null);
注意:SayHello是静态方法直接通过类就可获取
3.2.5、构造方法创建对象(方法二)
csharp
//LoadFile传入的路径是绝对路径 不是相对路径
//1.通过loadFile+程序集的绝对路径的方式 获取到了要反射的程序集,从文件中找,不要直接属性复制
Assembly ass= Assembly.LoadFile(@"D:\C#_Project\05C#高级代码\demo49反射_数据\bin\Debug\net7.0\demo49反射_数据.dll");
Type type = ass.GetType("demo49反射_数据.Person");
//获取单个构造函数
//参数:string name,int age,char gender
ConstructorInfo constructor= type.GetConstructor(new Type[] { typeof(string),typeof(int),typeof(char)});
object person= constructor.Invoke(new object[] {"张三",10,'男'});
//间接调用 person返回的就是你创建的对象
PropertyInfo property= type.GetProperty("Name");
//通过GetValue()调用这个方法可以获取属性的值
object propertys= property.GetValue(person);
Console.WriteLine(propertys);//输出:张三
3.2.6、获取有参有返回值方法
csharp
//01、获取程序集
Assembly ass= Assembly.LoadFile(@"D:\C#_Project\05C#高级代码\demo49反射_数据\bin\Debug\net7.0\demo49反射_数据.dll");
//02、通过Assembly对象 自己程序的GetType获取程序集中指定的类型 成员 GetType()+类型全名称 [命名空间.类型名字]
Type type = ass.GetType("demo49反射_数据.Person");
//3.1、创建类对象
object person = Activator.CreateInstance(type);
//3.2、通过Type数组 判断调用方法的参数个数和参数类型 依次告诉我们编译器我们想要哪个方法
MethodInfo method = type.GetMethod("Test", new Type[] { });
MethodInfo method1 = type.GetMethod("Test", new Type[] { typeof(string) });
MethodInfo method2= type.GetMethod("Test", new Type[] { typeof(int), typeof(int) });
//04、调用重载方法 编译器无法直接从是否传递的参数来潘顿我们调用的是哪个方法
Console.WriteLine(method.Name);
method.Invoke(person,new object[] { });
Console.WriteLine(method1.Name);
method1.Invoke(person,new object[] {10 });
Console.WriteLine(method2.Name);
method2.Invoke(person,new object[] { 10,20 });
四、反射常用API
- IsAssignableFrom:后面的值 能不能直接赋值给前面的类型
- IsSubclassOf :判断后面的类型 是否是前面的类型的父类 必须是严格的父子类继承关系 才会返回true
- IsInstanceOfType:判断的不是继承关系 而是单纯的判断 是否是某个类型的对象
- IsAbstrcat; 是否是抽象类 True
- //IsInterface:是否是接口
csharp
// See https://aka.ms/new-console-template for more information
//Console.WriteLine("Hello, World!");
//判断类之间的关系
//IsAssignableFrom 返回bool类型 后面的值 能不能直接赋值给前面的类型
// 判断继承 子类-----》父类
bool b1 = typeof(Person).IsAssignableFrom(typeof(Student));
Console.WriteLine(b1);
bool b2 = typeof(Student).IsAssignableFrom(typeof(Teacher));
Console.WriteLine(b2);
bool b3 = typeof(IinterFace).IsAssignableFrom(typeof(TestInterFace));
Console.WriteLine(b3);
Console.WriteLine("--------------------------------------------------");
//IsSubclassOf :判断后面的类型 是否是前面的类型的父类 必须是严格的父子类继承关系 才会返回true
bool b4 = typeof(Student).IsSubclassOf(typeof(Person));
Console.WriteLine(b4);
bool b5 = typeof(Teacher).IsSubclassOf(typeof(Person));
Console.WriteLine(b5);
//接口严格意义上来说 不是严格父子类的关系 二期实现的关系
bool b6 = typeof(TestInterFace).IsSubclassOf(typeof(IinterFace));
Console.WriteLine(b6);
//IsInstanceOfType
//判断的不是继承关系 而是单纯的判断 是否是某个类型的对象
bool b = typeof(Student).IsInstanceOfType(new Person());
Console.WriteLine(b1);
//IsAbstrcat; 是否是抽象类 True
//但凡不能创建对象的 都认识是抽象的
Console.WriteLine(typeof(IinterFace).IsAbstract);
Console.WriteLine(typeof(Stest).IsAbstract);
Console.WriteLine(typeof(Person).IsAbstract);
//IsInterface:是否是接口
Console.WriteLine(typeof(Person).IsInterface);
Console.WriteLine(typeof(IinterFace).IsInterface);
Console.WriteLine(typeof(Stest).IsInterface);
class Person
{
}
class Student : Person
{
}
class Teacher : Person
{
}
interface IinterFace
{
}
class TestInterFace : IinterFace
{
}
abstract class Test
{
}
static class Stest
{
}
/*
Console.WriteLine("a");*/
五、案例:使用反射开发记事本插件
插件时dll文件
5.1、记事本简单功能实现
csharp
private void 保存ToolStripMenuItem_Click(object sender, EventArgs e)
{
//保存对话框
saveFileDialog1 = new SaveFileDialog();
//当前选择的是ok的话
if (saveFileDialog1.ShowDialog()==DialogResult.OK)
{
string path = saveFileDialog1.FileName;
File.WriteAllText(path,textBox1.Text.Trim());
}
}
5.2、插件功能开发
5.2.1步骤一、记事本基础功能开发
csharp
private void 保存ToolStripMenuItem_Click(object sender, EventArgs e)
{
//保存对话框
saveFileDialog1 = new SaveFileDialog();
//当前选择的是ok的话
if (saveFileDialog1.ShowDialog()==DialogResult.OK)
{
string path = saveFileDialog1.FileName;
File.WriteAllText(path,textBox1.Text.Trim());
}
}
5.2.2步骤二、接口定义规范
添加引用,使得Form中的控件可以作为参数传递:引用的窗体类 system.windows.form
csharp
namespace NotePad_Interface
{
public interface DLLInterface
{
//规范: 规范程序员应该安装如何标准对我们的记事本程序进行二次开发
//写什么
//1.属性 用来定义 我们扩展功能的名称
//只能获取不能设置
string Name { get; }//.转大写 实现功能
//2.方法 用来定义插件扩展的功能 object 泛型 委托.....(都不对)
//必须要明确出来 Run方法的返回值和参数
//具体写啥 一定要理解我们的业务需求
//参数:1.首先拿到文本框 我们做修改
//步骤:1. 引用的窗体类 system.windows.form 拿到才可以把文本框 做参数传进来
void Run(TextBox textbox);
}
}
5.2.3步骤三、加载程序集
在Form窗体的Load事件中加载即可
csharp
private void Form1_Load(object sender, EventArgs e)
{
//1.允许我们的程序加载DLL程序集 进行功能的扩展
//2.当我们加载的时候 Debug / Plug -in-Componets目录下面的相关的DLL
//3.我们拿到了不同的开发入门扩展的DLL之后 我们应该如何去操作
//第一:DLL文件应该去写 第二:拿到DLL之后我们应该做什么
//当我们程序加载的时候 我们应该去读取Debug/Plugs-in-Components目录下的DLL
//全路径: 如果写全路径 当我们的程序发布后给了别人之后怎么办 他可能项目不会放在e盘
//相对路径 :获取
//Assembly.GetExecutingAssembly().Location获取当前运行程序的程序集
//MessageBox.Show(Assembly.GetExecutingAssembly().Location);
//获取我们的程序集目录GetDirectoryName
//MessageBox.Show(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
//1.拼接我们要加载的程序集的路径
string path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Plug-in-Componets");
//2.获取指定路径下 所以插件的全路径
string[] strPath= Directory.GetFiles(path);
//3.通过循环 获取每一个插件
foreach (var item in strPath)
{
Assembly ass= Assembly.LoadFile(item); //加载我们的程序集
//每一个程序集都有属性方法字段等一大堆东西 我们需要实现DLLInterface的
Type[] types= ass.GetExportedTypes();
//判断实现DLLInterface的
foreach (var item2 in types)
{
//两种情况 第一种 就是我们想要的 实现列接口的类的对象
//第二种:继续了接口的接口和抽象类
if (typeof(DLLInterface).IsAssignableFrom(item2)&& !item2.IsAbstract )
{
//我们拿到对象 创建对象 Activator 执行构造函数
//执行构造函数 比较麻烦 (如果必须执行 请在开发接口文档里面写好)
DLLInterface o =(DLLInterface) Activator.CreateInstance(item2);
//获取对象的Name属性 赋值 给窗体的MenuStrip控件对象
//toolStripItem 添加完成对象
ToolStripItem toolStripItem = menuStrip1.Items.Add(o.Name);
//给按钮注册应该单击事件
toolStripItem.Click += ToolStripItem_Click;
//创建插件对象 Tag :存放不重要的内容 id
toolStripItem.Tag = o;
}
}
}
}
主要代码解释:
csharp
string path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Plug-in-Componets");
-
Assembly.GetExecutingAssembly():这个方法用于获取当前正在执行的程序集(Assembly)。程序集是包含程序代码和元数据的集合,通常是一个DLL或EXE文件。
-
Location:这个属性返回程序集的文件路径。例如,如果程序集位于C:\MyApp\bin\Debug\MyApp.exe,那么.Location将返回"C:\MyApp\bin\Debug\MyApp.exe"。
-
Path.GetDirectoryName(...):这个方法接收一个文件路径作为参数,并返回该文件所在目录的路径。在这个例子中,它将返回"C:\MyApp\bin\Debug"。
找到插件功能类的dll文件并复制到主应用程序中