CS4L20——反射

本章代码关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Type                        // 类型信息类
object.GetType() // 获取一个变量的类型信息
typeof() // 通过类名去获取类型信息
Type.GetType() // 通过字符串获取类型信息
type.GetMembers() // 通过类型信息获取类的所有成员信息
// 构造函数信息
type.GetConstructors() // 通过类型信息获取类的所有构造函数信息
type.GetConstructor() // 通过类型信息和参数列表信息,获取类的一个特定的构造函数信息
ConstructorInfo // 构造函数信息
constructorInfo.Invoke() // 通过构造函数信息实例化一个类对象,需要通过object数组传入参数
// 公开变量信息
type.GetFields() // 通过类型信息获取类的所有公开成员变量信息
type.GetField() // 通过类型信息和成员变量名字符串,获取类的一个特定的公开成员变量信息
FieldInfo // 公开成员变量信息
fieldInfo.GetValue() // 获取某个类对象的某个成员变量的值
fieldInfo.SetValue() // 设置某个类对象的某个成员变量的值
// 公开方法信息
type.GetMethods() // 通过类型信息获取类的所有公开方法信息
type.GetMethod() // 通过类型信息和公开方法名字符串(还可能需要参数列表信息),获取类的一个特定的公开方法信息
MethodInfo // 公开方法信息
methodInfo.Invoke() // 通过公开方法信息调用某个对象的公开方法,需要通过object数组传入参数
// 属性信息
type.GetProperties() // 通过类型信息获取类的所有属性信息
type.GetProperty() // 通过类型信息和属性名字符串,获取类的一个特定的属性信息
PropertyInfo // 属性信息
propertyInfo.GetValue() // 获取某个类对象的某个属性的值
propertyInfo.SetValue() // 设置某个类对象的某个属性的值
// 枚举相关
type.IsEnum // 类型信息对应类型是否是枚举
type.GetEnumNames() // 获取枚举类型信息的所有枚举名
type.GetEnumName() // 获取对象在枚举类型中的枚举名
// Activator 相关
Activator.CreateInstance()
// 程序集相关
Assembly //
Assembly.Load() //
Assembly.LoadFrom() //
Assembly.LoadFile() //
assembly.GetTypes() //
assembly.GetType() //

程序集

编译器

编译器是一种翻译程序,它用于将源语言程序翻译成目标语言程序

  • 源语言程序:某种程序设计语言写成的,比如 C#、C、C++、Java 等语言写的程序
  • 目标语言程序:二进制数标识的伪机器代码写的程序

C# 的 Roslyn 编译器就会将我们编写的 .cs​ 源代码文件编译为 .exe​ 或者 .dll​ 等可执行程序

程序集是经由编译器编译得到的,供进一步编译执行的那个中间产物
在 Windows 系统中,一般表现为后缀为 .dll​(库文件)或者是 .exe​(可执行文件)的格式
其中:

  • .dll​ 是动态库,它可以被其他程序调用其中的函数、类等等,我们也可以调用其中的库函数或者类型等等
  • .exe​ 是可执行文件,就是那些可以被双击点击启动的运行的应用程序,我们编写的代码最终就是被编译为 exe​ 再运行

简单来说:
程序集就是我们写的一个代码集合,我现在写的所有代码
最终都会被编译器翻译为一个程序集供别人使用
比如一个代码库文件(.dll​)或者一个可执行文件(.exe​)

image

元数据

元数据就是用来描述数据的数据,这个概念不仅仅用于程序上,在别的领域也有元数据

简单来说,程序中的类、类中的函数、变量等等信息就是 程序的元数据
有关程序以及类型的数据被称为 元数据,他们被保存在程序集中

也就是说,我们自定义的类,接口,结构体,函数等等,C# 自带的类型,都存储在元数据内部

反射

程序在运行时,可以查看其他程序集或者自身的元数据,一个运行的程序查看本身或者其他程序的元数据的行为就叫做反射

简单来说就是,在程序运行时,通过 反射 可以得到其他程序集或者自己程序集代码的各种信息
类 函数 变量 对象 等等,实例化它们,执行它们,操作它们

直观来说,通过反射,我们可以使用 字符串 去从元数据中获取我们想要的类,方法等等,然后调用他们
这个 字符串 可以是程序运行期间得到的,由外部传入的,而不一定是在源代码里写出来的,

之前我们调用一个类,一个方法,都是在源代码里写明调用的类和方法,
而通过反射,我们可以通过一个字符串变量去调用一个类或者方法

反射的作用

因为反射可以在程序编译后获得信息,所以它提高了程序的拓展性和灵活性:

  1. 程序在运行时得到的所有元数据,包括元数据的特性
  2. 程序运行时,实例化对象,操作对象
  3. 程序运行时创建新对象,用这些对象执行任务

例如,我们可以通过在配置文件里,例如 Json文件 等等,
写一个类的名字,然后读取这个配置文件,拿到类名的字符串,通过反射,我们就可以实例化甚至调用这个类
这样便不需要去源代码内自己写出类的调用,也可以调用一个类
又比如,我们可以在一个程序集内部,通过反射调用另一个程序集内的类,方法

以后学习了序列化和反序列化,可以加深对反射的作用的理解

注意!反射可以让我们通过更灵活的方式调用类和方法 ,但反射调用的性能一般是不如代码直接调用的
因此,除了必要的情况(例如程序运行时才能知道需要调用哪个类外部代码随时可能变化,性能不敏感的情况),最好少用反射调用外部的类和方法

Type

Type​(类的信息类),它是反射功能的基础!它是访问元数据的主要方式。
使用 Type​ 的成员获取有关类型声明的信息,有关类型的成员(如构造函数、方法、字段、属性和类的事件)

简单来说,Type​ 本身是一个类,它记录了某个类的所有成员信息,包括成员变量,函数,构造函数,属性,事件等等

接下来的示例以下面的类作为反射获取的目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
namespace ReflectionSample
{
class Test
{
private int i = 1;
public int j = 0;
public string str = "123";

public Test() {}

public Test(int i)
{
this.i = i;
}

public Test(int i, string str) : this(i)
{
this.str = str;
}

public void Speak()
{
Console.WriteLine("[无参重载]说话内容是:" + str);
}

public void Speak(string speakText)
{
Console.WriteLine("[有参重载]说话内容是:" + speakText);
}

public static int StaticTestFunc()
{
Console.WriteLine("静态方法调用测试");
return 999;
}

public int Number
{
get => i;
set => i = value;
}
}
}

获取 Type

通过 object 的 GetType() 获取 Type

因为它是 object​ 就有的方法,因此实际上所有的类和结构体都可以通过 GetType()​ 来获取其 Type​ 信息

1
2
3
int a = 42;
Type type = a.GetType();
Console.WriteLine(type);

输出:

1
System.Int32

该方法可以获取一个类对象真正的 Type​,即使这个对象是父类装子类的变量,也可以得到它真正的类型
因此,如果一个方法传入的是一个 object​ 类型的参数,我们也可以通过 GetType()​ 得到其真正的类型 Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void Main()
{
int a = 42;
string str = "aaa";
Test testObj = new Test();
OutputType(a);
OutputType(str);
OutputType(testObj);
}

static void OutputType(object obj)
{
// 我们并不能在这个函数内知道传入的obj到底是什么类的对象,但我们可以通过GetType()去获取,它会返回这个obj真正的类型信息
Type type = obj.GetType();
Console.WriteLine(type);
}

输出:

1
2
3
System.Int32
System.String
Test

通过 typeof 关键字传入类名获取 Type

这个方法相当于直接获取某个类的 Type​ 值,它可以存储到一个变量内,可以参与传参,也可以用于比较
而原本我们是不能直接将类名去存储或者传参的,因为类名不是一个值

1
2
Type type = typeof(int);
Console.WriteLine(type);

输出:

1
System.Int32

通过类名字符串去获取类型

注意!类名必须包含命名空间,不然找不到!

通过类名的字符串获取一个类的 Type​ 信息是反射中非常重要的功能之一,
通过这个功能,我们才可以在程序运行时通过外部传入的字符串来获取一个类的 Type
进而通过这个 Type​ 去调用对应的类的各种成员(变量,方法等),
这样调用一个类就不需要一定要在代码内写清楚调用语句,提高了程序的灵活性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
namespace TestSpace
{
class Test {}
}

class Program
{
static void Main()
{
GetTypeFromString("Int32");
GetTypeFromString("System.Int32");
GetTypeFromString("TestSpace.Test");
}

static void GetTypeFromString(string typeName)
{
Type? type = Type.GetType(typeName);
if (type != null)
{
Console.WriteLine("得到类型:" + type);
}
else
{
Console.WriteLine("没有从字符串内得到类型");
}
}
}

输出:

1
2
3
没有从字符串内得到类型
得到类型:System.Int32
得到类型:TestSpace.Test

关于不同的Type变量

如果多个 Type​ 变量存储的是同一个类的类型信息,那么这几个 Type​ 变量肯定是存储的同一个地址信息
因此,我们就可以通过 !=​,==​ 来判断两个 Type​ 是不是存储的同一个类的类型信息,
也可以判断一个变量的类型或者字符串是不是某一个类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Program
{
static void Main()
{
Type type = typeof(int);
Type? type2 = Type.GetType("System.Int32");
if (type == type2)
{
Console.WriteLine("两个Type相等");
}

int i = 9;
long l = 10;
CheckType(i);
CheckType(l);
}

static void CheckType(object obj)
{
Type type = obj.GetType();
if (type == typeof(int))
{
Console.WriteLine("传入的变量类型是int类型");
}
else
{
Console.WriteLine("传入的变量类型不是int类型");
}
}
}

输出:

1
2
3
两个Type相等
传入的变量类型是int类型
传入的变量类型不是int类型

得到类所在的程序集信息

可以通过 Type​ 可以得到类型所在的程序集信息,Assembly​ 就是程序集类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System.Reflection;

class Program
{
static void Main()
{
int a = 42;
Type type = a.GetType();
Console.WriteLine(type);
Type type2 = typeof(int);
Console.WriteLine(type2);
Type? type3 = Type.GetType("System.Int32");
Console.WriteLine(type3);

Assembly assembly = type.Assembly;
Console.WriteLine(assembly);
Console.WriteLine(type2.Assembly);
Console.WriteLine(type3?.Assembly);
}
}

输出:

1
2
3
4
5
6
System.Int32
System.Int32
System.Int32
System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e
System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e

获取类中的所有公共成员

我们可以通过 Type​ 获取该类的所有成员信息,成员信息的类型是 MemberInfo
type.GetMembers()​ 可以一次获取所有的成员信息数组

示例(下面的代码中用于反射的类已经在上文定义):

1
2
3
4
5
6
7
8
9
10
11
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
MemberInfo[] infos = type.GetMembers();
for (int i = 0; i < infos.Length; i++)
{
Console.WriteLine("ReflectionSample.Test 成员" + i + ": " + infos[i]);
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ReflectionSample.Test 成员0: Void Speak()
ReflectionSample.Test 成员1: Void Speak(System.String)
ReflectionSample.Test 成员2: Int32 StaticTestFunc()
ReflectionSample.Test 成员3: Int32 get_Number()
ReflectionSample.Test 成员4: Void set_Number(Int32)
ReflectionSample.Test 成员5: System.Type GetType()
ReflectionSample.Test 成员6: System.String ToString()
ReflectionSample.Test 成员7: Boolean Equals(System.Object)
ReflectionSample.Test 成员8: Int32 GetHashCode()
ReflectionSample.Test 成员9: Void .ctor()
ReflectionSample.Test 成员10: Void .ctor(Int32)
ReflectionSample.Test 成员11: Void .ctor(Int32, System.String)
ReflectionSample.Test 成员12: Int32 Number
ReflectionSample.Test 成员13: Int32 j
ReflectionSample.Test 成员14: System.String str

可见,Test​ 类的所有成员都被获取到了,包括成员变量、方法、属性,其中 .ctor​ 是构造方法
get_Number()​ 和 set_Number(Int32)​ 是属性 Number​ 编译后生成的两个方法,可见,属性本质上就是两个方法

公共构造函数信息

我们可以通过 Type​ 获取该类的所有构造函数,构造函数成员信息的类型是 ConstructorInfo
通过 ConstructorInfo​ 构造函数信息,我们可以反射调用其对应的构造方法,来实例化一个类

得到所有公共构造函数信息

type.GetConstructors()​ 可以一次获取所有的构造函数信息数组

示例(下面的代码中用于反射的类已经在上文定义):

1
2
3
4
5
6
7
8
9
10
11
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
ConstructorInfo[] infos = type.GetConstructors();
for (int i = 0; i < infos.Length; i++)
{
Console.WriteLine("ReflectionSample.Test 构造方法" + i + ": " + infos[i]);
}

输出:

1
2
3
ReflectionSample.Test 构造方法0: Void .ctor()
ReflectionSample.Test 构造方法1: Void .ctor(Int32)
ReflectionSample.Test 构造方法2: Void .ctor(Int32, System.String)

得到特定的公开构造函数信息

type.GetConstructor()​ 可以获取一个特定的构造函数,需要传入 Type[]​ 数组,
也就是说,获取某个特定的构造函数,需要传入参数列表的各个参数类型去获取,
例如,无参构造函数,就可以直接传入一个 Type[0]​,也就是没有参数,
又比如,对于参数列表为 (int i, string s)​ 的构造函数,就需要传入包括 Int32​ 和 String​ 的 Type​ 数组(顺序不允许错!)

1
2
3
4
5
6
7
8
9
10
11
// 通过字符串获取类型信息
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
// 调用无参构造函数
ConstructorInfo? info1 = type.GetConstructor(new Type[0]);
// 调用有参构造函数
ConstructorInfo? info2 = type.GetConstructor(new Type[] { typeof(int), typeof(string) });

通过公开构造函数信息实例化对象

得到 ConstructorInfo​ 后,使用 Invoke()​ 即可通过反射实例化类,
其中 Invoke()​ 需要传入 object[]​ 数组,数组内容就是要传递到构造函数的参数,如果是无参构造函数传入 null​ 即可
Invoke()​ 返回的是一个 object​ 对象,我们需要手动转换成对应的类型

示例(下面的代码中用于反射的类已经在上文定义):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 通过字符串获取类型信息
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
// 调用无参构造函数
ConstructorInfo? info1 = type.GetConstructor(new Type[0]);
if (info1 == null)
{
Console.WriteLine("ReflectionSample.Test 构造方法获取失败");
return;
}
object obj1 = info1.Invoke(null);
Console.WriteLine((obj1 as ReflectionSample.Test)?.Number);
// 调用有参构造函数
ConstructorInfo? info2 = type.GetConstructor(new Type[] { typeof(int), typeof(string) });
if (info2 == null)
{
Console.WriteLine("ReflectionSample.Test 构造方法获取失败");
return;
}
object obj2 = info2.Invoke(new object[] { 123, "通过反射调用有参构造函数" });
Console.WriteLine((obj2 as ReflectionSample.Test)?.Number);
Console.WriteLine((obj2 as ReflectionSample.Test)?.str);

输出:

1
2
3
1
123
通过反射调用有参构造函数

可以看到,我们通过反射调用构造函数,实例化了两个对象

公共成员变量信息

我们可以通过 Type​ 获取该类的公共成员变量,公共成员变量信息的类型是 FieldInfo
通过 FieldInfo​ 公共成员变量信息,我们可以反射调用某个对象的公共成员变量,对该对象的公共成员变量进行读取或赋值操作

FieldInfo​ 也可以获取枚举类型的枚举值,详见下文:反射获取枚举值

得到所有成员变量

type.GetFields()​ 可以一次获取所有的公共成员变量信息数组

示例(下面的代码中用于反射的类已经在上文定义):

1
2
3
4
5
6
7
8
9
10
11
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
FieldInfo[] infos = type.GetFields();
for (int i = 0; i < infos.Length; i++)
{
Console.WriteLine("ReflectionSample.Test 公开变量" + i + ": " + infos[i]);
}

输出:

1
2
ReflectionSample.Test 公开变量0: Int32 j
ReflectionSample.Test 公开变量1: System.String str

得到指定名称的成员变量

type.GetField()​ 可以获取特定的公共成员变量信息,我们需要传入这个成员变量的名字的字符串

示例(下面的代码中用于反射的类已经在上文定义):

1
2
3
4
5
6
7
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
FieldInfo? info = type.GetField("str");

通过反射获取对象的某个变量的值

得到 FieldInfo​ 后,使用 GetValue()​ 即可通过获取对象的成员变量的值
GetValue()​ 需要传入一个类对象(且这个类对象的 Type​ 必须包含此 FieldInfo​),
返回值就是传入的类对象中,FieldInfo​ 对应的那个成员变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
// 通过反射调用无参构造函数去构造一个对象
object? testObj = type.GetConstructor(new Type[0])?.Invoke(null);
FieldInfo? info = type.GetField("str");
if (testObj != null && info != null && testObj is ReflectionSample.Test)
{
((ReflectionSample.Test)testObj).str = "aaaa";
// 通过反射获取str这个成员变量值
Console.WriteLine(info.GetValue(testObj));
}

输出:

1
aaaa

通过反射设置对象的某个变量的值

得到 FieldInfo​ 后,使用 SetValue()​ 即可对类对象的成员变量赋值
SetValue()​ 需要传入一个类对象和要赋的值(且这个类对象的 Type​ 必须包含此 FieldInfo​),
SetValue()​ 就会将传入的类对象的 FieldInfo​ 对应的那个成员变量的值修改为传入的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
// 通过反射调用无参构造函数去构造一个对象
object? testObj = type.GetConstructor(new Type[0])?.Invoke(null);
FieldInfo? info = type.GetField("str");
if (testObj != null && info != null)
{
// 通过反射修改testObj的str成员变量值
info.SetValue(testObj, "通过反射修改的值!");
// 通过反射获取testObj的str成员变量值
Console.WriteLine(info.GetValue(testObj));
}

输出:

1
通过反射修改的值!

公共成员方法信息

我们可以通过 Type​ 获取该类的公共成员方法,公共成员方法信息的类型是 MethodInfo
通过 MethodInfo​ 公共成员方法信息,我们可以反射调用某个对象的公共成员方法

得到所有公开成员方法信息

type.GetMethods()​ 可以一次获取所有的公共成员变量信息数组

示例(下面的代码中用于反射的类已经在上文定义):

1
2
3
4
5
6
7
8
9
10
11
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
MethodInfo[] infos = type.GetMethods();
for (int i = 0; i < infos.Length; i++)
{
Console.WriteLine("ReflectionSample.Test 公开方法" + i + ": " + infos[i]);
}

输出:

1
2
3
4
5
6
7
8
9
ReflectionSample.Test 公开方法0: Void Speak()
ReflectionSample.Test 公开方法1: Void Speak(System.String)
ReflectionSample.Test 公开方法2: Void StaticTestFunc()
ReflectionSample.Test 公开方法3: Int32 get_Number()
ReflectionSample.Test 公开方法4: Void set_Number(Int32)
ReflectionSample.Test 公开方法5: System.Type GetType()
ReflectionSample.Test 公开方法6: System.String ToString()
ReflectionSample.Test 公开方法7: Boolean Equals(System.Object)
ReflectionSample.Test 公开方法8: Int32 GetHashCode()

值得一提的是,由属性生成的两个 get​ 和 set​ 方法也是包含在该类型的公开方法里的

得到特定的公开成员方法信息

type.GetMethod()​ 可以获取一个特定的公开成员方法,需要传入函数名,
如果函数存在重载,还需要传入参数列表的对应 Type[]​ 数组,根据参数类型列表以确定需要获取的是哪个函数重载
如果是无参方法,就可以直接传入一个 Type[0]​,也就是没有参数,
如果是有参方法,例如参数列表为 (int i, string s)​ 的方法,就需要传入包括 Int32​ 和 String​ 的 Type​ 数组(顺序不允许错!)

示例(下面的代码中用于反射的类已经在上文定义):

1
2
3
4
5
6
7
8
9
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
MethodInfo? info1 = type.GetMethod("StaticTestFunc");
MethodInfo? info2 = type.GetMethod("Speak", new Type[0]);
MethodInfo? info3 = type.GetMethod("Speak", new Type[1] { typeof(string) });

通过公开成员方法信息调用方法

得到 MethodInfo​ 后,使用 Invoke()​ 即可通过调用类对象的方法
Invoke()​ 需要 要调用方法的对象 和 object[]​ 数组,其中数组内容就是要传递到构造函数的参数
如果是静态方法,第一个参数传入 null​,如果是无参方法,第二个参数传入 null​ 即可
如果调用的方法有返回值,则会返回一个 object​ 对象,如果无返回值则返回 null

示例(下面的代码中用于反射的类已经在上文定义):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
MethodInfo? info1 = type.GetMethod("StaticTestFunc");
MethodInfo? info2 = type.GetMethod("Speak", new Type[0]); // 获取无参重载
MethodInfo? info3 = type.GetMethod("Speak", new Type[1] { typeof(string) }); // 获取有参重载
// 通过反射调用无参构造函数去构造一个对象
object? testObj = type.GetConstructor(new Type[0])?.Invoke(null);
if (testObj != null)
{
object? result = info1?.Invoke(null, null); // 调用静态有返回值方法
Console.WriteLine("result: " + (result == null ? "NULL" : result));
result = info2?.Invoke(testObj, null); // 调用无参无返回值方法
Console.WriteLine("result: " + (result == null ? "NULL" : result));
result = info3?.Invoke(testObj, new object[] { "通过反射调用的方法" }); // 调用有参无返回值方法
Console.WriteLine("result: " + (result == null ? "NULL" : result));
}

输出:

1
2
3
4
5
6
静态方法调用测试
999
[无参重载]说话内容是:123
NULL
[有参重载]说话内容是:通过反射调用的方法
NULL

属性信息

我们可以通过 Type​ 获取该类的属性,属性信息的类型是 PropertyInfo
通过 PropertyInfo​ 属性信息,我们可以反射调用某个对象的属性,对该对象的属性进行读取或赋值操作

以这个类为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace ReflectionSample
{
class PropertyTest
{
private int i = 1;
public string str = "123";
public int Number
{
get => i;
set => i = value;
}
public string Text
{
get
{
Console.WriteLine("读取属性");
return str;
}
set
{
str = value;
Console.WriteLine("写入属性");
}
}
}
}

得到所有成员属性

type.GetProperties()​ 可以一次获取所有的属性信息数组

1
2
3
4
5
6
7
8
9
10
11
Type? type = Type.GetType("ReflectionSample.PropertyTest");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
PropertyInfo[] infos = type.GetProperties();
for (int i = 0; i < infos.Length; i++)
{
Console.WriteLine("ReflectionSample.Test 属性" + i + ": " + infos[i]);
}

输出:

1
2
ReflectionSample.Test 属性0: Int32 Number
ReflectionSample.Test 属性1: System.String Text

得到指定名称的属性

type.GetProperties()​ 可以获取特定的属性信息,我们需要传入这个属性的名字的字符串

示例(下面的代码中用于反射的类已经在上文定义):

1
2
3
4
5
6
7
8
Type? type = Type.GetType("ReflectionSample.PropertyTest");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
PropertyInfo? info1 = type.GetProperty("Number");
PropertyInfo? info2 = type.GetProperty("Text");

通过反射获取对象的某个属性的值

得到 PropertyInfo​ 后,使用 GetValue()​ 即可通过获取对象的属性的值
GetValue()​ 需要传入一个类对象(且这个类对象的 Type​ 必须包含此 PropertyInfo​),
返回值就是传入的类对象中,PropertyInfo​ 对应的那个属性的值

1
2
3
4
5
6
7
8
9
10
11
12
13
Type? type = Type.GetType("ReflectionSample.PropertyTest");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
// 通过反射构造一个对象
object? testObj = type.GetConstructor(new Type[0])?.Invoke(null);
// 通过反射读取属性的值
PropertyInfo? info1 = type.GetProperty("Number");
Console.WriteLine(info1?.GetValue(testObj));
PropertyInfo? info2 = type.GetProperty("Text");
Console.WriteLine(info2?.GetValue(testObj));

输出:

1
2
3
1
读取属性
123

通过反射设置对象的某个变量的值

得到 PropertyInfo​ 后,使用 SetValue()​ 即可对类对象的属性赋值
SetValue()​ 需要传入一个类对象和要赋的值(且这个类对象的 Type​ 必须包含此 PropertyInfo​),
SetValue()​ 就会将传入的类对象的 PropertyInfo​ 对应的那个属性的值修改为传入的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Type? type = Type.GetType("ReflectionSample.PropertyTest");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
// 通过反射构造一个对象
object? testObj = type.GetConstructor(new Type[0])?.Invoke(null);
if (testObj != null)
{
PropertyInfo? info1 = type.GetProperty("Number");
info1?.SetValue(testObj, 9999999); // 通过反射赋值
Console.WriteLine(info1?.GetValue(testObj)); // 通过反射读取值
PropertyInfo? info2 = type.GetProperty("Text");
info2?.SetValue(testObj, "测试反射写入");
Console.WriteLine(info2?.GetValue(testObj));
}

输出:

1
2
3
4
9999999
写入属性
读取属性
测试反射写入

枚举相关

以这个枚举为例:

1
2
3
4
5
6
7
namespace ReflectionSample
{
enum EnumTest
{
A, B, C, D, E, F
}
}

判断类型是否是枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Type? type = Type.GetType("ReflectionSample.EnumTest");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
if (type.IsEnum)
{
Console.WriteLine("是枚举类型");
}
else
{
Console.WriteLine("不是枚举类型");
}

输出:

1
是枚举类型

获取所有枚举名

type.GetEnumNames()​ 可以一次该获取该类型的所有枚举名,如果此类型不是枚举类型,则报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Type? type = Type.GetType("ReflectionSample.EnumTest");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
if (type.IsEnum)
{
string[] enumNames = type.GetEnumNames();
for (int i = 0; i < enumNames.Length; i++)
{
Console.WriteLine("enum " + i + ": " + enumNames[i]);
}
}

输出:

1
2
3
4
5
6
enum 0: A
enum 1: B
enum 2: C
enum 3: D
enum 4: E
enum 5: F

获取对象的枚举名

type.GetEnumName()​ 可以用于获取枚举对象的名字,可用于得到枚举值但是不知道其名字的情况

1
2
3
4
5
6
7
8
9
10
11
12
Type? type = Type.GetType("ReflectionSample.EnumTest");
if (type == null)
{
Console.WriteLine("类型获取失败!");
return;
}
int enumValue = 1;
if (type.IsEnum)
{
string? name = type.GetEnumName(enumValue);
Console.Write(name != null ? ("Enum Name: " + name) : "Enum value is null");
}

输出:

1
Enum Name: B

反射获取枚举值

获取某个枚举的枚举值实际上和获取公开成员方法一样,都是使用 type.GetField()​ 来获取,
其中 type.GetField()​ 需要先传入枚举名,然后从获取到的 fieldInfo.GetValue(null)​ 来获取枚举值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Type? testEnum = Type.GetType("ReflectionSample.EnumTest");
if (testEnum == null)
{
Console.WriteLine("类型获取失败!");
return;
}
// 获取多个枚举信息值
FieldInfo[] enumValues = testEnum.GetFields();
for (int i = 0; i < enumValues.Length; i++)
{
Console.WriteLine(enumValues[i]);
}
// 通过枚举值名获取枚举值
FieldInfo? enumValueInfo = testEnum.GetField("A");
object? enumValue = enumValueInfo?.GetValue(null);
if (enumValue != null)
{
string? name = testEnum.GetEnumName(enumValue);
Console.WriteLine(name != null ? ("Enum Name: " + name) : "Enum value is null");
}

输出:

1
2
3
4
5
6
7
Int32 value__
ReflectionSample.EnumTest A
ReflectionSample.EnumTest B
ReflectionSample.EnumTest C
ReflectionSample.EnumTest D
ReflectionSample.EnumTest E
ReflectionSample.EnumTest F

其他

  • 获取事件信息:

    • 获取事件信息的方法:type.GetEvent()​,type.GetEvents()
    • 事件信息:EventInfo
  • 获取接口信息:

    • 获取实现接口的方法:type.GetInterface()​,type.GetInterfaces()
    • 该类实现的接口的信息:InterfaceInfo

Activator

用于快速实例化对象的类,用于将 Type​ 对象快捷实例化为对象,先得到 Type​,然后直接实例化一个对象
原来我们需要通过 Type​ 去获取构造函数信息,再通过函数构造信息去反射实例化对象,

接下来的示例以下面的类作为反射获取的目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
namespace ReflectionSample
{
class Test
{
private int i = 1;
public int j = 0;
public string str = "123";

public Test() {}

public Test(int i)
{
this.i = i;
}

public Test(int i, string str) : this(i)
{
this.str = str;
}

public void Speak()
{
Console.WriteLine("[无参重载]说话内容是:" + str);
}

public void Speak(string speakText)
{
Console.WriteLine("[有参重载]说话内容是:" + speakText);
}

public static int StaticTestFunc()
{
Console.WriteLine("静态方法调用测试");
return 999;
}

public int Number
{
get => i;
set => i = value;
}
}

class Test1 {};
class Test2 {};
interface Itest {};
}

Activator 实例化对象

使用 Activator.CreateInstance()​(返回值是 object​)可以直接从一个 Type​ 实例化对象
如果调用的是无参构造,则 Activator.CreateInstance()​ 只需要传入一个需要实例化对象的 Type​ 对象即可

1
2
3
4
5
6
7
8
9
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("获取类型失败!");
return;
}

ReflectionSample.Test? obj = Activator.CreateInstance(type) as ReflectionSample.Test;
Console.WriteLine(obj?.str);

输出:

1
123

如果调用的是有参构造,Activator.CreateInstance()​ 需要先传入一个需要实例化对象的 Type​ 对象,再在后续直接对照参数传入对象
Activator.CreateInstance()​ 参数列表是变长参数,因此可以接收足够多的传入到构造函数的参数)

注意!type​ 参数后边传入的参数必须和要 type​ 的构造函数的参数列表对应,如果后续传入的参数有缺失、多余,或者错误,都会直接导致报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Type? type = Type.GetType("ReflectionSample.Test");
if (type == null)
{
Console.WriteLine("获取类型失败!");
return;
}
// 无参构造
ReflectionSample.Test? obj = Activator.CreateInstance(type) as ReflectionSample.Test;
Console.WriteLine("1: " + obj?.str);
// 有参构造
ReflectionSample.Test? obj1 = Activator.CreateInstance(type, 99) as ReflectionSample.Test;
Console.WriteLine("2: " + obj1?.Number);
ReflectionSample.Test? obj2 = Activator.CreateInstance(type, 888, "ActivatorTest") as ReflectionSample.Test;
Console.WriteLine("3.Number: " + obj1?.Number);
Console.WriteLine("3.str: " + obj2?.str);

输出:

1
2
3
4
1: 123
2: 99
3.Number: 99
3.str: ActivatorTest

创建类库

类库,顾名思义是类的集合仓库,类库项目会编译出 dll​ 文件,它不会直接运行,而是被其他的项目调用其中的类与方法
我们可以将一些通用的类和方法作为一个模块放到类库工程内,然后被其他的类调用,这样,这些通用的类和方法就可以跨工程使用
将不同模块的代码划分到不同工程,也可以减少各个工程的编译时间,因为一个工程所需要编译的代码量减少了

Visual Studio 可以直接选择 类库 来创建类库工程

image

如果不使用 Visual Studio,也可以使用终端(cmd 或 powershell)去创建类库,在需要创建工程的路径下使用如下命令即可

1
dotnet new classlib

Assembly

程序集类,主要用来加载其它程序集,加载后才能用 Type​ 来使用其它程序集中的信息
如果想要使用不是自己程序集中的内容,需要先加载程序集,比如 .dll​ 文件(库文件)
简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类三种加载程序集的函数

  • 加载在同一文件下的其它程序集

    1
    Assembly asembly1 = Assembly.Load("程序集名称");
  • 加载不在同一文件下的其它程序集

    1
    2
    Assembly asembly2 = Assembly.LoadFrom("包含程序集清单的文件的名称或路径");
    Assembly asembly3 = Assembly.LoadFile("要加载的文件的完全限定路径");

假设,目前存在如下的类库,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
namespace GetAssemblyTest
{
class Test
{
private int i = 1;
public int j = 0;
public string str = "123";

public Test() {}

public Test(int i)
{
this.i = i;
}

public Test(int i, string str) : this(i)
{
this.str = str;
}

public void Speak()
{
Console.WriteLine("[无参重载]说话内容是:" + str);
}

public void Speak(string speakText)
{
Console.WriteLine("[有参重载]说话内容是:" + speakText);
}

public static int StaticTestFunc()
{
Console.WriteLine("静态方法调用测试");
return 999;
}

public int Number
{
get => i;
set => i = value;
}
}
}

这个类库工程处于另一个工程内,编译出来的 dll​ 文件位于:E:\CodeField\CSharpProjects\GetAssemblyTest\bin\Debug\net8.0
如果要在不直接引用这个工程的情况下调用上面的类,就可以通过 Assembly​ 这个类来读取程序集,进而获取其 Type​ ,再反射调用各种成员

获取程序集下所有的类型

使用 assembly.GetTypes()​ 可以获取一个程序集下的所有类型

1
2
3
4
5
6
Assembly assembly = Assembly.LoadFrom("E:/CodeField/CSharpProjects/GetAssemblyTest/bin/Debug/net8.0/GetAssemblyTest");
Type[] types = assembly.GetTypes();
for (int i = 0; i < types.Length; i++)
{
Console.WriteLine("type" + i + ": " + types[i]);
}

输出:

1
2
3
4
type0: GetAssemblyTest.Test
type1: GetAssemblyTest.Test1
type2: GetAssemblyTest.Test2
type3: GetAssemblyTest.Itest

获取程序集下的某一个类型

使用 assembly.GetType()​ 传入类名,获取程序集下类名对应的类型 Type
接下来就可以通过反射,去反射实例化,调用其中的各种成员方法等等
(注意!如果工程内没有引用外部的类库工程,我们不能直接写代码来调用外部类的方法,因为无法直接找到类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Assembly assembly = Assembly.LoadFrom("E:/CodeField/CSharpProjects/GetAssemblyTest/bin/Debug/net8.0/GetAssemblyTest");
Type? testType = assembly.GetType("GetAssemblyTest.Test");
if (testType == null)
{
Console.WriteLine("类型获取失败!");
return;
}

object? test = Activator.CreateInstance(testType);
// 调用反射得到的类的各种方法
MethodInfo? info1 = testType.GetMethod("StaticTestFunc");
MethodInfo? info2 = testType.GetMethod("Speak", new Type[0]); // 获取无参重载
MethodInfo? info3 = testType.GetMethod("Speak", new Type[1] { typeof(string) }); // 获取有参重载
if (test != null)
{
object? result = info1?.Invoke(null, null); // 调用静态有返回值方法
Console.WriteLine("result: " + (result == null ? "NULL" : result));
result = info2?.Invoke(test, null); // 调用无参无返回值方法
Console.WriteLine("result: " + (result == null ? "NULL" : result));
result = info3?.Invoke(test, new object[] { "通过反射调用的方法" }); // 调用有参无返回值方法
Console.WriteLine("result: " + (result == null ? "NULL" : result));
}

输出:

1
2
3
4
5
6
静态方法调用测试
result: 999
[无参重载]说话内容是:123
result: NULL
[有参重载]说话内容是:通过反射调用的方法
result: NULL