65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 讲座 - 第 7 讲: 反射( 通过 C# 示例)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (30投票s)

2016年4月1日

CPOL

7分钟阅读

viewsIcon

54820

downloadIcon

1049

这是我关于 C# 系列的第 7 篇文章, 关于反射

全部课程集


引言

这是我 C# 课程系列的第七篇文章。这次我将讨论 .NET 技术,称为反射。这是一种非常强大的技术,适用于由多家公司开发的软件模型,其中一家开发服务器/主机站点,另一家开发客户端。这种插件模型非常普遍,当主机在运行时查找并发现可用的客户端时。当您需要在编译时不知道任何信息的情况下发现和/或实例化类型时,反射就适用。您应该知道,从 .NET 4.5 开始,反射 API 已更新,并且它构建了比以前更高效的功能集,因为以前的功能集效率低下且速度缓慢,消耗了大量资源。

反射的能力和使用领域

System.Reflection 中的类与 System.Type 一起为我们提供了以下主要使用领域:

  •  获取已加载程序集的信息

    1. Assemblies

    1. Types

      1. 接口

      1. 值类型

  • 在运行时实例化类型

  • 调用对已加载程序集中类型的访问

反射实际上在普通应用程序中很少使用,而这个确实非常强大的机制主要由 Microsoft 自己用于 Visual Studio 的工作及其框架。当开发人员需要发现某些第三方二进制文件或需要从特定程序集中加载特定类型时,他们就会使用反射。您可以想象这样一种情况:例如,某个主机接收第三方插件,并且根据用户决定,应为特定类型发现插件。

反射的主要缺点是它运行速度非常慢。如果有变通方法,您就不应该使用反射。当您使用反射时,您依赖于事物的字符串表示,在幕后,您的程序集的元数据会不断地被监控以查找事物的字符串名称。这是一个消耗资源的操作。

反射示例代码

对于这篇文章,我在我的程序集中定义了以下类,我们将在下面的部分中以编程方式发现这些数据。

    [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = false)]//I want my attribute to be applied to anything where it is possible - AttributeTargets.All
    //also my attribute can be inherited - Inherited=true
    //also my attribute can't be applied several times for one element - AllowMultiple=false
    public sealed class MyOwnExcitingAttribute : System.Attribute
    {
        private string m_StringData;
        private int m_IntegerData;

        public MyOwnExcitingAttribute()
        {
            m_StringData = "default value";
        }
        public MyOwnExcitingAttribute(string s)
        {
            m_StringData = s;
        }
        public int IntegerData
        {
            get { return m_IntegerData; }
            set { m_IntegerData = value; }
        }
        public string StringData
        {
            get { return m_StringData; }//we encapsulate only reading, you can set this variable only in constructor
        }
    }
    public class AttributeUser
    {
        [MyOwnExciting("custom value applied to constructor", IntegerData = 2)]
        public AttributeUser()
        {
        }
        [MyOwnExciting("custom value applied to function", IntegerData = 5)]
        public void FunctionThatDoSomething()
        {
        }
        [MyOwnExciting("custom value applied to member", IntegerData = 10)]
        public int m_IntData;
        [MyOwnExciting(IntegerData = 10)]//here string data will have "default value"
        private int m_IntData2;

        public int IntData2
        {
            get { return m_IntData2; }
            set { m_IntData2 = value; }
        }
    }

程序集级别的反射

在本节中,我们将回顾如何使用反射来发现程序集。当 CLR 将任何程序集加载到您的应用程序中时,它会使用 System.Reflection.Assembly 类中的 Load 方法。此方法等同于 Win32 API 中的 LoadLibrary。要调用 Load 方法,您需要向其提供有关程序集的信息,并且要获取它,您需要使用特殊的实用程序。此方法有几个重载,此外,Assembly 类还有几个函数可以提供加载程序集的替代方法。例如,要从特定路径加载程序集,您需要调用 Assembly.LoadFrom 函数。对于这篇文章,我将所有源构建到一个程序集中,并在运行时发现它本身。为此,我使用 Assembly.GetExecutingAssembly 方法。

 

Assembly current = Assembly.GetExecutingAssembly();

 

一旦您加载了程序集,您就拥有了一个保存其引用的变量,并可以使用它来发现它。Assembly 类具有属性和方法的列表,您可以调用这些属性和方法来了解特定程序集已定义和应用的。下面的代码演示了如何以编程方式提取程序集数据。

            //-------------------------------------ASSEMBLY------------------------------
            Assembly currentAssembly = Assembly.GetExecutingAssembly();
            Assembly loadedAssembly = Assembly.LoadFrom(@"..\..\..\ReflectionSample\bin\Debug\ReflectionSample.dll");
            //properties
            Console.WriteLine(currentAssembly.CodeBase);//shows assembly binary file path
            //below I output only data about my own attribute applied for this assembly (look for AssemblyInfo.cs)
            var customAttributes = currentAssembly.CustomAttributes;
            foreach(CustomAttributeData attribute in customAttributes)
            {
                string s = attribute.AttributeType.GetType().Name;
                if (attribute.AttributeType == typeof(MyOwnExcitingAttribute))
                {
                    Console.WriteLine(attribute.AttributeType);
                    Console.WriteLine(attribute.Constructor);
                    Console.WriteLine(attribute.ConstructorArguments);
                    Console.WriteLine(attribute.NamedArguments);
                }
            }
            //below I read all types defined in my assembly
            Console.WriteLine("\n Assemblies types are: " + currentAssembly.EntryPoint);
            var definedTypes = currentAssembly.DefinedTypes;
            foreach (Type t in definedTypes)
            {
                Console.WriteLine("Type namespace = " + t.Namespace);
                Console.WriteLine("Type name = " + t.Name);
                Console.WriteLine("Is type public =" + t.IsPublic);
            }
            //outputs entry point
            Console.WriteLine("\n Assembly entry point = " + currentAssembly.EntryPoint);
            //below I read all types that are exported by my assembly
            Console.WriteLine("\n Assemblies types that are exported are: ");
            var exportedTypes = currentAssembly.ExportedTypes;
            foreach (Type t in exportedTypes)
            {
                Console.WriteLine("Type namespace = " + t.Namespace);
                Console.WriteLine("Type name = " + t.Name);
                Console.WriteLine("Is type public =" + t.IsPublic);
            }
            //below I read all types that are exported by my assembly
            Console.WriteLine("\n Assembliy loaded only for reflection: " + currentAssembly.ReflectionOnly);

            //below I read all modules in my assembly
            Console.WriteLine("\n Assemblies modules are: ");
            var modules = currentAssembly.Modules;
            Module myModule = null;
            foreach (Module m in modules)
            {
                Console.WriteLine("Scope name = " + m.ScopeName);
                Console.WriteLine("Type name = " + m.Name);
                Console.WriteLine("Assembly =" + m.Assembly);
                myModule = m;
            }

总的来说,使用 Assembly 类,您可以:

  • 获取程序集中的所有类型列表
  • 获取导出的类型列表
  • 获取应用于程序集的属性列表
  • 获取程序集中的模块列表
  • 获取不同的程序集属性,例如:名称、代码库、入口点、仅用于反射等。
  • 以多种不同方式加载程序集
  • 从此程序集中实例化指定的类型
  • 获取特定类型的程序集
  • 等等。
  • 等等。
  • 等等。

Assembly 类充满了有用的功能,如果您使用此类加载了某个 .NET 程序集,您将能够发现它以及其中声明的所有内容。

模块级别的反射

一旦您加载了程序集并使用反射对其进行发现,您就会获得它包含的模块并对其进行发现。要获取有关模块的信息,您可以使用 System.Reflection.Module 类。下面的代码演示了模块级别的一些反射示例。

            //-------------------------------------MODULE------------------------------
            Console.WriteLine("Module's assembly is: " + myModule.Assembly);
            customAttributes = myModule.CustomAttributes;
            foreach (CustomAttributeData attribute in customAttributes)
            {
                    Console.WriteLine(attribute.AttributeType);
                    Console.WriteLine(attribute.Constructor);
                    Console.WriteLine(attribute.ConstructorArguments);
                    Console.WriteLine(attribute.NamedArguments);
            }
            Console.WriteLine("Module's fully qualified name is: " + myModule.FullyQualifiedName);
            Console.WriteLine("Module's name: " + myModule.Name);
            Console.WriteLine("Module's scope name: " + myModule.ScopeName);

            //i'm looking for specific type in my module
            var queriedType = myModule.GetType("_07_Reflection.AttributeUser");
            if (queriedType != null)
            {
                Console.WriteLine(queriedType.FullName);
            }

使用 Module 类,您可以获取:

  • 其程序集名称
  • 完全限定名
  • 元数据流版本
  • 元数据令牌
  • 模块句柄
  • 版本 ID
  • 作用域名称
  • 名称
  • 模块包含的自定义属性
  • 模块的类型
  • 模块的全局方法(至少 C# 我不知道如何在 C# 中定义全局方法)

 

Type 模块中还有不同的方法,可以让您处理它或模块中的数据。有一套方法可以查询模块中定义的类型、方法和字段。

在此处保持您所做的任何更改或改进的实时更新。

类型级别的反射

您可以使用一组类来获取有关特定类型的信息:

  •  System.Reflection.ConstructorInfo - 用于发现有关构造函数的信息:名称、参数、可见性、实现细节(抽象或虚拟)
  •  System.Reflection.MethodInfo - 用于发现有关方法的信息:名称、参数、可见性、实现细节(抽象或虚拟)
  • System.Reflection.FieldInfo - 用于获取有关字段的信息:名称、可见性、实现细节(静态或非静态)
  • System.Reflection.EventInfo - 用于获取有关事件的信息:名称、事件处理程序数据类型、自定义属性、声明类型
  • System.Reflection.PropertyInfo - 用于获取有关属性的信息:名称、数据类型、声明类型、反射类型、属性的只读或可写状态
  • System.Reflection.ParameterInfo - 用于获取有关参数的信息:名称、数据类型、参数是输入还是输出、在方法签名中的位置

下面的代码演示了使用其中一些类来发现有关内部类型组织的各种信息。

            //-------------------------------------TYPE------------------------------
            Console.WriteLine("------------DISCOVERING TYPE: " + queriedType.Name + "-----------------");
            Console.WriteLine("--Constructors details");
            var constructorsInfo = queriedType.GetConstructors();
            foreach (ConstructorInfo ci in constructorsInfo)
            {
                Console.WriteLine("Name = " + ci.Name);
                Console.WriteLine("Member type = " + ci.MemberType);
                Console.WriteLine("Is virtual = " + ci.IsVirtual);
                Console.WriteLine("Is static = " + ci.IsStatic);
                Console.WriteLine("Is public = " + ci.IsPublic);
            }
            Console.WriteLine("--Methods details");
            var methodsInfo = queriedType.GetMethods();
            foreach (MethodInfo mi in methodsInfo)//here we can see also methods derieved from System.Object
            {
                Console.WriteLine("Name = " + mi.Name);
                Console.WriteLine("Member type = " + mi.MemberType);
                Console.WriteLine("Is virtual = " + mi.IsVirtual);
                Console.WriteLine("Is static = " + mi.IsStatic);
                Console.WriteLine("Is public = " + mi.IsPublic);
            }
            Console.WriteLine("--Fields details");
            var fieldsInfo = queriedType.GetFields();//here we can see only public fields
            foreach (FieldInfo fi in fieldsInfo)
            {
                Console.WriteLine("Name = " + fi.Name);
                Console.WriteLine("Member type = " + fi.MemberType);
                Console.WriteLine("Is static = " + fi.IsStatic);
                Console.WriteLine("Is public = " + fi.IsPublic);
            }
            Console.WriteLine("--Properties details");
            var propInfo = queriedType.GetProperties();
            foreach (PropertyInfo pi in propInfo)
            {
                Console.WriteLine("Name = " + pi.Name);
                Console.WriteLine("Member type = " + pi.MemberType);
                Console.WriteLine("Can read = " + pi.CanRead);
                Console.WriteLine("Can write = " + pi.CanWrite);
            }

 

反射不仅仅是发现。使用反射实例化和调用

反射不仅为我们提供了发现托管程序集内容的能力,还可以在运行时实例化和使用它们。有几种方法可以做到这一点:

  • 后期隐式绑定。这种方法适用于 Visual Basic 和 JScript 等语言,与本主题关系不大,但我想在此提及,因为它了解反射很重要。绑定是基于唯一名称查找实现的过程。当这种情况发生在运行时而不是编译时,就称为后期绑定。Visual Basic 允许您在代码中使用隐式后期绑定;Visual Basic 编译器会调用一个辅助方法,该方法使用反射来获取对象类型。传递给辅助方法的参数会导致在运行时调用相应的方法。
  • 自定义绑定。除了编译器执行的后期隐式构建之外,还可以在代码中显式使用反射来执行后期绑定。CLR 支持多种语言,后期绑定的规则因所有这些语言而异。我们将使用 C# 的示例回顾后期构建。通过反射,您可以在运行时加载程序集,探索它以查找所需类型,然后调用其方法或访问其字段或属性。当您在编译时不知道对象的类型,并且其类型稍后在程序执行期间确定时,此技术非常有用。

一旦您获得了派生自 Type 的对象的引用,就有几种方法可以实例化它:

  • System.Activator 的 CreateInstance - 此方法具有一组重载,使我们能够创建对象。我在下面的示例中使用了接受 Type 作为参数的方法。
  • System.Activator 的 CreateInstanceFrom - 此方法与上面类似,但其任何重载都不能接受 Type 参数,所有重载都需要程序集和类型的字符串数据。
  • System.Reflection.ConstructorInfo 的 Invoke

下面的代码演示了本节所述的内容。

            //-------------------------------------INSTATIATING------------------------------
            Console.WriteLine("------------INSTANTIATING TYPE: -----------------");
            Console.WriteLine("--System.Activator.CreateInstnce");
            var Instance1 = System.Activator.CreateInstance(queriedType);
            Console.WriteLine(Instance1.GetType().FullName);
            var Instance2 = constructorsInfo[0].Invoke(null);
            Console.WriteLine(Instance2.GetType().FullName);

在运行时构建类型。

反射有一个名为 System.Reflection.Emit 的命名空间。此命名空间包含允许您发出 Microsoft 中间语言 (MSIL) 元数据的类,并可以选择在磁盘上生成 PE 文件。Emit 类的用户是脚本引擎和编译器。我不会在本文中深入研究 System.Reflection.Emit 的细节,因为这是一个需要单独文章的主题,但我想让您了解 Reflection 命名空间中存在这样的东西。

来源

Jeffrey Richter - CLR via C#
Andrew Troelsen - Pro C# 5.0 and the .NET 4.5 Framework
© . All rights reserved.