C#: 有人能解释一下反射的实际应用吗?

20
我尝试在stackoverflow上搜索这个问题,并希望有人能给出一个好的解释,但很遗憾没有找到。另外问了一个朋友一个不同的问题,他回答说 "reflection" 然后就下线了。作为一个 C# 初学者(之前只是业余的VB.net程序员,也了解JavaScript,ActionScript和C),我正在竭尽全力理解这些高级概念。虽然有很多哲学性的答案——比如 "应用程序看着自己",但它们并没有提供任何关于实际发生了什么以及在什么情况下如何使用的实用提示。那么,什么是反射,它为什么重要,我应该如何使用它?

这应该是两个问题,或者去掉对反射和异常的小抱怨。 - Samuel
@Tom:请发布完整的异常信息,包括所有内部异常。使用try Application.Run(new frmMain()); catch (Exception ex){MessageBox.Show(ex.ToString());},然后发布消息框的内容。 - John Saunders
@Tom:因为你的问题可能与反射无关。 - John Saunders
很抱歉,发牢骚是无关紧要的。我可以找到内部异常并从中工作,只是很烦人当VS停在外部异常时,而实际上是内部异常揭示了问题。我会将其编辑掉。 - Tom Corelis
@Tom:请看下面我的帖子,了解如何让Visual Studio在异常被抛出时停止,而不是在异常被捕获时停止。 - Matt Davis
18个回答

15

反射提供了在运行时确定事物并执行代码的能力。

如果您不想使用它,也不必使用它,但它对于动态行为非常有用。

例如:

a)您可以使用反射通过加载外部配置文件并基于其启动服务来配置应用程序。只要它们符合特定接口或API,您的应用程序就不必预先知道实现这些服务的类。

b)使用反射,您可以即时生成类和代码,从而简化某些编程任务,因为程序员不必明确创建所需的所有代码。

c)反射对于检查代码的程序非常宝贵。其中一个例子是集成开发环境(IDE)或用户界面(UI)设计器。

d)反射帮助您减少样板代码。

e)反射对于在代码中定义小型领域特定语言(DSL)非常方便。


9

(我的定义)反射是编写静态代码以在运行时执行代码的能力,通常在编译时确定。

例如,我可以通过编译该命令来调用类方法进行绘图:

pen.DrawLine()

通过反射,我可以首先查看我的对象是否有一个叫做“drawline”的方法,如果有,就调用它。(请注意,这不是实际的C#反射语法)

 if(pen.Methods.Contains("DrawLine"))
 {
    pen.InvokeMethod("DrawLine"))
 }

我不是反射大师,但我曾在插件架构中使用过反射技术。
通过反射,我可以在运行时加载一个.NET程序集(在这种情况下是一个dll),找出所有在.NET程序集中的类型,查看这些类型是否实现了特定的接口,如果是,则实例化该类,并调用接口方法。
我知道这个用例有点技术性,但基本上反射允许我动态地加载插件(即在运行时),并让我对其进行类型安全的调用。

9
反射最常见的用途是扩展所谓的RTTI(运行时类型信息),主要是C++程序员的领域。反射是.net构建方式的副作用,微软决定向微软以外的开发人员公开他们用于创建Visual Studio和.net运行时的库。大部分反射库都集中在可以在运行时调用的类型发现和创建上。这允许一些非常强大的自引用代码。以下示例是我们配置管理系统核心的一个例子(为了清晰起见删除了一些内容):
    public static IMyCompanySetting UnwrapSetting(XmlNode settingNode)
    {

        string typeName = settingNode.Attributes["type"].Value;
        string typeAssembly;
        if(settingNode.Attributes["assembly"] != null)
        {
            typeAssembly = settingNode.Attributes["assembly"].Value;
        }

        Type settingType = null;
        Assembly settingAssembly = null;
        try
        {
            // Create an object based on the type and assembly properties stored in the XML
            try
            {
                settingAssembly = Assembly.Load(typeAssembly);
                if (settingAssembly == null)
                {
                    return null;
                }
            }
            catch (Exception outerEx)
            {
                try
                {
                    settingType = GetOrphanType(typeName);
                }
                catch (Exception innerEx)
                {
                    throw new Exception("Failed to create object " + typeName + " :: " + innerEx.ToString(), outerEx);
                }
            }

            // We will try in order:
            // 1. Get the type from the named assembly.
            // 2. Get the type using its fully-qualified name.
            // 3. Do a deep search for the most basic name of the class.
            if (settingType == null && settingAssembly != null) settingType = settingAssembly.GetType(typeName);
            if (settingType == null) settingType = Type.GetType(typeName);
            if (settingType == null) settingType = GetOrphanType(typeName);
            if (settingType == null) throw new System.Exception(
                String.Format("Unable to load definition for type {0} using loosest possible binding.", typeName));
        }
        catch (Exception ex)
        {
            throw new CometConfigurationException(
                String.Format("Could not create object of type {0} from assembly {1}", typeName, typeAssembly), ex);
        }

        bool settingIsCreated = false;
        IMyCompanySetting theSetting = null;

        // If the class has a constructor that accepts a single parameter that is an XML node,
        // call that constructor.
        foreach (ConstructorInfo ctor in settingType.GetConstructors())
        {
            ParameterInfo[] parameters = ctor.GetParameters();
            if (parameters.Length == 1)
            {
                if (parameters[0].ParameterType == typeof(XmlNode))
                {
                    object[] theParams = { settingNode };
                    try
                    {
                        theSetting = (IMyCompanySetting)ctor.Invoke(theParams);
                        settingIsCreated = true;
                    }
                    catch (System.Exception ex)
                    {
                        // If there is a pre-existing constructor that accepts an XML node
                        // with a different schema from the one provided here, it will fail
                        // and we'll go to the default constructor.
                        UtilitiesAndConstants.ReportExceptionToCommonLog(ex);
                        settingIsCreated = false;
                    }
                }
            }
        }

这段代码允许我们创建无限数量的实现IMyCompanySetting接口并使用XML进行序列化和反序列化的类。然后,给定一块对象序列化的XML输出,系统可以将其转换回对象,即使该对象本身来自序列化库没有静态链接的库。
这里有三个重要的反射操作,如果没有反射是不可能实现的:
1. 根据名称在运行时加载程序集。 2. 根据名称在运行时从程序集中加载对象。 3. 基于类的对象签名调用对象构造函数,该类在编译时未知。

5

假设你有一个接口的两个不同实现。你想要让用户通过一个简单的文本配置文件来选择其中一个。

使用反射,你可以从配置文件中读取你想要使用的实现类的名称(作为字符串),并实例化该类的一个实例。


我明白了。所以,本质上你是在说反射是在运行时确定一个神秘的组件/接口/DLL能做什么的能力?这很有道理。 - Tom Corelis

4
有时候在设计时不知道一个类有哪些属性或方法,但又需要读取这些属性或调用这些方法,这时候就可以使用反射。下面的代码演示了如何在不知道类的信息的情况下获取类的所有属性并检索它们的值。或者你可以通过反射按名称获取方法,即使你在编译时不知道方法的名称,也可以通过反射调用它。这样,你就可以创建一个脚本语言,操作在另一个用户提供的 DLL 中定义的对象。当然,你也可以枚举类中的所有方法或按名称检索特定属性,但下面的代码没有演示这些情况。
class Program
{
  static void Main(string[] args)
  {
     UserDefinedClass udc = new UserDefinedClass();
     udc.UserProperty = "This is the property value";

     ClassTracer ct = new ClassTracer(udc);
     ct.TraceProperties();
     ct.CallMethod("UserFunction", "parameter 1 value");
  }
}

class ClassTracer
{
  object target;

  public ClassTracer(object target)
  {
     this.target = target;
  }

  public void TraceProperties()
  {
     // Get a list of all properties implemented by the class
     System.Reflection.PropertyInfo[] pis = target.GetType().GetProperties();
     foreach (System.Reflection.PropertyInfo pi in pis)
     {
        Console.WriteLine("{0}: {1}", pi.Name, pi.GetValue(target, null));
     }
  }

  public void CallMethod(string MethodName, string arg1)
  {
     System.Reflection.MethodInfo mi = target.GetType().GetMethod(MethodName);
     if (mi != null)
     {
        mi.Invoke(target, new object[] { arg1 });
     }
  }
}

class UserDefinedClass
{
  private string userPropertyValue;

  public string UserProperty
  {
     get
     {
        return userPropertyValue;
     }
     set
     {
        userPropertyValue = value;
     }
  }

  public void UserFunction(string parameter)
  {
     Console.WriteLine("{0} - {1}", userPropertyValue, parameter);
  }
}

4

反射可以让您深入一个程序集并使用它,即使您以后没有引用它。

考虑一个插件系统,其中主机不知道它将持有哪些插件;使用反射(和正确的架构),您可以构建一个简单的解决方案来实现这一点。

再考虑另一种情况,您必须根据字符串调用对象上的方法,反射也可以帮助您实现这一点。

还有许多其他用途,但我希望这两个用例能够激发您对CLR这个优秀特性的兴趣。


3

反射是使用代码来检查代码本身。例如,您可以调用 foo.DoBar() ,也可以调用:

foo.GetType().GetMethod("DoBar").Invoke(foo,null);

那看起来是一个绕路的方法,但它也可以用于执行一些可能会违反规则的黑魔法,例如调用框架类的私有方法。它还可以用于实际生成代码和输出新二进制文件,以及许多对少数人非常有用的事情。
要获取更多信息,请查看Reflection 的 MSDN 部分

2

有许多情况下,应用程序不应该对自身做出太多假设,而应该在运行时查看其实际情况。这就是反射发挥作用的地方。例如,ASP.NET对使用的成员资格提供程序没有任何假设,除了它继承了适当的类。它使用反射在运行时查找类并实例化它。由于解耦和减少类之间的依赖性,这提供了很大程度上的可扩展性。

当然,这只是反射的单个用例。还有许多其他用例可能非常有用。


2
安德鲁·特罗尔森(Andrew Troelsen)是《Pro C# 2008 and the .NET 3.5 Platform》一书的作者,他这样定义反射:

在.NET世界中,反射是运行时类型发现的过程。

我不确定我能给你一个准确的解释,但我可以告诉你我如何使用它。

我可以在程序集(dll)上放置一个用户定义的属性。使用反射在运行时,我可以检查某个目录位置中的所有程序集,以查看它们是否定义了此属性。这可以提示我的应用程序如何使用程序集,比如作为插件。

我还使用反射来加载和保存应用程序设置。


“就我所知,除非你在使用反射,否则我不确定它与您的GUI抛出异常有什么关系。”
“关于C#反射,我唯一知道的是它非常烦人,因为所有我的GUI应用程序都喜欢在Application.Run(new frmMain())处抛出令人非常恼火和无意义的“目标调用引发了异常”的异常,而不是停在真正出问题的地方(如内部异常中所示)。”
“您的陈述使我相信您的应用程序中几乎没有try/catch块,以至于每个异常都会向调用堆栈的顶部渗透。如果您正在使用Visual Studio 2005/2008的调试模式,请进入Debug菜单并选择Exceptions...菜单选项。在“异常”对话框中,检查Thrown列下的复选框。现在,当您运行应用程序并抛出异常时,调试器将在抛出异常的位置中断。”

目前,我的异常情况很多都没有被捕获并且一直冒泡到顶层。我正在稳定代码,这使得难以准确找出问题所在——但是应用充满了实验性的东西,所以并不总是编写try...catch。谢谢! - Tom Corelis
好的,不难...但是很烦人。 - Tom Corelis

2
假设您有一些业务实体,它们全部都来自于一个名为Entity的基类。同时,您需要/希望所有派生类都可以克隆。您可以在基类上实现一个名为“Clone”的方法(接口ICloneable),该方法将循环遍历当前类的所有属性(尽管它是在Entity类中实现的)并将其复制到克隆对象中。这是Reflection真正有用的情况之一,因为您无法知道基类的属性名称和数量。您也不想在所有派生类中实现该方法。但是,您可能希望使该方法虚拟化。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接