使用反射是否有问题?


49

我不知道为什么,但是当我使用反射时,我总是觉得自己在“作弊”-也许是因为我知道我要表现出色。

我的一部分说,如果它是您正在使用的语言的一部分,并且可以完成您尝试做的事情,那么为什么不使用它呢?我的另一部分说,必须有一种无需使用反射即可执行此操作的方法。我想也许这取决于情况。

使用反射时需要注意哪些潜在问题,我应该如何关注它们?尝试寻找更常规的解决方案需要花费多少精力?


2
我想这取决于语言和环境。一些人支持甚至鼓励它。有时,在Java中工作时,我希望获得更好的反射支持。
FrustratedWithFormsDesigner

18
好吧,我想在“作弊反射”和有效使用之间进行区分并不难:当您检查私有成员或使用它调用私有方法(在接口周围工作)时,很可能就是作弊。当您使用它与您以前不了解的类型进行交互时,则可能没问题(数据绑定,代理等)。
猎鹰

1
您使用什么语言?Brainfuck没有思考,Python则不同。
工作

1
我也从来不明白为什么允许它通过反射来访问私有方法。根据我的直觉,应该禁止这样做。
乔治

3
很难相信这样一个措辞不佳的问题在未经任何编辑的情况下获得了27次投票。
亚伦诺特,2011年

Answers:


48

不,这不是在作弊-这是一种解决某些编程语言问题的方法。

现在,它通常不是最佳的(最干净,最简单,最容易维护的)解决方案。如果有更好的方法,请确实使用该方法。但是,有时没有。或者,如果存在的话,它会变得更加复杂,涉及大量的代码重复等,从而使其不可行(从长远来看很难维护)。

当前项目(Java)中的两个示例:

  • 我们的一些测试工具使用反射从XML文件加载配置。要初始化的类具有特定的字段,并且配置加载程序使用反射将命名fieldX为该元素的XML元素与该类中的适当字段进行匹配,并对其进行初始化。在某些情况下,它可以根据所标识的属性即时构建一个简单的GUI对话框。如果没有反思,这将需要跨多个应用程序使用数百行代码。因此,反思帮助我们快速构建了一个简单的工具,而不必大惊小怪,并使我们能够专注于重要的部分(对Web应用程序进行回归测试,分析服务器日志等),而不是无关紧要的部分。
  • 我们的旧版网络应用程序的一个模块旨在将数据从数据库表导出/导入到Excel工作表,然后再返回。它包含许多重复的代码,当然重复项并不完全相同,其中一些包含错误等。通过反射,自省和注释,我设法消除了大部分重复项,从头开始减少了代码量5K到2.4K以下,同时使代码更健壮,更易于维护或扩展。现在,由于明智地使用了反射,该模块不再成为我们的问题。

像其他任何强大的工具一样,最重要的是反射也可以用来射击自己的脚。如果您了解何时以及如何使用(不使用)它,它将为您带来优雅而干净的解决方案,以解决其他困难的问题。如果滥用它,则可以将原本很简单的问题变成一个复杂而丑陋的混乱局面。


8
当您出于转换原因而拥有中间对象时,+ 1反射对于数据导入/导出非常有用。
艾德·詹姆斯

2
它在数据驱动的应用程序中也非常有用-无需使用大量的if (propName = n) setN(propValue);,您可以将您的XML标记命名为与代码属性相同的名称,并对其进行循环。此方法还使以后添加属性变得更加简单。
Michael K

反射在Fluent接口(例如FluentNHibernate)中大量使用。
Scott Whitlock

4
@Michael:直到您决定重命名该类中的那些属性之一,然后发现您的代码都会爆炸。如果您不需要与程序生成的内容保持可比性,那很好,但是我怀疑不是我们大多数人。
比利·奥尼尔

1
@比利,我不是要和你矛盾,我同意你的所作所为。虽然,您带来的示例是恕我直言,但它还是一般规则“避免更改公共接口”的一个子案例。
彼得Török

37

这不是在作弊。但这至少在生产代码中是个坏主意,至少由于以下原因:

  • 您将失去编译时类型的安全性 -让编译器验证在编译时可用的方法会很有帮助。如果使用反射,则在运行时会收到错误消息,如果测试不够充分,可能会影响最终用户。即使您确实捕获到错误,调试起来也会更加困难。
  • 重构时会导致错误 -如果您基于成员的名称访问成员(例如,使用硬编码的字符串),那么大多数代码重构工具都不会对此进行更改,并且您会立即发现一个错误,这可能是相当严重的。很难追踪。
  • 性能较慢 -运行时的反射将比静态编译的方法调用/变量查找慢。如果您只是偶尔进行反射,那没关系,但是在每秒通过反射进行呼叫数千次或数百万次的情况下,这可能成为性能瓶颈。通过消除所有反射,我曾经在某些Clojure代码中获得了10倍的加速,所以是的,这是一个真正的问题。

我建议将反射的使用限制在以下情况下:

  • 当它是最简单的解决方案时,用于快速原型设计或“一次性”代码
  • 对于真正的反射用例,例如IDE工具,允许用户在运行时检查任意对象的字段/方法。

在所有其他情况下,我建议找出一种避免反射的方法。使用适当的方法定义接口并在要调用该方法的类集上实现它通常足以解决大多数简单情况。


3
+1-有几个用例可以进行反思;但是大多数时候,我看到程序员在使用它,他们正在为存在严重缺陷的设计做文章。定义接口并尝试消除对反射的依赖。就像GOTO一样,它也有用途,但大多数用途都不佳。(当然,其中有些很好)
Billy ONeal

3
请记住,以上几点可能适用于或可能不适用于您喜欢的语言。例如,反射在Smalltalk中没有速度损失,也不会失去编译时安全性,因为计算完全是后期约束。
2011年

D中的反射没有您提到的缺点。因此,我认为您将更多语言归咎于某些语言的实现。我对Smalltalk不太了解,但是根据@Frank Shearar所说,它也没有那些缺点。
deadalnix

2
@deadalinx:您指的是哪一方面?这个答案有几个。性能问题是唯一取决于语言的问题。
Billy ONeal

1
我应该补充一点,第二个问题-重构期间引起的错误-可能是最严重的问题。特别是,当名称更改时,对具有特定名称的东西的依赖会立即中断。除非您非常小心,否则由此引起的错误可能不足以提供信息。
2011年

11

反射只是元编程的另一种形式,并且与当今大多数语言中基于类型的参数一样有效。反射功能强大且通用,反射程序是高度可维护性的(当然,如果正确使用的话)比纯面向对象程序或过程程序更重要。是的,您为性能付出了代价,但是我很乐意选择一个速度较慢的程序,该程序在很多甚至大多数情况下都可以维护。


2
+1,这有助于我了解思考。我是C ++程序员,所以我从来没有真正思考过。这会有所帮助。(尽管我承认,从我的观点来看,Reflection似乎是一种调和语言缺陷的尝试。我们C ++的人为此使用模板。所以我想您也可以这么说。:)
greyfade 2011年

4
重新性能-在某些情况下,您可以使用反射API来写回完整的性能代码。例如,在.NET中,您可以将IL写入当前应用程序-因此您的“反射”代码可以与任何其他代码一样快(除了您可以删除很多if / else / whatever之外,就像您已经删除的一样)弄清楚确切的规则)
Marc Gravell

@greyfade:从某种意义上说,模板只是在进行编译时反射。能够在运行时做到这一点的优势在于,无需重新编译即可在应用程序中构建强大的功能。您以性能为代价来换取灵活性,这是比您预期的时间要多得多的折衷(考虑C ++背景)。
Donal Fellows,

我不明白你的回答。您似乎在说使用反射的程序比不使用反射的程序更具可维护性,就好像反射是一种高级的编程模型,如果不需要性能,它应该取代面向对象和过程式程序。
DavidS

8

当然,这完全取决于您要实现的目标。

例如,我编写了一个媒体检查器应用程序,该应用程序使用依赖项注入来确定要检查的媒体类型(MP3文件或JPEG文件)。该外壳需要显示一个网格,其中包含每种类型的相关信息,但是它不知道要显示什么。这是在读取该类型媒体的程序集中定义的。

因此,我必须使用反射来获取要显示的列数及其类型和名称,以便可以正确设置网格。这也意味着我可以在不更改任何其他代码或配置文件的情况下更新注入的库(或创建一个新库)。

唯一的其他方法是拥有一个配置文件,当我切换要检查的媒体类型时,该文件需要更新。这将为应用程序带来另一个故障点。


1
“当然,这完全取决于您要实现的目标。” +1
DaveShaw

7

如果您是图书馆作者,那么反射是一个了不起的工具,因此对传入的数据没有影响。反射和元编程的结合可以使您的库与任意调用者无缝协作,而无需他们跳过代码生成等方面。

但是,我确实不鼓励在应用程序代码中进行反思。在应用程序层,您应该使用不同的隐喻-接口,抽象,封装等。


这样的库通常很难使用,最好避免使用(例如Spring)。
Sridhar Sarnobat,

@Sridhar,所以您从未使用过任何序列化API?还是任何ORM / micro-ORM?
马克·格雷夫

我没有选择就使用了它们。如果我不被告知该怎么做,那我本可以避免的问题令人沮丧。
Sridhar Sarnobat

7

反射对于为开发人员构建工具而言是极好的。

因为它允许您的构建环境检查代码,并可能生成正确的工具来操纵/初始化检查代码。

作为一种通用的编程技术,它可能有用,但比大多数人想象的要脆弱。

反射(IMO)真正用于开发的用途是,它使编写通用流库非常简单(只要您的类描述永不改变(然后它就变得非常脆弱))。


确实,春天“比大多数人想象的要脆。”
Sridhar Sarnobat

5

如果不加意识地使用反射,在OO语言中通常是有害的。

我已经记不清我在StackExchange网站上看到过多少个坏问题了,

  1. 使用的语言是OO
  2. 作者想找出传入的对象类型,然后调用其方法之一
  3. 作者拒绝接受反思是错误的技术,并指责指出这一点的任何人为“不知道如何帮助我”

典型的例子。

OO的重点是

  1. 您对知道如何使用事物本身来操纵结构/概念的函数进行分组。
  2. 在代码的任何一点上,您都应该对对象的类型有足够的了解,以了解对象具有哪些相关功能,并要求它调用这些功能的特定版本。
  3. 通过为每个函数指定正确的输入类型并让每个函数对相关知识的了解(或做得更多),可以确保上一点的真实性。

如果在代码中的任何一点,第2点对您已传递的对象无效,则其中一个或多个为true

  1. 输入错误的输入
  2. 您的代码结构不良
  3. 您不知道自己在做什么。

技能欠佳的开发人员根本不会理解这一点,他们认为可以在代码的任何部分传递任何内容,并从一组(硬编码的)可能性中进行所需的操作。这些白痴使用反射很多

对于OO语言,仅应在元活动(类加载器,依赖项注入等)中进行反射。在这些情况下,需要进行反思,因为您正在提供通用服务来协助对出于正当理由而一无所知的代码进行操作/配置。在几乎任何其他情况下,如果您需要反思,那么您就在做错事,您需要问自己为什么这段代码对传递给它的对象不足够了解。


3

在反射类的域定义明确的情况下,一种替代方法是使用反射以及其他元数据来生成代码,而不是在运行时使用反射。我使用FreeMarker / FMPP来完成;还有很多其他工具可供选择。这样做的好处是最终会产生易于调试的“真实”代码,等等。

根据情况,这可以帮助您更快地编写代码-或仅使大量代码膨胀。它避免了反射的缺点:

  • 失去编译时类型的安全性
  • 因重构而产生的错误
  • 性能较慢

之前提到。

如果反射感觉像是在作弊,那可能是因为您基于不确定性很大的猜测,而您的直觉警告您这是有风险的。确保提供一种方法来增强您自己的元数据反射中固有的元数据,您可以在其中描述您可能会遇到的现实类的所有怪癖和特殊情况。


2

它不是在作弊,但像其他任何工具一样,应将其用于要解决的问题。根据定义,反射使您可以通过代码检查和修改代码;如果那是您需要做的,那么反思就是完成这项工作的工具。反思全都与元代码有关:以代码为目标的代码(与以数据为目标的常规代码相反)。

良好的反射用法的一个示例是通用的Web服务接口类:一种典型的设计是将协议实现与有效负载功能分开。因此,您有一个T实现您的有效负载的类(我们称之为),而另一个实现了协议(P)的类。T非常简单:对于您要进行的每个调用,只需编写一个方法即可完成应有的功能。P但是,需要将Web服务调用映射到方法调用。使这种映射通用是可取的,因为它避免了冗余,并P具有很高的可重用性。反射提供了一种T在运行时检查类并基于P通过Web服务协议传递的字符串来调用其方法的方法,而无需任何编译时对类的了解T。使用“关于代码的代码”规则,可以认为类将类P中的代码T作为其数据的一部分。

然而。

反射还为您提供了解决语言类型系统限制的工具-理论上,您可以将所有参数作为type传递object,并通过反射调用其方法。Voilà,一种本来应该强制执行严格的静态键入规则的语言,现在的行为就像具有后期绑定的动态类型的语言一样,只是其语法要复杂得多。到目前为止,我所见过的这种模式的每个实例都是一个肮脏的hack,而且在语言的类型系统中总是有可能提供一种解决方案,并且在所有方面都将是更安全,更优雅,更高效的。

存在一些例外情况,例如可以将控件绑定到各种不相关类型的数据源的GUI控件。强制您的数据实现某个接口只是为了对它进行数据绑定是不现实的,而且程序员也没有为每种类型的数据源实现适配器。在这种情况下,使用反射来检测数据源的类型并调整数据绑定是一个更有用的选择。


2

我们在反射中遇到的一个问题是在混合中添加了混淆处理。所有类都获得新名称,然后突然按其名称加载类或函数停止工作。



1

反射是创建基于约定的系统的主要方法。我发现大多数MVC框架中都大量使用了它,这并不奇怪。它是ORM中的主要组成部分。很有可能,您已经每天都在使用它内置的组件。

这种用途的替代方案是配置,它有其自身的缺点。


0

反思可以实现实际上无法通过其他方式完成的工作。

例如,考虑如何优化此代码:

int PoorHash(char Operator, int seed, IEnumerable<int> values) {
    foreach (var v in values) {
        seed += 1;
        switch (char) {
            case '+': seed += v; break;
            case '^': seed ^= v; break;
            case '-': seed -= v; break;
            ...
        }
        seed *= 3;
    }
    return seed;
}

内部循环的中间有一个昂贵的测试提示,但是要提取它,需要为每个操作员重写一次循环。反射使我们能够获得与提取测试相当的性能,而无需重复执行多次循环(从而牺牲了可维护性)。只需动态生成并编译所需的循环即可。

实际上,我已经完成了此优化,尽管情况有些复杂,结果却是惊人的。性能提高了一个数量级,更少的代码行。

(注意:我最初尝试传递一个Func而不是一个char等效,它稍好一些,但不能达到近10倍的反射率。)


1
更好的优化方法是传递函子。JIT编译器可能会在运行时内联,并且比反射更易于维护。
比利·奥尼尔

这将更易于维护,我实际上先尝试过,但是速度不够快。JIT绝对没有内联它,可能是因为在我的情况下,要执行的操作还有第二个内部循环。
克雷格·吉德尼

另外,我指出您所拥有的实际上是自我修改的代码,并不是真正的反思。人们可以通过大多数支持反射的语言中的反射API来访问它,但是可以像不支持反射的语言那样简单地进行访问(例如,在C ++中是可能的)
Billy ONeal

我想避免使用标准的“结构平等”示例。反射对于自我修改代码不是必需的,但肯定会有所帮助。
Craig Gidney

并不是的。您可以使用非反射语言来做到这一点。
Billy ONeal

-2

它绝不是骗人的……相反,它使应用程序服务器能够使用用户选择的名称来运行用户创建的类,从而为用户提供了灵活性,而不是骗人。

而且,如果您想查看任何.class文件(在Java中)的代码,那么可以免费使用几个反编译器!


反编译不是反射功能。
比利·奥尼尔

是的,不是。但是我的意思是,如果您在使用反射时感觉作弊(它为您提供了Java中任何类的详细信息),那么您错了bcoz反射是一个非常有用的概念,并且您可以获取任何详细信息类是您所关心的,任何反编译器都可以轻松完成...
ANSHUL JAIN

2
在某些情况下,反思是一个有用的概念,是的。但是99.9%的时间我看到它被使用了,但是我发现它曾经被用来解决设计错误。
Billy ONeal
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.