C# Advanced Tutorial 1-2-Reflection


主要内容概要

1 反射调用实例方法、静态方法、重载方法 选修:调用私有方法 调用泛型方法
2 反射字段和属性,分别获取值和设置值
3 反射的好处和局限

反射反射 程序员的快乐。
反射无处不在,MVC ASP.Net ORM IOC AOP几乎所有的框架都离不开反射。

反编译工具 ILSpy

  1. 反编译工具不是用的反射,是一个逆向工程
  2. IL (中间语言)也是一种面向对象的语言,C#语言的对比,不太好阅读和编写
    IL Language
    CompilingPrinciple
    编译过程如图,高级语言转换成中间语言,再到机器语言。
    metadata:元数据, 数据清单,只是描述了类中有什么. CLR会读metadata。

反射的过程

反射:Reflection, System.Reflection命名空间,是微软.Net Framework提供的一个帮助类库,可以读取并使用metadata

下面看一个具体的例子:

  1. 正常使用数据库查询的三部曲:
  • 添加引用
  • 创建对象
  • 调用方法

具体的示例代码如下:

IDBHelper dBHelper = new SqlServerHelper();
IDBHelper dBHelper = new MySqlHelper();
dBHelper.Query();

这种写法的主要问题是:如果MySqlHelper这个具体实现类发生变化,例如操作数据库使用SQL Server,就需要修改代码。
2. 再看一下反射的具体步骤:

  • 动态加载
  • 获取类型
  • 创建对象
  • 类型转换
  • 调用方法

代码实现如下:

// 1、动态加载
Assembly assembly = Assembly.Load("Ruanmou.DB.SqlServer");// dll名称  默认到当前目录下查找
// 2、获取类型
Type type = assembly.GetType("Ruanmou.DB.SqlServer.SqlServerHelper");
// 3、创建对象
object oDbHelper = Activator.CreateInstance(type);
// oDbHelper.Query();// 为什么不能直接用query  编译器就不认可  c# 是强类型语言,不能Query 
dynamic dDbHelper = Activator.CreateInstance(type);
dDbHelper.Query(); //dynamic 是一个动态类型,可以避开编译器的检查,运行时检查  存在安全问题 
// 4、类型转换
IDBHelper iDBHelper = oDbHelper as IDBHelper; //不报错 类型不对就返回null
//5、调用方法
iDBHelper.Query();

其中,动态加载呢又有好几种方法,如下:

Assembly assembly = Assembly.Load("Ruanmou.DB.SqlServer");// dll名称  默认到当前目录下查找
Assembly assembly1 = Assembly.LoadFile(@"D:\软谋教育\Ruanmou\Advanced13Encrypt\20191010Advanced13Course3Reflection\20191010Advanced13Course3Reflection\MyReflection\MyReflection\bin\Debug\Ruanmou.DB.SqlServer.dll");// 全名称= 全路径+dll名称 + 后缀
Assembly assembly2 = Assembly.LoadFrom("Ruanmou.DB.SqlServer.dll");// 全名称
Assembly assembly3 = Assembly.LoadFrom(@"D:\软谋教育\Ruanmou\Advanced13Encrypt\20191010Advanced13Course3Reflection\20191010Advanced13Course3Reflection\MyReflection\MyReflection\bin\Debug\Ruanmou.DB.SqlServer.dll");// 全名称

当我们获取到类型type之后,就可以遍历去获取metadata了。

foreach (Type type in assembly.GetTypes())
{
  Console.WriteLine(type.Name);
  foreach (MethodInfo method in type.GetMethods())
  {
    Console.WriteLine(method.Name);
  }
}
  1. 封装
    至此,我们可以封装一下反射的这个过程,通过 Factory + config 代码可以不用编译发布。
//封装
public class SimlpFactory
{ 
  private static string IDBHelperConfig = ConfigurationManager.AppSettings["IDBHelperConfig"]; 
  private static string DllName = IDBHelperConfig.Split(',')[1];
  private static string TypeName = IDBHelperConfig.Split(',')[0]; 
  public static IDBHelper CreateInstentce()
  {
    Assembly assembly = Assembly.Load(DllName);
    Type type = assembly.GetType(TypeName);
    object oDbHelper = Activator.CreateInstance(type);
    return oDbHelper as IDBHelper;
  }
}

//调用
IDBHelper iDBHelper = SimlpFactory.CreateInstentce();
iDBHelper.Query(); 

此时,如果需要换成别的数据库实现,我们只需要修改配置文件就可以了,不需要修改代码:

  <appSettings>
    <add key="IDBHelperConfig" value="Ruanmou.DB.Orcale.OrcaleHelper,Ruanmou.DB.Orcale"/>
  </appSettings>

这个封装的过程体现了程序的两大特性:

  • 程序的可配置
    实现类必须是事先已有的,而且在目录下面
    没有写死类型,而是通过配置文件执行,反射创建的

  • 程序的可扩展
    完全不修改原有代码,只是增加新的实现,copy,修改配置文件,就可以支持新功能
    反射的动态加载和动态创建对象,以及配置文件的结合

  1. 反射的多构造函数用法
    对于有多个构造函数的类,可以在创建实例的时候,指定使用哪个构造函数,如下:
object oDbHelper = Activator.CreateInstance(type);//默认无参构造函数
object oDbHelper1 = Activator.CreateInstance(type, new object[] { "杰克" });
object oDbHelper2 = Activator.CreateInstance(type, new object[] { "我是谁" });
object oDbHelper3 = Activator.CreateInstance(type, new object[] { 123 });//参数是int的构造函数

可以获取不同的构造函数,构造函数里面的参数等。

foreach (ConstructorInfo item in type.GetConstructors())
{
  Console.WriteLine(item.Name);
  foreach (var parameter in item.GetParameters())
  {
    Console.WriteLine(parameter.ParameterType);
  }
}

反射破坏单例

单例模式:类,能保证在整个进程中只有一个实例。
怎么实现?
构造函数私有化,别人就无法调用。
对外公开一个静态方法,在这个方法里面去获得实例。

public sealed class Singleton
{
  private static Singleton _Singleton = null;//静态实例
  private Singleton()//构造函数私有化
  {
    Console.WriteLine("Singleton被构造");
  }

  static Singleton()//静态构造函数,第一次调用这个类之前就初始化,只初始化一次
  {
    _Singleton = new Singleton();
  }

  public static Singleton GetInstance()
  {
    return _Singleton;
  }
}

这样可以发现,不管怎么实例化,得到的都是同一个实例化的类:

Singleton singleton = new Singleton(); 
Singleton singleton1 = Singleton.GetInstance();
Singleton singleton2 = Singleton.GetInstance();
Singleton singleton3 = Singleton.GetInstance();
Singleton singleton4 = Singleton.GetInstance();
Console.WriteLine(singleton1.Equals(singleton4));//得到的是相同的对象

再看下反射是怎么破坏单例的?

Assembly assembly = Assembly.Load("Ruanmou.DB.SqlServer");
Type type = assembly.GetType("Ruanmou.DB.SqlServer.Singleton");
//这个true 就表示 nonPublic,可以调用私有方法,这样就可以被多次实例化
object oSingleton1 = Activator.CreateInstance(type, true); //完全等价于 New Singleton
object oSingleton2 = Activator.CreateInstance(type, true);
object oSingleton3 = Activator.CreateInstance(type, true);
object oSingleton4 = Activator.CreateInstance(type, true);
Console.WriteLine(oSingleton1.Equals(oSingleton4));//完全

泛型在反射中的实例化

首先有一个泛型类,如下:

public class GenericClass<T, W, X>
{
  public void Show(T t, W w, X x)
  {
    Console.WriteLine("t.type={0},w.type={1},x.type={2}", t.GetType().Name, w.GetType().Name, x.GetType().Name);
  }
}

再看看这个泛型类在反射中怎么被调用?

//泛型编译之后生成展位符  `1   `2    `3
GenericMethod genericMethod = new GenericMethod();
genericMethod.Show<int, string, DateTime>(1234,"",DateTime.Now);

Assembly assembly = Assembly.Load("Ruanmou.DB.SqlServer");
Type typeNull = assembly.GetType("Ruanmou.DB.SqlServer.GenericClass");//这样找,type是null,要有占位符
Type type = assembly.GetType("Ruanmou.DB.SqlServer.GenericClass`3"); //获取一个泛型类型 
//实例化需要指定类型
Type type1 = type.MakeGenericType(new Type[] { typeof(int), typeof(string), typeof(DateTime) });
object oGenericTest = Activator.CreateInstance(type1);

反射中调用方法

首先在被反射的类中有如下这些方法:

public void Show1()
{
  Console.WriteLine("这里是{0}的Show1", this.GetType());
}

public void Show2(int id)
{
  Console.WriteLine("这里是{0}的Show2", this.GetType());
}

public void Show3(int id, string name)//重载方法1
{
  Console.WriteLine("这里是{0}的Show3", this.GetType());
}

public void Show3(string name, int id)//重载方法2
{
  Console.WriteLine("这里是{0}的Show3_2", this.GetType());
}

public void Show3(int id)//重载方法3
{
  Console.WriteLine("这里是{0}的Show3_3", this.GetType());
}

public void Show3(string name)//重载方法4
{
  Console.WriteLine("这里是{0}的Show3_4", this.GetType());
}

public void Show3()//重载方法5
{
  Console.WriteLine("这里是{0}的Show3_1", this.GetType());
}

private void Show4(string name)//私有方法
{
  Console.WriteLine("这里是{0}的Show4", this.GetType());
}

public static void Show5(string name)//静态方法
{
  Console.WriteLine("这里是{0}的Show5", typeof(ReflectionTest));
}

那么在反射中怎么去调用呢?
反射创建实例,根据方法名称,反射调用方法。
dll名称——类名称——方法名称,调用方法。
MVC就是靠这一招,调用Action。
例如

  1. http://localhost:9999/home/index 经过解析会调用HomeController类中Index方法。
    浏览器输入时只告诉了服务器类名称和方法名称,肯定是反射。MVC在启动时会先加载,扫描全部的dll,找到全部的Controller存起来,用controller来匹配,dll+类型名称。
    思考一下:MVC的局限性,如果Action重载,那么反射就无法区分方法。只能通过Httpget HttpPost等特性来区分。
  2. AOP 反射调用方法时可以在前后插入逻辑。
Assembly assembly = Assembly.Load("Ruanmou.DB.SqlServer");
Type type = assembly.GetType("Ruanmou.DB.SqlServer.ReflectionTest");
object oTest = Activator.CreateInstance(type);//获得实例

// 反射调用方法
MethodInfo show1 = type.GetMethod("Show1");
show1.Invoke(oTest, new object[0]); 

// 反射调用方法 ,需要参数的时候,参数类型必须和方法参数类型保持一致
MethodInfo show2 = type.GetMethod("Show2");
show2.Invoke(oTest, new object[] { 123 });

// 反射调用重载方法:
MethodInfo show3 = type.GetMethod("Show3", new Type[] { typeof(string), typeof(int) });
show3.Invoke(oTest, new object[] { "飞哥", 134 }); 

// 反射调用私有方法 
MethodInfo show4 = type.GetMethod("Show4", BindingFlags.NonPublic | BindingFlags.CreateInstance);
show4.Invoke(oTest, new object[] { "Tenk" });

// 反射调用静态方法: 对象的实例oTest  可以传入,也可以传入null
MethodInfo show5 = type.GetMethod("Show5");
show5.Invoke(oTest, new object[] { "彭于晏" });
show5.Invoke(null, new object[] { "Korey" });

反射中调用泛型方法:
首先有如下两个类:

public class GenericMethod
{
  public void Show<T, W, X>(T t, W w, X x)
  {
    Console.WriteLine("t.type={0},w.type={1},x.type={2}", t.GetType().Name, w.GetType().Name, x.GetType().Name);
  }
}

public class GenericClass<T, W, X>
{
  public void Show(T t, W w, X x)
  {
    Console.WriteLine("t.type={0},w.type={1},x.type={2}", t.GetType().Name, w.GetType().Name, x.GetType().Name);
  }
}

普通类中调用泛型方法:
泛型方法没有占位符,在泛型类,接口中才有占位符。

Assembly assembly = Assembly.Load("Ruanmou.DB.SqlServer");
Type type = assembly.GetType("Ruanmou.DB.SqlServer.GenericMethod");
object oTest = Activator.CreateInstance(type);
MethodInfo genericMethod = type.GetMethod("Show");
MethodInfo genericMethod1 = genericMethod.MakeGenericMethod(new Type[] { typeof(int), typeof(string), typeof(DateTime) });// 指定泛型方法的  具体参数 
genericMethod1.Invoke(oTest, new object[] { 123, "jun", DateTime.Now }); // 传入参数必须和声明的一样

泛型类中调用泛型方法:

Assembly assembly = Assembly.Load("Ruanmou.DB.SqlServer");
Type type = assembly.GetType("Ruanmou.DB.SqlServer.GenericClass`3"); //获取一个泛型类型                                                                                       
Type type1 = type.MakeGenericType(new Type[] { typeof(int), typeof(string), typeof(DateTime) });
object oGenericTest = Activator.CreateInstance(type1);
MethodInfo genericMethod = type1.GetMethod("Show");
genericMethod.Invoke(oGenericTest, new object[] { 123, "子玉", DateTime.Now });//要和T W X的顺序保持一致

反射中使用属性和字段

首先,有一个实体类People:

public class People
{
  public People()
  {
    Console.WriteLine("{0}被创建", this.GetType().FullName);
  }
  public int Id { get; set; }
  public string Name { get; set; }
  public string Description; 
}

正常使用的写法,这里不再赘述。
反射中使用这种实体类,首先没有必要去加载dll了,因为实体类变了的话,代码肯定是要重构的了。因此直接引用:

Type type = typeof(People);
object oPeople = Activator.CreateInstance(type);
//使用属性
foreach (PropertyInfo prop in type.GetProperties())
{
  if (prop.Name.Equals("Id"))
  {
    prop.SetValue(oPeople, 123);
  }
  else if (prop.Name.Equals("Name"))
  {
    prop.SetValue(oPeople, "风之林");//修改属性
  }
  Console.WriteLine($"oPeople.{prop.Name}={prop.GetValue(oPeople)}");//读取值
}
//使用字段
foreach (FieldInfo field in type.GetFields())
{
  if (field.Name.Equals("Description"))
  {
    field.SetValue(oPeople, ".Net learner");
  }
}
  • Get 反射展示是有意义的,反射遍历,可以不用改代码
  • Set 感觉没啥用,但是实际还是有用的,下面通过一个数据库操作实例来体会这个作用。

数据库操作实例中应用反射
首先有几个实体类:

public class BaseModel
{
  public int Id { set; get; }
}

public class Company : BaseModel
{
  public string Name { get; set; }
  public System.DateTime CreateTime { get; set; }
  public int CreatorId { get; set; }
  public int? LastModifierId { get; set; }
  public DateTime? LastModifyTime { get; set; }
}

public class User : BaseModel
{
  public string Name { set; get; }
  public string Account { set; get; }
  public string Password { set; get; }
  public string Email { set; get; }
  public string Mobile { set; get; }
  public int CompanyId { set; get; } 
}

然后在数据库帮助类中有:
这里只查询了Company,思考怎么用一个方法满足多个类型的需求?

public static Company Find(int id)
{
  string sql = @"SELECT   [Id]
         ,[Name]
         ,[CreateTime]
         ,[CreatorId]
         ,[LastModifierId]
         ,[LastModifyTime]
         FROM [SystemDB].[dbo].[Company] where Id=" + id;

  Type type = typeof(Company);
  object oCompany = Activator.CreateInstance(type);
  using (SqlConnection connection = new SqlConnection(Customers))
  {
    SqlCommand sqlCommand = new SqlCommand(sql, connection);
    connection.Open();
    SqlDataReader reader = sqlCommand.ExecuteReader();
    if (reader.Read()) //开始读取
    {
      foreach (var prop in type.GetProperties())
      {
        prop.SetValue(oCompany, reader[prop.Name]);
        //if (prop.Name.Equals("Id"))
        //{
        //    prop.SetValue(oCompany, reader[prop.Name]);
        //}
        //else if (prop.Name.Equals("Name"))
        //{
        //    prop.SetValue(oCompany, reader[prop.Name]);
        //} 
       }
      //Console.WriteLine(reader["Id"]);
      //Console.WriteLine(reader["Name"]); 
      //return new Company() { Id =(int)reader["Id"] };
      }
    }
  return (Company)oCompany;
}

下面写一个泛型方法来实现上面的Find功能,支持多种类型的查询:

public static T Find<T>(int id) where T : BaseModel
{
  Type type = typeof(T);
  object oObject = Activator.CreateInstance(type);//有可能是user 有可能是company
  //List<string> 
  var propNames = type.GetProperties().Select(a => $"[{a.Name}]");
  string props = string.Join(",", propNames);
  string sql = $"SELECT {props } FROM [{type.Name}] where Id={id}"; //生成sql语句
  using (SqlConnection connection = new SqlConnection(Customers))
  {
    SqlCommand sqlCommand = new SqlCommand(sql, connection);
    connection.Open();
    SqlDataReader reader = sqlCommand.ExecuteReader();
    if (reader.Read()) //开始读取
    {
      foreach (var prop in type.GetProperties())
      {
        prop.SetValue(oObject, reader[prop.Name]);
      }
    }
  }
  return (T)oObject;
}

这里有几个注意点:

  • $"SELECT {props } FROM [{type.Name}] where Id={id}这里面的[]可以避免因为关键字产生的错误
  • propNames 是一个list,然后用string.Join(",", propNames)拼接成一个字符串

反射的优缺点

优点 :反射是动态,支持良好的扩展性

  • 减少对象和对象之间的依赖,只需要知道类名(字符串)、方法名(字符串),就可以调用
  • 还可以突破特定权限,可以做到普通方式无法做到的

缺点

  • 使用麻烦,编写比较困难,代码量大,可以封装一下
  • 避开编译器检查,编写的时候容易出错
  • 性能问题 性能损耗大

下面用一个实例来验证一下性能:

public static void Show()
{
  Console.WriteLine("*******************Monitor*******************");
  long commonTime = 0;
  long reflectionTime = 0;
  {
    Stopwatch watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < 1_000_000; i++)
    {
      IDBHelper iDBHelper = new SqlServerHelper();
      iDBHelper.Query();
    }
    watch.Stop();
    commonTime = watch.ElapsedMilliseconds;
  }
  {
    Stopwatch watch = new Stopwatch();
    watch.Start();
    //缓存
    Assembly assembly = Assembly.Load("Ruanmou.DB.SqlServer");//1 动态加载
    Type dbHelperType = assembly.GetType("Ruanmou.DB.SqlServer.SqlServerHelper");//2 获取类型 
    for (int i = 0; i < 1_000_000; i++)
    {
      //Assembly assembly = Assembly.Load("Ruanmou.DB.SqlServer");//1 动态加载
      //Type dbHelperType = assembly.GetType("Ruanmou.DB.SqlServer.SqlServerHelper");//2 获取类型
      object oDBHelper = Activator.CreateInstance(dbHelperType);//3 创建对象
      IDBHelper dbHelper = (IDBHelper)oDBHelper;//4 接口强制转换
      dbHelper.Query();//5 方法调用
    }
    watch.Stop();
    reflectionTime = watch.ElapsedMilliseconds;
  }
  Console.WriteLine("commonTime={0} reflectionTime={1}", commonTime, reflectionTime);
  }
}

经过测试: 发现反射确实性能损耗比较大 普通方式:140, 反射:34860,确实让人无法接受,经过缓存优化之后发现,普通方式:65 反射:628,对于性能损耗,要理性看待,因为执行的数量巨大,中间的这些性能损耗是可以忽略不计。
缓存优化,把dll加载和类型获取,只执行一次。
MVC, Asp.Net ORM IOC AOP都在用反射,几乎都有缓存 !
MVC ORM启动很慢是因为完成了很多初始化,反射的那些东西。后面运行就会很快了。


文章作者: Chaoqiang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Chaoqiang !
评论
 上一篇
C# Advanced Tutorial 1-3-Generic-and-Reflection-Practice C# Advanced Tutorial 1-3-Generic-and-Reflection-Practice
document.querySelectorAll('.github-emoji') .forEach(el => { if (!el.dataset.src) { return
下一篇 
C# Advanced Tutorial 1-1 Generic C# Advanced Tutorial 1-1 Generic
主要内容概要 引入泛型:延迟声明 如何声明和使用泛型 泛型的好处和原理 泛型类、泛型方法、泛型接口、泛型委托 泛型约束 协变 逆变(选修) 泛型缓存(选修) 为什么要有泛型很常见的比如List, List可以用List来表示。List就是
  目录