前言

       C#反射是一种强大而灵活的工具,最近写游戏业务使用到了反射,并且遇到一些问题,并借此机会总结分享一下。


反射的概念        

        反射是指在程序运行中,查看、操作其他程序集或者自身的元数据的各种信息(类、函数、变量、对象等)的行为。C#中的反射(Reflection)是一种强大的特性,允许你在运行时检查和操作程序集、类型和对象的信息,基本上,使用反射可以在代码运行时检查和操作指定的类及其成员。C#反射的原理主要基于元数据(与C#特性相似),即程序集中存储的有关类型、方法等的信息。


反射的使用

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


Type(类的信息类)

反射功能的基础,访问元数据的主要方式,使用Type的成员获取有关类型声明的信息

static void Main(string[] args)
{
    // 获取Type,Type在内存中只有一份,获取的都是同一个
	// 1.万物之父object中的`GetType()`
	int a = 21;
	Type type = a.GetType();
        
	// 2.通过`typeof()`传入类名
    Type type2 = typeof(int);
    
    // 3.通过类的名字,类名必须包含命名空间
    Type type3 = Type.GetType("System.Int32");
}

Activator(快速实例化对象的类)

用于将Type对象快捷实例化为对象

static void Main(string[] args)
{
    Type t = typeof(Test);
    // 无参构造
    Test test = Activator.CreateInstance(t) as Test;
    // 有参构造
    test = Activator.CreateInstance(t, 99) as Test;
}

Assembly(程序集类)

相关概念

主要用来加载其他程序集,加载后才能用Type来使用其他程序集中的信息,如果想要使用不是自己程序集中的内容,需要先加载程序集。

dll文件(库文件)可以简单的把库文件看成一种代码仓库

在C#中,程序集可以以DLL或EXE文件形式存储,并可以通过反射API在运行时加载和操作,其中包含了编译后的代码和元数据。程序集通常需要在运行时被加载,以便在应用程序中使用其中定义的类型和成员。

程序集在运行时的加载可以发生在两种不同的上下文中,分别是:

  1. 上下文中解析(Contextual Resolve):这是程序集加载的一种默认行为,其中.NET运行时会尝试根据程序集的引用和依赖关系来自动解析和加载程序集。当你在代码中引用一个程序集,并且它有其他依赖的程序集,运行时会自动查找和加载这些依赖的程序集,以便构建完整的程序集树。

  2. 特定路径加载(LoadFrom):LoadFrom方法允许你显式指定程序集的路径,而不依赖于运行时上下文进行解析。你可以使用Assembly.LoadFrom方法来加载程序集,提供程序集的完整文件路径作为参数。这种方式适用于需要加载特定位置的程序集,而不需要考虑运行时上下文中的解析。

加载方法
  1. Assembly.Load: 当使用Assembly.Load时,参数是程序集的显示名称(可以包含长名称、版本、文化和公共密钥令牌信息)。此方法将在应用程序域的上下文中查找和加载程序集。如果程序集已经被加载,则将返回现有的程序集引用。Load方法最适合在已知需要加载程序集的完整名称的情况下使用。

    Assembly assembly = Assembly.Load("System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
  2. Assembly.LoadFrom: 使用 Assembly.LoadFrom 时,参数是包含程序集文件的路径。此方法将从指定路径加载程序集,并在包含目录下解析其依赖项。注意,这可能会导致同一个程序集被多次加载,例如,当相同的程序集位于不同的目录时。LoadFrom适用于需要从特定路径加载程序集,而不必在上下文中解析的情况。

    Assembly assembly = Assembly.LoadFrom(@"C:\MyAssemblies\MyAssembly.dll");
  3. Assembly.LoadFileAssembly.LoadFileAssembly.LoadFrom 类似,因为它需要一个包含程序集文件的路径作为参数。然而,LoadFile 在加载程序集时不会将其添加到上下文中,也不会解析其依赖项。这意味着,如果程序集具有未解析的依赖项,可能会在运行时出现问题。LoadFile 在只需要检查程序集信息而无需实际执行代码的场景下非常有用。

    Assembly assembly = Assembly.LoadFile(@"C:\MyAssemblies\MyAssembly.dll");

然后就可以通过assembly1.GetType();传入带命名空间的类名获取程序集下的指定类的类信息


 如何使用反射

static void Main(string[] args)
{
    // using System.Reflection

	// 获取类的程序集 type.Assembly
    Console.WriteLime(type.Assembly);
    
    // 获取类中的成员 MemberInfo	
    Type t = typeof(Test);
    MemberInfo[] infos = t.GetMembers();
    MemberInfo info1 = t.GetMember("HelloWorld");
    
    // 获取类的公共构造函数并调用 ConstructorInfo GetConstructors GetConstructor
    // 1.获取所有公共构造函数
    ConstructorInfo[] ctors = t.GetConstructors();
    // 2.获取其中一个构造函数
    // 得到构造传入 Type数组 数组中的内容按顺序是参数类型
    // 执行构造传入 object数组 表示按顺序传入的参数l
    // 2-1.得到无参构造器
    ConstructorInfo info1 = t.GetConstructor(new Type[0]);
    // 执行无参构造器
    Test test = info1.Invoke(null) as Test;
    // 2-2.得到有参构造器
    ConstructorInfo info2 = t.GetConstructor(new Type[]{ typeof(int) });
    // 执行有参构造器
    test = info2.Invoke(new object[]{ 2 }) as Test;
    
    // 获取类的成员变量 FieldInfo
    // 1.得到所有成员变量 
    FieldInfo[] fieldInofs = t.GetFields();
    // 2.得到指定名称的成员变量 GetField
    FieldInfo infoj = t.GetField("j");
    // 3.通过反射获取和设置对象的值 GetValue SetValue
    infoj.GetValue(test);
    infoj.SetValue(test, 100);
    
    // 获得类的成员方法 MethodInfo
    Type strType = typeof(string);
    MethodInfo[] methods = strType.GetMethods();
    // 1.如果存在方法重载 用Type数组表示参数类型
    MethodInfo method = strType.GetMethod("SubString", new Type[]{typeof(int), typeof(int)});
    // 2.调用方法
    string str = "歪比巴卜";
    object[] parameters = new object[] { 1, 2 };
    string result = method.Invoke(str, parameters) as string;
    // 如果是静态方法,第一个参数传null
}

利用反射可以访问类的所有成员,包括私有成员。使用 System.Type 类的方法,如 GetMethods(), GetProperties(), GetFields() 等,可以获取公共和非公共(私有、受保护)成员。这些方法默认情况下返回公共成员。若要获取私有成员,需要提供合适的绑定标志(BindingFlags)。

BindingFlags 是一个枚举,而 fieldBindingFlags 可能是其中的一种组合,用于在反射操作中定义搜索条件。以下是 BindingFlags 枚举中一些常用的标志:

  1. Public:指定公共成员。
  2. NonPublic:指定非公共成员,包括私有成员。
  3. Instance:指定实例成员。
  4. Static:指定静态成员。
  5. FlattenHierarchy:在继承链中搜索成员。
  6. IgnoreCase:不区分大小写。
  7. DeclaredOnly:仅搜索指定类型的成员,而不搜索基类成员。

你可以使用这些标志的组合来精确定义反射操作,以获取符合特定条件的成员。如果要获取所有方法,包括私有方法,可以使用以下代码:

Type type = typeof(MyClass);
MethodInfo[] methods = type.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);

  反射在游戏开发中的运用场景

  1. 插件系统:C#反射允许你在运行时加载和调用外部程序集,以实现插件式架构。这使得你可以轻松地添加、更新和移除功能扩展,而无需重新编译应用。

  2. 自定义脚本和行为:在游戏开发中,你可以使用反射来动态加载脚本,允许玩家创建自定义行为或角色。这使得游戏更加可扩展,因为不需要重新编译游戏代码。

  3. 资源管理:反射可以用于动态加载和管理资源,特别是当你的游戏有大量不同类型的资源,但你希望在运行时根据需要加载它们。

  4. 编辑器扩展:Unity的编辑器可以受益于反射,以动态查找和调用自定义编辑器工具、窗口和插件。这使得创建自定义工具和面板更加容易。

  5. 模块化系统: 在一些大型游戏中,你可能会设计一个模块化的系统(任务系统、新手引导系统、关卡系统),其中各种功能由独立的模块提供。反射可以用于动态地加载和初始化这些模块。


注意事项

        尽管反射很强大并且给开发中带来了强大的灵活性和动态性,但是其仍存在一些潜在的问题和性能影响需要我们注意,特别是在游戏开发等性能敏感的领域。

  1. 性能开销:反射操作比直接调用代码更为耗时。由于反射需要遍历程序集下的内容并对其中的类和成员进行解析,所以会引入额外的性能开销,尤其是在频繁调用或内循环中。
  2. 编译时检查的缺失: 反射操作不会在编译时进行检查,而是在运行时。这意味着某些错误只能在程序实际运行时被发现,而不是在编译时。这可能增加调试和维护的难度。
  3. 安全性问题:反射可以访问和修改对象的私有成员,这可能违反了对象的封装性原则,同时反射允许在运行时动态地创建类型的实例、访问和修改对象的私有成员等,而这些操作绕过了编译时的类型检查。
  4. 兼容性问题:在Unity3D中使用IL2CPP打包可能会带来兼容性的问题,因为IL2CPP在编译时会对代码进行更改,导致游戏打包后运行出现类型匹配错误等问题

        总体而言,使用反射要权衡其带来的灵活性和动态性与引入的性能和可维护性问题。在大多数情况下,如果有其他更静态、类型安全的替代方案,最好首选这些替代方案。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐