反射

1 篇文章 0 订阅

什么是反射

反射本质上是.Net Framework 提供的一个帮助类库。

先从C#代码执行原理上了解一下反射。

0201

如上图,通过编写的代码(高级语言,人类能够看懂),在VS内置的编译器中进行编译操作(首次编译),从而产生DLL(类库生成DLL)或执行文件;如果需要打开首次编译的文件,是需要一个依赖环境,即,CLR(安装.Net Framework时,自动配置的环境,里面包含了一些基本类库等)+ JIT(即时编译器);然后,执行文件在JIT的编译(二次编译)下,最终形成机器语言。

为什么需要二次编译呢?是希望能够在不同的平台使用。

CLR就像一个“适配器”,会根据不同的平台产生不同版本CLR,这里简单提一下,网上有很多资料可供查阅。

编译器如同一个“中间层”,像语法糖、泛型等都是源自于此。那么编译器生成的DLL和执行文件(.EXE后缀)本质是一样的;里面都包含两大内容:Metadata(元数据)和IL(中间语言)。

IL是一种面向对象的语言;Metadata是一个描述性的清单数据,里面包含了当前DLL或执行文件的具体“描述”,比如里面有哪些方法、属性、字段等等;统称为元数据。反射就是用来读取和使用Metadata的

反射创建对象

1. 反射的基本使用步骤
  • 加载程序集的三种方法

    //DLL名称  当前目录加载  (推荐使用该方法)
    Assembly assemblyLoad = Assembly.Load("DB.MySql");
    //完整目录加载   所以可以指定其他目录
    Assembly assemblyLoadFile = Assembly.LoadFile(@"E:\Study\MyReflection\MyReflection\bin\Debug\DB.MySql.dll");
    //DLL名称(带后缀)或者完整路径
    Assembly assemblyLoadFrom = Assembly.LoadFrom("DB.MySql.dll");
    
  • 获取类型

    //获取类型
    Type type = assemblyLoad.GetType("DB.MySql.MySqlHelper");
    
  • 创建程序集对象(创建时调用相关构造函数)

    1. 创建时调用相关构造函数
    2. 与通过new关键字创建的对象相同
    3. 当前产生的对象还无法调用方法等,需要转换;是因为编译器不认可。
    //创建对象 调用相关程序集的构造函数
    object oMySql = Activator.CreateInstance(type);
    
  • 类型转换并调用方法

    //类型转化并调用其方法
    MySqlHelper mySqlHelper = (MySqlHelper)oMySql;
    mySqlHelper.Query();
    
  • 步骤总结

    0202

2. 反射封装(反射+简单工厂+配置文件)

工厂就是用来创建对象的

  • App.config配置

    <appSettings>
        <add key="IDBHelperConfig" value="DB.MySql,DB.MySql.MySqlHelper"/>
     </appSettings>
    
  • 创建工厂

    工厂使用时,静态字段在第一次调用方法、属性或实例化之前,就已经完成了初始化;注意异常的处理。

    using DB.Interface;
    using System;
    using System.Configuration;
    using System.Reflection;
    
    namespace MyReflection
    {
        public class Factory
        {
            //获取配置文件信息
            private static string dBHelperInfo = ConfigurationManager.AppSettings["IDBHelperConfig"];
            //获取Load字符串
            private static string loadInfo = dBHelperInfo.Split(',')[0];
            //获取类型字符串
            private static string typeInfo = dBHelperInfo.Split(',')[1];
    
            /// <summary>
            /// 创建对象
            /// </summary>
            /// <returns></returns>
            public static IDBHelper CreateHelper()
            {
                Assembly assemblyLoad = Assembly.Load(loadInfo);
                Type type = assemblyLoad.GetType(typeInfo);
                object oIDBHelper = Activator.CreateInstance(type);
                return (IDBHelper)oIDBHelper;
            }
    
        }
    }
    
    
  • 调用封装

    Console.WriteLine("******** Reflection + 简单工厂 + 配置文件 ******");
    IDBHelper dBHelper = Factory.CreateHelper();
    dBHelper.Query();
    

截止当前,发现反射很麻烦,多步操作才能操作对象;即便通过封装,也看到实际效果。

接下来的操作,就会眼前一亮,也会慢慢体会到反射的强大。

完成封装并对代码重新生成,在程序集(DLL)目录下,如图,打开配置文件,并在其中直接更改相关配置信息;

0203

0204

随着配置文件更改,输出结果在没有更改代码的前提下,也达到预期效果;这种情况称为程序的可配置。

如果将其他项目的程序集复制过来(例如OracleHelper),通过程序的可配置,可实现同样的效果;这种情况称为程序的可配置可扩展。

之所以能够实现程序的可配置可拓展,就是因为反射是动态的,依赖的是字符串。也是反射的核心特点。

概括来讲,这就是IOC;IOC容器就是通过反射+配置文件+工厂实现的。

3. 破坏单例

所谓破坏单例就是可以调用私有函数。因为在常规的单例模式中,私有函数是无法进行调用,从而也无法实例化的。通过下面的例子具体了解一下反射是如何破坏单例。

using System;

namespace DB.SqlServer
{
    /// <summary>
    /// 单例模式
    /// </summary>
    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;
        }
    }
}

创建单例模式后,并对其调用;这里主要是说明一下CreateInstance(type, true)的第二个参数,为true时,可调用私有函数。

Console.WriteLine("******************** Reflection 破坏单例 **********************");
Assembly assembly = Assembly.Load("DB.SqlServer");
Type type = assembly.GetType("DB.SqlServer.Singleton");
Singleton singleton = (Singleton)Activator.CreateInstance(type, true);
4. 映射参数类型不同构造函数的对象

同预想中的一样,映射默认是调用无参构造函数。

如果想调用有参构造函数,则需对CreateInstance()方法继续深挖。


0205

创建测试类并调用,如上图,方法的第二个参数传入不同类型的对象数组,则调用不同类型构造函数。

5. 映射泛型对象

Console.WriteLine("******************** Reflection 泛型 **********************");
Assembly assembly = Assembly.Load("DB.SqlServer");
//务必写明泛型的占位符
Type type = assembly.GetType("DB.SqlServer.GenericTest`3");
//在类型信息的基础上,设定数组类型
Type newType = type.MakeGenericType(new Type[] { typeof(int), typeof(string), typeof(int) });
object genericTest = Activator.CreateInstance(newType); 

如上,创建测试类并调用;要注意以下内容:

  1. 通过反射获取泛型类型时,字符串中务必写明占位符(具体详见上篇的《泛型》);如缺失,则获取不到泛型的类型信息。
  2. 类型信息需两次设定(泛型方法的调用,则继续往下看)。

反射调用方法

通过反射调用方法最具体的例子就是体现在MVC上;通过MVC的路由路径,底层架构便获取到类和方法。

继续在ReflectionTest类中创建几个测试方法,并调用。

#region Method
//无参方法
public void Show1()
{
    Console.WriteLine($"{GetType()}的无参方法Show1");
}

//有参方法
public void Show2(int id)
{
    Console.WriteLine($"{GetType()}的入参为整数的方法Show2");
}

//重载方法
public void Show3(int id, string str)
{
    Console.WriteLine($"{GetType()}的重载方法Show3,入参为整数、字符串");
}

public void Show3(string str, int id)
{
    Console.WriteLine($"{GetType()}的重载方法Show3,入参为字符串、整数");
}

//私有方法
private void Show4(string str)
{
    Console.WriteLine($"{GetType()}的私有方法Show4");
}

//静态方法
public static void Show5(int id)
{
    Console.WriteLine($"{typeof(ReflectionTest)}的私有方法Show5");
}
#endregion
Console.WriteLine("******************** Reflection 方法操作 **********************");

Assembly assembly = Assembly.Load("DB.SqlServer");
Type type = assembly.GetType("DB.SqlServer.ReflectionTest");
object oTest = Activator.CreateInstance(type);

//无参方法
{
    MethodInfo show1Method = type.GetMethod("Show1");
    show1Method.Invoke(oTest, new object[] { });
}

//有参方法
{
    MethodInfo show2Method = type.GetMethod("Show2");
    show2Method.Invoke(oTest, new object[] { 123 });
}

//重载方法1
{
    MethodInfo show3Method = type.GetMethod("Show3", new Type[] { typeof(int), typeof(string) });
    show3Method.Invoke(oTest, new object[] { 111, "重载方法" });
}

//重载方法2
{
    MethodInfo showMethod = type.GetMethod("Show3", new Type[] { typeof(string), typeof(int) });
    showMethod.Invoke(oTest, new object[] { "重载方法", 111 });
}

//私有方法
{
    MethodInfo privateMethod = type.GetMethod("Show4", BindingFlags.Instance | BindingFlags.NonPublic);
    privateMethod.Invoke(oTest, new object[] { "私有方法" });
}

从调用中能够看出,不同方法的调用主要是掌握GetMethod()Invoke()方法;具体不再赘述,结合例子和方法API便能很好的掌握。

泛型方法的调用比较有趣,继续往下看。

/// <summary>
/// 泛型类
/// </summary>
/// <typeparam name="T"></typeparam>
public class GenericMethod<T>
{
    public void Show<X, Y>(T t, X x, Y y)
    {
        Console.WriteLine($"t.type={t.GetType().Name}, x.type={x.GetType().Name}, y.type={y.GetType().Name}");
     }
}
Console.WriteLine("******************** Reflection 泛型方法调用 **********************");
Assembly assembly = Assembly.Load("DB.SqlServer");
Type type = assembly.GetType("DB.SqlServer.GenericMethod`1");
Type newType = type.MakeGenericType(new Type[] { typeof(int) }); 
object oTest = Activator.CreateInstance(newType);
//根据类型获取方法
MethodInfo methodInfo = newType.GetMethod("Show");
//制定泛型方法参数类型
MethodInfo newMethodInfo = methodInfo.MakeGenericMethod(new Type[] { typeof(string), typeof(int) });
//方法调用
newMethodInfo.Invoke(oTest, new object[] { 123, "泛型方法", 123 });

反射泛型对象,并调用泛型方法;需再次确定泛型方法的参数类型。

反射获取、设置字段和属性

关于属性和字段的操作,更多是在ORM中使用。

首先创建一个People类。

using System;

namespace DB.Model
{
    public class People
    {
        /// <summary>
        /// 无参构造函数
        /// </summary>
        public People()
        {
            Console.WriteLine($"{GetType().FullName}被创建");
        }

        /// <summary>
        /// ID
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// 姓名
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 备注
        /// </summary>
        public string Notes;
    }
}

通常会将此类作为数据的载体,实例化并赋值,如下:

Console.WriteLine("******************** Reflection 属性和字段 **********************");
var people = new People
    {
        Id = 1,
        Name = "Rock",
        Notes = "People类"
     };
Console.WriteLine($"People.Id = {people.Id}");
Console.WriteLine($"People.Name = {people.Name}");
Console.WriteLine($"People.Notes = {people.Notes}");

相关的API较多,这里仅描述常用的几个,例如SetValue()、GetValue(),以及获取属性和字段集合的方法GetProperties()、GetFields()。通过反射对实体进行操作:

var type = typeof(People);
var peopleObj = Activator.CreateInstance(type);
foreach (var prop in type.GetProperties())
{
    if (prop.Name.Equals("Id"))
    {
        prop.SetValue(peopleObj, 2);
    }
    else if (prop.Name.Equals("Name"))
    {
        prop.SetValue(peopleObj, "David");
    }

    Console.WriteLine($"{type.Name}.{prop.Name} = {prop.GetValue(peopleObj)}");
}

foreach (var field in type.GetFields())
{
    if (field.Name.Equals("Notes"))
    {
        field.SetValue(peopleObj, "备注内容");
    }
    Console.WriteLine($"{type.Name}.{field.Name} = {field.GetValue(peopleObj)}");
}

当前,似乎没有看出反射在属性和字段上带来的便利;通过普通实例化和设置更为优越。

实际上,反射在属性和字段上实践主要用于以下两个场景。

  1. 属性和字段的遍历;通常的实例对属性或字段的遍历,是直接写死的,比如上面的例子提到的Console.WriteLine($"People.Id = {people.Id}");Id属性是直接写死在代码中的。而通过反射,无论实体属性(字段)如何增减,都会通过相应方法动态反馈出来。

  2. DB实体赋值给展示实体;以往一般都会这么做:

    //DB实体
    var people = new People
    {
        Id = 1,
        Name = "Rock",
        Notes = "People类"
    };
    //展示实体
    var peopleDto = new PeopleDTO
    {
        Id = people.Id,
        Name = people.Name,
        Notes = people.Notes
    };
    

    此方式通常叫做硬编码。同样是“写死”了,后期维护同样需要改动代码。而通过反射就得以解决。

    先呈现“基本思路”的代码:

    var peopleType = typeof(People);
    var peopleDtoType = typeof(PeopleDTO);
    var peopleDtoTypeObj = Activator.CreateInstance(peopleDtoType);
    foreach (var prop in peopleDtoType.GetProperties())
    {
        if (prop.Name.Equals("Id"))
        {
            prop.SetValue(peopleDtoTypeObj, peopleType.GetProperty("Id").GetValue(people));
        }
        else if (prop.Name.Equals("Name"))
        {
            prop.SetValue(peopleDtoTypeObj, peopleType.GetProperty("Name").GetValue(people));
        }
    }
    

    优化后的代码:

    var peopleType = typeof(People);
    var peopleDtoType = typeof(PeopleDTO);
    var peopleDtoTypeObj = Activator.CreateInstance(peopleDtoType);
    foreach (var prop in peopleDtoType.GetProperties())
    {
    prop.SetValue(peopleDtoTypeObj, peopleType.GetProperty(prop.Name)?.GetValue(people));
    }
    

    ?. 是C# 6.0引进的运算符;如果对象为NULL,则不进行后面的获取成员的运算,直接返回NULL

    从反射优化的代码能够看出其重要特性:动态。

    细心的小伙伴也发现了这种方式的弊端,如果两个实体属性命名不同(一个是Name,另一个是UserName)就会发生异常。这个通过“特性”可以完美解决(下篇讲解的特性会讲解)。

    通过泛型,可以将该方法进一步优化,从而实现不同实体之间数据的传递。

反射的优点和局限

优点:动态

这个优点凸显出实际意义;C#是面向对象的开发语言;而面向对象自然会将功能进行封装,等待调用即可;封装后,就代表着代码的“稳定”;如多处调用后,再行改动的话,便与“面向对象”相违背,也导致程序不稳定。反射拥有可配置可拓展的特点,很好的诠释了这一点。

另外,框架的搭建和反射有着密不可分的。

局限:
  1. 编写复杂

  2. 避开编译器的检查

    我想这是反射最主要的软肋;编译器在编写代码的过程中是不可或缺的。当然细心也是程序员的职业素养,也不是难事_

  3. 性能问题

    大家众说纷纭,大致都认为消耗性能。

    首先,实际代码中不会像泛型一样常用,基本是用在底层或封装方法中,性能上几乎可以忽略不计。

    其次,当前硬件设备性能都有目共睹,完全可以不用考虑性能上疑虑。

    最后,像EF、WebAPI等,首次访问都会比较慢,其后就很快;相比较反射,就是小巫见大巫了。近年等.Net Core普及后,更可宽心使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值