Reflection如何不会导致代码味道?
我来自低级语言 – C ++是我编程的最高级别。
最近我遇到了Reflection,我无法理解它如何在没有代码气味的情况下使用。
在我看来,在运行时检查类/方法/函数的想法指出了devise上的一个缺陷 – 我认为大部分reflection(试图)解决的问题都可以用于多态或正确使用inheritance。
我错了吗? 我误解了Reflection的概念和用途吗?
我正在寻找一个很好的解释,何时使用reflection,其他解决scheme将失败或太繁琐的实施,以及何时不使用它。
请指教这个低级的笨蛋。
reflection是最常用的规避静态types系统,但它也有一些有趣的用例:
我们来写一个ORM!
如果您熟悉NHibernate或大多数其他ORM,那么您可以编写映射到数据库中的表的类,如下所示:
// used to hook into the ORMs innards public class ActiveRecordBase { public void Save(); } public class User : ActiveRecordBase { public int ID { get; set; } public string UserName { get; set; } // ... }
你如何看待Save()
方法? 那么,在大多数ORM中,Save方法并不知道派生类中有哪些字段,但是可以使用reflection来访问它们。
它完全可能以types安全的方式具有相同的function,只需要用户重写一个将字段复制到数据行对象的方法,但这会导致大量的样板代码和膨胀。
存根!
犀牛嘲笑是一个嘲弄的框架。 你将一个接口types传递给一个方法,在后台框架将dynamic地构造和实例化一个实现接口的模拟对象。
当然,程序员可以手动为模拟对象编写样板代码,但是为什么她想要如果框架能为她做呢?
元数据!
我们可以用属性(元数据)来装饰方法,这可以用于多种目的:
[FilePermission(Context.AllAccess)] // writes things to a file [Logging(LogMethod.None)] // logger doesn't log this method [MethodAccessSecurity(Role="Admin")] // user must be in "Admin" group to invoke method [Validation(ValidationType.NotNull, "reportName")] // throws exception if reportName is null public void RunDailyReports(string reportName) { ... }
您需要反思检查属性的方法。 .NET的大多数AOP框架都使用属性来进行策略注入。
当然,你可以直接写同样的代码,但是这种风格更具说明性。
让我们来创build一个依赖框架!
许多IoC容器需要一定程度的reflection才能正常运行。 例如:
public class FileValidator { public FileValidator(ILogger logger) { ... } } // client code var validator = IoC.Resolve<FileValidator>();
我们的IoC容器将实例化一个文件validation器,并将相应的ILogger实现传递给构造函数。 哪个实现? 这取决于如何实施。
假设我在configuration文件中给出了程序集和类的名称。 该语言需要读取类的名称作为string,并使用reflection来实例化它。
除非我们知道编译时的实现,否则就没有types安全的方法来根据名字实例化一个类。
晚绑定/鸭打字
有各种各样的理由你想要在运行时读取对象的属性。 我会select日志作为最简单的用例 – 假设你正在写一个接受任何对象的logging器,并将其所有属性吐出到一个文件中。
public static void Log(string msg, object state) { ... }
您可以覆盖所有可能的静态types的Log方法,或者您可以使用reflection来读取属性。
像OCaml和Scala这样的语言支持静态检查鸭式打字(称为结构打字 ),但是有时候你只是没有对象界面的编译时知识。
或者像Java程序员知道的那样,有时候types系统会自行设置,并要求你编写各种样板代码。 有一篇着名的文章描述了dynamic打字简化了多lessdevise模式。
偶尔绕过types系统允许你比静态types更可能重构你的代码,导致代码更简洁一些(最好隐藏在程序员友好的API后面)。 许多现代的静态语言都采用了“尽可能静态input,必要时dynamicinput”的黄金法则,允许用户在静态和dynamic代码之间切换。
如果没有Reflection,那么像hibernate (O / R映射)和StructureMap (dependency injection)这样的项目是不可能的。 如何单独用多态性解决这些问题?
是什么让这些问题如此难以用其他方式来解决,就是图书馆不能直接了解你的class级结构 – 他们不能。 然而他们需要知道你的类的结构,例如,只需要使用字段的名称和属性的名称,就可以将任意一行数据从数据库映射到类中的某个属性。
reflection对于映射问题特别有用。 约定代码的思想变得越来越stream行,你需要一些types的reflection来做到这一点。
在.NET 3.5+中,你有一个替代scheme,就是使用expression式树。 这些是强types的,使用reflection的经典解决的许多问题已经使用lambdas和expression式树(请参阅Fluent NHibernate , Ninject )重新实现。 但请记住,并不是每种语言都支持这种构造。 当他们不可用时,你基本上被reflection。
在某种程度上(我希望我不会用这种方式吹毛求疵),reflection经常被用作面向对象语言的一种解决方法,用于function语言中免费提供的function。 随着function语言变得越来越stream行,和/或更多的OO语言开始实现更多的function特性(比如C#),我们很可能会开始看到reflection越来越less。 但是我怀疑它会一直存在,因为像插件这样的更传统的应用程序(正如其他响应者所指出的那样)。
其实, 你已经每天都在使用reflection系统 :你的电脑。
当然,它不是类,方法和对象,而是程序和文件。 程序像创build和修改对象一样创build和修改文件。 但是,程序本身就是文件,有些程序甚至检查或创build其他程序!
那么,为什么一个Linux安装可以reflection回来,为什么没有人想到这个问题呢,而且对于面向对象程序来说也是可怕的。
我已经看到自定义属性的良好用法。 比如一个数据库框架。
[DatabaseColumn("UserID")] [PrimaryKey] public Int32 UserID { get; set; }
然后可以使用reflection来获得有关这些字段的更多信息。 我很确定LINQ To SQL做了类似的事情…
其他的例子包括testing框架…
[Test] public void TestSomething() { Assert.AreEqual(5, 10); }
没有反思,你经常不得不重复自己。
考虑这些情况:
- 在一个testing用例中运行一组方法,例如testXXX()方法
- 在gui构build器中生成属性列表
- 让你的课程可以脚本化
- 实施序列化scheme
通常不能在C / C ++中做这些事情,而不必在代码中重复其他地方的受影响方法和属性的整个列表。
实际上C / C ++程序员经常使用接口描述语言在运行时暴露接口(提供一种reflectionforms)。
明智地使用reflection和注释结合明确的编码规则可以避免代码重复泛滥,增加可维护性。
我认为反思是这些强大的机制之一,但可以很容易地被滥用。 你被赋予了为特定目的而成为“超级用户”的工具,但这并不意味着要取代适当的面向对象的devise(就像面向对象的devise不是解决所有问题的scheme),或者被轻率地使用。
由于Java结构化的方式,你已经在运行时支付了在内存中代表你的类层次结构的代价(相比于C ++,除非你使用类似虚拟方法的东西,否则你不需要支付任何代价)。 因此,没有成本的理由来充分阻止它。
reflection对像序列化这样的东西很有用 – 像Hibernate或沼气池这样的东西可以用来决定如何最好地自动存储对象。 同样,JavaBeans模型基于方法的名称(我承认这是一个值得怀疑的决定),但是您需要能够检查构build可视化编辑器的可用属性。 在最新版本的Java中,reflection是使注释有用的东西 – 您可以使用源代码中存在但可以在运行时访问的实体编写工具和元编程。
作为一名Java程序员,整个职业生涯都是可能的,而且不必使用反思,因为你所处理的问题并不需要它。 另一方面,对于某些问题,这是非常必要的。
如上所述,reflection主要用于实现需要处理任意对象的代码。 例如,ORM映射器需要从用户定义的类中实例化对象,并使用数据库行中的值填充它们。 最简单的方法是通过反思。
其实,你是部分正确的,reflection往往是一种代码味道。 大多数时候你和你的class级一起工作,不需要反思,如果你知道你的types,你可能会牺牲安全性,性能,可读性和世界上所有好的东西,毫无用处。 但是,如果你正在编写库,框架或通用工具,你可能会遇到最好的reflection情况。
这是在Java中,这是我所熟悉的。 其他语言提供的东西可以用来实现相同的目标,但在Java中,reflection具有明确的应用程序,因此它是最好的(有时是唯一的)解决scheme。
像NUnit这样的unit testing软件和框架使用reflection来获取要执行的testing列表并执行它们。 他们在一个模块/程序集/二进制文件中find所有的testing套件(在C#中,这些代码都是由类表示的),这些套件中的所有testing(在C#中都是类中的方法)。 如果您正在testingexception合同,NUnit还允许您使用期望的exception标记testing。
没有反思,你需要指定什么样的testing套件可用以及每个套件中有哪些testing可用。 而且,例外情况也需要手动testing。 我见过的C ++unit testing框架已经使用macros来做到这一点,但有些东西仍然是手动的,这种devise是有限制的。
保罗·格雷厄姆(Paul Graham)有一篇很好的文章可以说是最好的:
程序写程序? 你什么时候想要这样做? 如果你在Cobol中想,不是很常见。 所有的时间,如果你认为在Lisp。 如果我能举一个强大的macros的例子,那么在这里很方便,并且在那里说! 那个怎么样? 但是如果我这样做了,对于一个不懂Lisp的人来说,看起来就像是胡言乱语; 这里没有空间来解释你需要知道的一切。 在Ansi Common Lisp中,我尝试尽可能快地移动,甚至直到第160页才得到macros。
以…结束。 。 。
多年来,我们在Viaweb上工作,阅读了很多职位描述。 每个月左右,新的竞争对手似乎都脱颖而出。 看看他们是否有在线演示,我会做的第一件事是看他们的工作列表。 几年后,我可以告诉哪些公司担心,哪些不是。 职位描述所具有的IT风格越多,公司的危险就越小。 最安全的是那些想要Oracle体验的人。 你从来不必担心这些。 如果他们说他们想要C ++或Java开发人员,你也是安全的。 如果他们想要Perl或者Python程序员,那将会有点可怕 – 这听起来像是一个公司,至less技术方面是由真正的黑客运营的。 如果我曾经看过一个招聘Lisp黑客的工作,我真的很担心。
这就是快速发展。
var myObject = // Something with quite a few properties. var props = new Dictionary<string, object>(); foreach (var prop in myObject.GetType().GetProperties()) { props.Add(prop.Name, prop.GetValue(myObject, null); }
插件是一个很好的例子。
工具是另一个例子 – 检查工具,构build工具等
我会举一个我开始学习时给出的ac#解决scheme的例子。
它包含标有[Exercise]属性的类,每个类包含未实现的方法(抛出NotImplementedException)。 解决scheme也有unit testing,都失败了。
目标是实现所有的方法,并通过所有的unit testing。
该解决scheme还有一个用户界面,它将读取标有Excercise的所有类,并使用reflection来生成用户界面。
后来我们被要求实现我们自己的方法,后来还要了解用户界面是如何“神奇地”改变的,包括我们实现的所有新方法。
非常有用,但往往不很好理解。
这背后的想法是能够查询任何GUI对象的属性,以在GUI中提供它们进行自定义和预configuration。 现在它的用途已经被扩展,并被certificate是可行的。
编辑:拼写
这对dependency injection非常有用。 您可以探索使用给定属性实现给定接口的加载程序集types。 结合适当的configuration文件,certificate这是一个非常强大和干净的方式来添加新的inheritance类而不修改客户端代码。
另外,如果你正在做一个并不关心底层模型的编辑器,而是直接构造对象,那么System.Forms.PropertyGrid
)
没有反思,没有插件架构将工作!
Python中非常简单的例子。 假设你有一个有三种方法的类:
class SomeClass(object): def methodA(self): # some code def methodB(self): # some code def methodC(self): # some code
现在,在其他一些类中,您希望用一些额外的行为来修饰这些方法(即,您希望该类模仿SomeClass,但具有附加function)。 这很简单:
class SomeOtherClass(object): def __getattr__(self, attr_name): # do something nice and then call method that caller requested getattr(self.someclass_instance, attr_name)()
通过reflection,您可以编写less量不依赖于域的代码,而不需要经常更改,而不需要经常更改(例如,添加/删除属性时)更多依赖于域的代码。 通过在项目中build立的约定,您可以基于某些属性,属性等的存在执行常见function。不同域之间的对象数据转换是reflection真正派上用场的一个例子。
或者是一个更简单的例子,你可以把数据从数据库转换成数据对象,而不必在属性改变的时候修改转换代码,只要维护约定(在这种情况下匹配属性名称和特定的属性) :
///-------------------------------------------------------------------------------- /// <summary>Transform data from the input data reader into the output object. Each /// element to be transformed must have the DataElement attribute associated with /// it.</summary> /// /// <param name="inputReader">The database reader with the input data.</param> /// <param name="outputObject">The output object to be populated with the input data.</param> /// <param name="filterElements">Data elements to filter out of the transformation.</param> ///-------------------------------------------------------------------------------- public static void TransformDataFromDbReader(DbDataReader inputReader, IDataObject outputObject, NameObjectCollection filterElements) { try { // add all public properties with the DataElement attribute to the output object foreach (PropertyInfo loopInfo in outputObject.GetType().GetProperties()) { foreach (object loopAttribute in loopInfo.GetCustomAttributes(true)) { if (loopAttribute is DataElementAttribute) { // get name of property to transform string transformName = DataHelper.GetString(((DataElementAttribute)loopAttribute).ElementName).Trim().ToLower(); if (transformName == String.Empty) { transformName = loopInfo.Name.Trim().ToLower(); } // do transform if not in filter field list if (filterElements == null || DataHelper.GetString(filterElements[transformName]) == String.Empty) { for (int i = 0; i < inputReader.FieldCount; i++) { if (inputReader.GetName(i).Trim().ToLower() == transformName) { // set value, based on system type loopInfo.SetValue(outputObject, DataHelper.GetValueFromSystemType(inputReader[i], loopInfo.PropertyType.UnderlyingSystemType.FullName, false), null); } } } } } } // add all fields with the DataElement attribute to the output object foreach (FieldInfo loopInfo in outputObject.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance)) { foreach (object loopAttribute in loopInfo.GetCustomAttributes(true)) { if (loopAttribute is DataElementAttribute) { // get name of field to transform string transformName = DataHelper.GetString(((DataElementAttribute)loopAttribute).ElementName).Trim().ToLower(); if (transformName == String.Empty) { transformName = loopInfo.Name.Trim().ToLower(); } // do transform if not in filter field list if (filterElements == null || DataHelper.GetString(filterElements[transformName]) == String.Empty) { for (int i = 0; i < inputReader.FieldCount; i++) { if (inputReader.GetName(i).Trim().ToLower() == transformName) { // set value, based on system type loopInfo.SetValue(outputObject, DataHelper.GetValueFromSystemType(inputReader[i], loopInfo.FieldType.UnderlyingSystemType.FullName, false)); } } } } } } } catch (Exception ex) { bool reThrow = ExceptionHandler.HandleException(ex); if (reThrow) throw; } }
一个用法还没有提到:虽然reflection通常被认为是“慢”,但是可以使用reflection来提高使用接口的代码的效率,比如IEquatable<T>
存在时使用接口,并且使用其他方法检查等式不。 在没有reflection的情况下,想要testing两个对象是否相等的代码将不得不使用Object.Equals(Object)
,否则在运行时检查一个对象是否实现了IEquatable<T>
,如果是,则将该对象到那个界面。 在任何一种情况下,如果被比较的东西的types是一个值types,至less需要一个装箱操作。 使用reflection可以让一个类EqualityComparer<T>
为任何特定的typesT
自动构造一个特定types的IEqualityComparer<T>
实现,该实现使用IEquatable<T>
如果已经定义),或者使用Object.Equals(Object)
如果不是。 对于任何特定typesT
,第一次使用EqualityComparer<T>.Default
,系统将不得不经过比testing所需要的更多的工作,一次是否一个特定types实现了IEquatable<T>
。 另一方面,一旦完成了这个工作,就不会再需要运行时types检查了,因为系统将为所讨论的types生成一个EqualityComparer<T>
的定制实现。