02-C#.Net-反射-面试题
题目1:什么是反射?它有什么用?
答案
反射(Reflection)是 .NET 提供的一种机制,来自 System.Reflection 命名空间,允许程序在运行时动态地获取程序集(dll/exe)中的类型信息,并基于这些信息创建对象、调用方法、读写属性和字段。
主要用途:
- IOC 容器:动态加载程序集,在不修改代码的情况下切换实现
- MVC 框架:根据 URL 中的控制器名和方法名,反射创建实例并调用方法
- ORM 框架:通过反射实现对象与数据库的自动映射
- 突破访问权限:读写私有成员
- Emit:配合动态生成代码,实现动态代理、AOP
出题意图
考察候选人对反射基本概念的理解,以及是否有实际使用经验。
解答思路
先说"是什么",再说"能做什么",最好能举出 MVC、ORM、IOC 这类实际框架中的应用例子,体现深度。
题目2:反射加载程序集有哪几种方式?有什么区别?
答案
有三种方式:
// 1. LoadFrom:传 dll 文件名(含 .dll 后缀),从当前目录或指定路径加载
Assembly.LoadFrom("Business.DB.SqlServer.dll");
// 2. LoadFile:传完整物理路径(含 .dll 后缀),必须绝对路径
Assembly.LoadFile(@"C:\path\to\Business.DB.SqlServer.dll");
// 3. Load:传程序集名称(不含后缀),从 GAC 或应用程序目录查找
Assembly.Load("Business.DB.SqlServer");
主要区别:
LoadFrom最常用,支持相对路径,会自动解析依赖程序集LoadFile必须绝对路径,不会自动解析依赖,可能导致同一程序集被加载多次Load主要用于加载 GAC 中的程序集
出题意图
考察候选人对反射基础 API 的掌握程度,以及是否了解不同加载方式的适用场景。
解答思路
先分别说明三种方式的语法和参数要求,再对比核心区别,最后说明推荐使用场景。
题目3:反射创建对象时,为什么不能直接调用 object 类型变量上的方法?有哪些解决方案?
答案
C# 是强类型语言,编译时以变量声明的类型为准。Activator.CreateInstance 返回 object,编译器只知道它是 object,不知道它有业务方法,所以编译不通过。
解决方案有三种:
object oInstance = Activator.CreateInstance(type);
// 方案1:强制类型转换(不推荐,类型不匹配会抛异常)
SqlServerHelper helper = (SqlServerHelper)oInstance;
// 方案2:as 转换(推荐,类型不匹配返回 null 而不是抛异常)
IDBHelper helper = oInstance as IDBHelper;
helper.Query();
// 方案3:dynamic(绕过编译器检查,运行时决定类型)
dynamic dInstance = Activator.CreateInstance(type);
dInstance.Query(); // 运行时如果方法不存在才报错
实际开发中推荐方案2,依赖接口而不是具体类,符合依赖倒置原则。
出题意图
考察候选人对 C# 类型系统的理解,以及 as、强转、dynamic 三者的区别。
解答思路
核心是解释"编译时类型"的概念,再对比三种方案的优缺点。
题目4:如何使用反射调用私有方法和静态方法?
答案
调用私有方法:
// 关键:使用 BindingFlags.NonPublic | BindingFlags.Instance
MethodInfo privateMethod = type.GetMethod("Show4", BindingFlags.NonPublic | BindingFlags.Instance);
privateMethod.Invoke(oInstance, new object[] { "参数" });
调用静态方法:
MethodInfo staticMethod = type.GetMethod("Show5");
// 关键:第一个参数传 null,静态方法不需要实例
staticMethod.Invoke(null, new object[] { "参数" });
BindingFlags 常用组合:
BindingFlags.Public | BindingFlags.Instance // 公共实例方法
BindingFlags.NonPublic | BindingFlags.Instance // 私有实例方法
BindingFlags.Public | BindingFlags.Static // 公共静态方法
BindingFlags.NonPublic | BindingFlags.Static // 私有静态方法
反射能突破访问权限,是因为访问修饰符是编译器层面的约束,反射在运行时直接操作元数据,完全绕过编译器检查。
出题意图
考察候选人对 BindingFlags 的理解,以及是否知道反射可以突破访问权限限制。
解答思路
分别演示私有方法和静态方法的调用,重点说明 BindingFlags 的使用和 Invoke 方法的参数差异,再解释为什么反射能突破访问权限。
题目5:反射如何调用泛型方法?MakeGenericMethod 和 MakeGenericType 有什么区别?
答案
MakeGenericMethod:用于确定泛型方法的类型参数MakeGenericType:用于确定泛型类的类型参数
调用泛型方法:
Type type = assembly.GetType("Business.DB.SqlServer.GenericMethod");
object oInstance = Activator.CreateInstance(type);
MethodInfo show = type.GetMethod("Show");
// 先确定类型参数,再调用
MethodInfo genericShow = show.MakeGenericMethod(new Type[] { typeof(int), typeof(string), typeof(DateTime) });
genericShow.Invoke(oInstance, new object[] { 123, "张三", DateTime.Now });
创建泛型类实例:
// 注意:泛型类型名称后面要加 `n,n 是泛型参数个数
Type type = assembly.GetType("Business.DB.SqlServer.GenericClass`3");
Type genericType = type.MakeGenericType(new Type[] { typeof(int), typeof(string), typeof(DateTime) });
object oInstance = Activator.CreateInstance(genericType);
MethodInfo show = genericType.GetMethod("Show");
show.Invoke(oInstance, new object[] { 123, "张三", DateTime.Now });
类和方法都有泛型时,需要先 MakeGenericType 再 MakeGenericMethod,分别处理。
出题意图
考察候选人对泛型反射的掌握,这是反射中较难的部分,能体现候选人的技术深度。
解答思路
先区分两个方法的作用对象(方法 vs 类),再用代码说明,最后提到两者都有泛型时的处理顺序。
题目6:反射 + 配置文件 + 工厂模式是什么原理?它和 IOC 有什么关系?
答案
核心思路是:把"要创建哪个类"的信息从代码中抽离出来,放到配置文件里。程序运行时读取配置,通过反射动态创建对象,代码本身只依赖接口。
{
"ReflictionConfig": "Business.DB.MySql.MySqlHelper,Business.DB.MySql.dll"
}
public static IDBHelper CreateInstance()
{
string config = GetConfig("ReflictionConfig");
string typeName = config.Split(',')[0];
string dllName = config.Split(',')[1];
Assembly assembly = Assembly.LoadFrom(dllName);
Type type = assembly.GetType(typeName);
return Activator.CreateInstance(type) as IDBHelper;
}
这就是 IOC(控制反转)容器的雏形:
- 传统方式:调用方自己
new对象,控制权在调用方 - IOC 方式:对象的创建交给容器(工厂),调用方只声明需要什么接口,控制权反转给了容器
Autofac、Unity 等 .NET IOC 框架的底层都是这个原理。
出题意图
考察候选人对设计模式和框架原理的理解,能否把反射和架构设计联系起来。
解答思路
先解释反射 + 配置文件的机制,再引出依赖倒置原则,最后点出这是 IOC 容器的核心原理。
题目7:反射如何实现一个简单的 ORM 查询?
答案
ORM 的核心是:根据实体类的类型信息,自动生成 SQL 语句,并把查询结果自动映射回实体对象。
public T Find<T>(int id) where T : BaseModel
{
Type type = typeof(T);
// 1. 反射生成 SQL
string sql = $"Select {string.Join(',', type.GetProperties().Select(c => $"[{c.Name}]"))} from {type.Name} where id={id}";
// 2. 执行查询
object result = Activator.CreateInstance(type);
using (SqlConnection conn = new SqlConnection(connectionString))
{
conn.Open();
SqlDataReader reader = new SqlCommand(sql, conn).ExecuteReader();
if (reader.Read())
{
// 3. 反射赋值
foreach (var prop in type.GetProperties())
{
prop.SetValue(result, reader[prop.Name] is DBNull ? null : reader[prop.Name]);
}
}
}
return (T)result;
}
调用:
SysUser user = sqlHelper.Find<SysUser>(1);
SysCompany company = sqlHelper.Find<SysCompany>(1);
约束:实体类属性名必须与数据库列名一致,实体类继承 BaseModel(保证有 Id 字段)。
出题意图
考察候选人能否将反射和泛型结合起来解决实际问题,这是 ORM 框架的核心原理,也是高级开发岗的必考点。
解答思路
分三步讲清楚:反射生成 SQL → 执行查询 → 反射赋值。重点说明泛型约束 where T : BaseModel 的作用。
题目8:什么是泛型缓存?它和普通字典缓存有什么区别?
答案
泛型缓存利用的是泛型类的静态成员对每个类型参数独立存在这一特性:
public class ConstantSqlString<T>
{
private static string FindSql = null;
// 每个 T 只执行一次(CLR 保证)
static ConstantSqlString()
{
Type type = typeof(T);
FindSql = $"Select {string.Join(',', type.GetProperties().Select(c => $"[{c.Name}]"))} from {type.Name} where id=";
}
public static string GetFindSql(int id) => $"{FindSql}{id}";
}
// ConstantSqlString<SysUser> 和 ConstantSqlString<SysCompany> 是两个不同的类
// 各自的静态构造函数只执行一次
与字典缓存的对比:
| 对比项 | 泛型缓存 | 字典缓存(Dictionary) |
|---|---|---|
| 线程安全 | 天然线程安全(CLR 保证静态构造函数只执行一次) | 需要手动加锁 |
| 访问速度 | 直接访问静态字段,极快 | 需要哈希查找,稍慢 |
| 适用场景 | 按类型缓存,类型在编译时已知 | 按任意 key 缓存,更灵活 |
出题意图
考察候选人对泛型原理的深度理解,以及是否有性能优化意识。
解答思路
核心是解释"泛型类的静态成员对每个类型参数独立"这个特性,再对比字典缓存的优缺点。
题目9:反射有性能问题吗?如何优化?
答案
有性能问题,主要损耗在动态加载 dll(Assembly.LoadFrom)和获取类型(assembly.GetType)这两步。
实测数据(100 万次循环创建对象 + 调用方法):
| 方式 | 耗时 |
|---|---|
| 普通 new | ~17 毫秒 |
| 反射(每次循环都 LoadFrom) | ~6300 毫秒 |
| 反射(LoadFrom 缓存到循环外) | ~71 毫秒 |
优化方法1:缓存 Assembly 和 Type
// 只加载一次
Assembly assembly = Assembly.LoadFrom("Business.DB.SqlServer.dll");
Type dbHelperType = assembly.GetType("Business.DB.SqlServer.SqlServerHelper");
for (int i = 0; i < 1_000_000; i++)
{
object obj = Activator.CreateInstance(dbHelperType);
IDBHelper helper = (IDBHelper)obj;
helper.Query();
}
优化方法2:使用表达式树(Expression Tree)
将反射调用编译成委托,性能接近直接调用:
var parameter = Expression.Parameter(typeof(object), "instance");
var call = Expression.Call(Expression.Convert(parameter, type), methodInfo);
var compiled = Expression.Lambda<Action<object>>(call, parameter).Compile();
// 后续直接调用委托,性能极高
compiled(instance);
结论:反射经过缓存优化后,性能损耗是可以接受的,实际项目中可以放心使用。
出题意图
考察候选人是否真正用过反射,以及有没有性能意识。只会说"反射有性能问题"而不知道如何优化的候选人,说明没有实战经验。
解答思路
先承认有性能问题,再说清楚损耗在哪里,给出缓存优化方案,说明优化后的效果,加分项是提到表达式树。
题目10:反射能破坏单例模式吗?如何防止?
答案
能。反射可以通过传入 true 参数来访问私有构造函数,从而绕过单例的保护:
Assembly assembly = Assembly.LoadFrom("Business.DB.SqlServer.dll");
Type type = assembly.GetType("Business.DB.SqlServer.Singleton");
// 第二个参数 true 表示允许访问非公开构造函数
Singleton s1 = (Singleton)Activator.CreateInstance(type, true);
Singleton s2 = (Singleton)Activator.CreateInstance(type, true);
Console.WriteLine(object.ReferenceEquals(s1, s2)); // false,单例被破坏
防止方式:在私有构造函数中加入判断,如果实例已存在则抛出异常:
private Singleton()
{
if (_Singleton != null)
throw new InvalidOperationException("单例已存在,禁止重复创建");
}
出题意图
考察候选人对单例模式和反射能力边界的理解,这是设计模式 + 反射的综合考题。
解答思路
先说"能破坏"并给出代码,再说防御方案,体现对两个知识点都有深度理解。
题目11:什么是 Emit?它和反射有什么关系?什么场景下会用到?
答案
Emit(System.Reflection.Emit)是反射的进阶能力,允许在程序运行时动态生成 IL 代码,从而创建新的程序集、类型、方法等。
与反射的关系:
- 反射是"读":读取已有程序集的元数据并使用它
- Emit 是"写":在运行时动态生成新的程序集和代码
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName("DynamicAssemblyExample"), AssemblyBuilderAccess.RunAndCollect);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MyModule");
TypeBuilder typeBuilder = moduleBuilder.DefineType("MyClass", TypeAttributes.Public);
MethodBuilder method = typeBuilder.DefineMethod(
"SayHello", MethodAttributes.Public | MethodAttributes.Static, null, null);
ILGenerator il = method.GetILGenerator();
il.Emit(OpCodes.Ldstr, "Hello");
il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }));
il.Emit(OpCodes.Ret); // 每个方法最后必须有 Ret
应用场景:
- 动态代理(Castle、AspectCore 等 AOP 框架的底层)
- 高性能序列化(替代反射赋值,生成直接赋值的 IL 代码)
- ORM 框架的属性映射优化
出题意图
考察候选人对 .NET 底层机制的了解深度,Emit 是高级岗位的加分项。
解答思路
先区分 Emit 和反射的关系(读 vs 写),再说应用场景,不需要背 IL 指令,但要知道它的用途和使用流程。
题目12:typeof(T) 和 obj.GetType() 有什么区别?
答案
typeof(T):编译时操作,T 必须是已知的类型名称,返回该类型的Type对象obj.GetType():运行时操作,返回对象实际运行时类型的Type对象
object obj = new SqlServerHelper();
Type t1 = typeof(SqlServerHelper); // 编译时确定
Type t2 = obj.GetType(); // 运行时确定
Console.WriteLine(t1 == t2); // true
// 区别体现在多态场景
IDBHelper helper = new SqlServerHelper();
typeof(IDBHelper) // → IDBHelper(接口类型)
helper.GetType() // → SqlServerHelper(实际运行时类型)
在泛型方法中,typeof(T) 获取的是泛型参数的实际类型,这是 ORM 框架中常用的方式。
出题意图
考察候选人对 C# 类型系统和多态的理解,这是反射基础知识中容易混淆的点。
解答思路
核心是"编译时 vs 运行时",再用多态场景举例说明两者的差异。
题目13:反射在实际项目中有哪些应用场景?
答案
1. IOC/DI 容器:根据配置文件动态创建对象,实现依赖倒置,通过修改配置文件切换实现,无需重新编译。
2. ORM 框架:一个泛型方法支持所有实体类型的查询,如 Dapper、EF Core 底层都有反射的影子。
3. MVC 框架路由:URL http://localhost/Home/Index → 反射创建 HomeController 实例 → 调用 Index 方法。
4. 插件系统:动态加载插件 DLL,实现功能扩展,程序不需要停止。
5. 序列化/反序列化:JSON.NET(Newtonsoft.Json)、System.Text.Json 等都大量使用反射读写属性。
6. 单元测试:测试私有方法、访问私有字段。
出题意图
考察候选人是否真正理解反射的价值,以及在实际项目中的应用经验。这道题能区分理论派和实战派。
解答思路
列举常见应用场景,结合代码或框架名称说明,重点说明反射带来的灵活性和可扩展性。
本文来自博客园,作者:龙猫•ᴥ•,转载请注明原文链接:https://www.cnblogs.com/nullcodeworld/p/19733163

浙公网安备 33010602011771号