代码之家  ›  专栏  ›  技术社区  ›  LiraNuna

反射怎么可能不会导致代码气味?

  •  63
  • LiraNuna  · 技术社区  · 14 年前

    我来自低级语言——C++是最高级别的I程序。

    最近我遇到了反射,我只是无法理解没有代码气味如何使用它。

    在我看来,在运行时检查类/方法/函数的想法指出了设计中的一个缺陷——我认为反射(尝试)解决的大多数问题都可以用于多态性或继承的正确使用。

    我错了吗?我是否误解了反思的概念和效用?

    我正在寻找一个很好的解释,在其他解决方案将失败或过于繁琐而无法实施以及何时不使用它的情况下,何时使用反射。

    请打开这个低级润滑油。

    18 回复  |  直到 12 年前
        1
  •  90
  •   Juliet    14 年前

    反射最常用于绕过静态类型系统,但是它也有一些有趣的用例:

    让我们写一个ORM!

    如果您熟悉nhibernate或大多数其他表单,您可以编写映射到数据库中表的类,如下所示:

    // 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() 方法是否已写入?在大多数窗体中,save方法不知道派生类中有哪些字段,但它可以使用反射访问这些字段。

    它的 完全地 可以以类型安全的方式拥有相同的功能,只需要求用户重写一个方法,将字段复制到数据行对象中,但这将导致大量样板代码和膨胀。

    树桩!

    Rhino Mocks 是一个模拟框架。您将一个接口类型传递给一个方法,在后台,框架将动态地构造和实例化一个实现接口的模拟对象。

    当然,程序员可以手工编写模拟对象的样板代码,但是如果框架为她做这件事,她为什么要这样做呢?

    元数据!

    我们可以用属性(元数据)来修饰方法,这些属性(元数据)可以用于多种用途:

    [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) { ... }
    

    您需要对方法进行反射以检查属性。大多数 AOP frameworks for .NET 使用属性进行策略注入。

    当然,您可以在内联中编写相同类型的代码,但是这种样式更具声明性。

    让我们建立一个依赖框架!

    许多IOC容器需要一定程度的反射才能正常运行。例如:

    public class FileValidator
    {
        public FileValidator(ILogger logger) { ... }
    }
    
    // client code
    var validator = IoC.Resolve<FileValidator>();
    

    我们的IOC容器将实例化一个文件验证器,并将ILogger的适当实现传递给构造函数。哪些实施?这取决于它是如何实现的。

    假设我在配置文件中给出了程序集和类的名称。语言需要将类的名称作为字符串读取,并使用反射来实例化它。

    除非我们在编译时知道实现,否则没有基于类名称实例化类的类型安全方法。

    后期装订/鸭子打字

    您希望在运行时读取对象属性的原因有很多种。我选择日志记录作为最简单的用例——假设您正在编写一个日志记录程序,它接受任何对象并将其所有属性吐出到一个文件中。

    public static void Log(string msg, object state) { ... }
    

    能够 为所有可能的静态类型重写Log方法,或者只使用反射来读取属性。

    一些语言,如ocaml和scala支持静态检查的duck类型(称为 structural typing ,但有时您只是没有对象接口的编译时知识。

    或者正如Java程序员所知道的,有时类型系统会得到你的方式,并要求你编写各种样板代码。有一篇著名的文章描述了有多少种设计模式 simplified with dynamic typing .

    有时,绕过类型系统可以让您将代码重构得比静态类型更深入,从而使代码更干净(最好隐藏在程序员友好的API之后)。许多现代静态语言都采用了“可能的静态类型,必要的动态类型”的黄金法则,允许用户在静态代码和动态代码之间切换。

        2
  •  11
  •   Aaronaught    14 年前

    项目,例如 hibernate (O/R映射)和 StructureMap (依赖注入)没有反射是不可能的。如何单独使用多态性来解决这些问题?

    使这些问题如此难以解决的是图书馆 直接地 了解关于类层次结构的任何信息—它们不知道。但是它们需要了解类的结构,以便(例如)仅使用字段名和属性名将数据库中的任意数据行映射到类中的属性。

    反射对 映射 问题。理念 convention over code 变得越来越流行,你需要某种类型的思考来做到这一点。

    在.NET 3.5+中,您有一种选择,即使用表达式树。这些都是强类型的,并且使用lambda和表达式树(参见 Fluent NHibernate , Ninject )但请记住,并不是每种语言都支持这些类型的构造;当它们不可用时,您基本上会陷入反射。

    在某种程度上(我希望我不会对此感到恼火),反射经常被用作面向对象语言中的解决方法/黑客,用于功能语言中免费提供的功能。随着功能语言越来越流行,和/或更多的OO语言开始实现更多的功能特性(如C),我们很可能会看到反射越来越少地被使用。但我怀疑它仍然存在,对于更传统的应用程序,比如插件(正如其他响应者之一乐于指出的那样)。

        3
  •  8
  •   Damien Pollet    14 年前

    事实上, 你每天都在使用反光系统 :您的计算机。

    当然,它没有类、方法和对象,而是有程序和文件。程序创建和修改文件就像方法创建和修改对象一样。但是程序 文件本身,一些程序甚至检查或创建 其他程序!

    那么,为什么Linux的安装是自反的,以至于没有人会考虑它,对OO程序来说是可怕的呢?

        4
  •  6
  •   user110714    14 年前

    我见过自定义属性的良好用法。例如数据库框架。

    [DatabaseColumn("UserID")]
    [PrimaryKey]
    public Int32 UserID { get; set; }
    

    然后可以使用反射来获得关于这些字段的进一步信息。我很确定Linq to SQL做了类似的事情…

    其他示例包括测试框架…

    [Test]
    public void TestSomething()
    {
        Assert.AreEqual(5, 10);
    }
    
        5
  •  6
  •   Alex Jasmin    14 年前

    没有反省,你经常要重复很多。

    考虑以下情况:

    • 在测试用例中运行一组方法,例如testxxx()方法
    • 在GUI生成器中生成属性列表
    • 使类可编写脚本
    • 实现序列化方案

    通常不能在C/C++中做这些事情,而不重复代码中其他地方的受影响的方法和属性的整个列表。

    事实上,C/C++程序员经常使用 Interface description language 在运行时公开接口(提供反射形式)。

    明智地将反射和注释与定义良好的编码约定结合使用,可以避免代码重复,提高可维护性。

        6
  •  3
  •   Uri    14 年前

    我认为反射是这些机制中的一种,它很强大,但很容易被滥用。为达到特定的目的,你被赋予了成为“超级用户”的工具,但这并不意味着取代正确的面向对象设计(就像面向对象设计不是解决所有问题的解决方案一样),也不意味着被轻率地使用。

    由于Java的结构方式,在运行时,您已经付出了在内存中代表类层次结构的代价(与C++相比,除非使用虚拟方法之类的东西,否则不需要付出任何代价)。因此,完全阻止它是没有成本依据的。

    反射对于序列化之类的事情很有用——Hibernate或Digester之类的事情可以使用反射来确定如何自动地最佳地存储对象。类似地,JavaBeans模型基于方法的名称(我承认这是一个可疑的决定),但是您需要能够检查哪些属性可用于构建可视化编辑器之类的东西。在Java的最新版本中,反射是使注释有用的工具。您可以使用源代码中存在的这些实体编写工具并进行元编程,但可以在运行时访问这些实体。

    作为Java程序员,有可能经历整个职业生涯,而不必使用反射,因为你处理的问题不需要它。另一方面,对于某些问题,这是非常必要的。

        7
  •  3
  •   alex    14 年前

    如上所述,反射主要用于实现需要处理任意对象的代码。例如,ORM映射器需要从用户定义的类中实例化对象,并用数据库行中的值填充它们。实现这一点的最简单方法是通过反射。

    实际上,你是对的,反思是 经常 代码气味。大多数时候,你在课堂上工作,不需要思考——如果你知道你的类型,你可能会牺牲类型的安全性、性能、可读性以及在这个世界上所有的好东西,这是不必要的。但是,如果您正在编写库、框架或通用实用程序,则可能会遇到使用反射进行最佳处理的情况。

    这是在Java中,这是我所熟悉的。其他语言提供的东西可以用来实现相同的目标,但在爪哇,反射有明确的应用,这是最好的(有时也是唯一)的解决方案。

        8
  •  3
  •   strager    14 年前

    单元测试软件和框架(如nunit)使用反射获取要执行和执行的测试列表。它们在一个模块/程序集/二进制文件中找到所有测试套件(在C中,这些套件由类表示)和这些套件中的所有测试(在C中,这些是类中的方法)。nunit还允许您标记一个带有预期异常的测试,以防您正在测试异常合同。

    如果没有反射,您需要以某种方式指定哪些测试套件可用,以及每个套件中哪些测试可用。此外,像异常这样的事情需要手动测试。我见过的C++单元测试框架使用宏来实现这一点,但是有些东西仍然是手工的,这种设计是有限制性的。

        9
  •  3
  •   klochner    14 年前

    保罗·格雷厄姆有一个 great essay 这也许是最好的说法:

    写程序的程序?什么时候? 你想这样做吗?不是 如果你用COBOL来思考的话,通常是这样。所有 时间,如果你用口齿不清的话。它 如果可以的话,在这里会很方便 举一个强大宏的例子, 然后说出来!那怎么样?但是如果 是的,看起来就像 对不认识的人胡说八道 口齿不清;这里没有解释的余地 你需要知道的一切 理解它的含义。在ANSI中 常见的口齿不清我试着移动东西 尽我所能,甚至如此 直到第160页我才接触到宏。

    结束语。…

    我们在Viaweb i工作的那些年里 阅读大量的工作描述。一个新的 竞争对手似乎从 每月左右做木工活。第一 我会做的事 看看他们是否有现场在线演示, 看看他们的工作清单。后 这几年我可以看出 哪些公司值得担心 哪个不可以。更多的IT味道 工作描述越少 公司很危险。最安全的 善良的是那些想要甲骨文的人 经验。你不用担心 关于那些。你也很安全如果 他们说他们想要C++或Java 开发人员。如果他们想要Perl或者 python程序员,那将是 有点吓人——开始 听起来像一家公司 至少技术方面是由 真正的黑客。如果我见过工作 发布寻找Lisp黑客,我 会很担心的。

        10
  •  2
  •   ChaosPandion    14 年前

    一切都是为了快速发展。

    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);
    }
    
        11
  •  2
  •   Cheeso    14 年前

    插件就是一个很好的例子。

    工具是另一个示例-检查器工具、构建工具等。

        12
  •  2
  •   Paul Creasey    14 年前

    我将举一个例子,说明在我开始学习时给出的C解决方案。

    它包含用[exercise]属性标记的类,每个类包含未实现的方法(引发NotImplementedException)。该解决方案还进行了单元测试,但都失败了。

    目标是实现所有方法并通过所有单元测试。

    该解决方案还具有一个用户界面,它可以读取所有标有excercise的类,并使用反射来生成用户界面。

    后来我们被要求实现我们自己的方法,后来我们仍然理解用户界面是如何“神奇地”被更改为包含我们实现的所有新方法的。

    非常有用,但往往不被很好地理解。

        13
  •  2
  •   stacker    14 年前

    其背后的想法是能够查询任何图形用户界面对象属性,并在图形用户界面中提供这些属性以进行定制和预配置。目前,它的应用已经得到了扩展,证明是可行的。

    编辑:拼写

        14
  •  1
  •   Coincoin    14 年前

    它对于依赖注入非常有用。您可以探索使用给定属性实现给定接口的已加载程序集类型。结合正确的配置文件,它被证明是在不修改客户机代码的情况下添加新继承类的一种非常强大和干净的方法。

    另外,如果您所做的编辑器并不真正关心底层模型,而是关注对象的直接结构,那么ala System.Forms.PropertyGrid )

        15
  •  1
  •   fastcodejava    14 年前

    没有反射,任何插件架构都无法工作!

        16
  •  1
  •   Tomasz Zieliński    14 年前

    python中非常简单的例子。假设有一个类有三个方法:

    class SomeClass(object):
        def methodA(self):
           # some code
        def methodB(self):
           # some code
        def methodC(self):
           # some code
    

    现在,在其他一些类中,您希望用一些额外的行为来修饰这些方法(也就是说,您希望该类模仿某个类,但具有额外的功能)。 这很简单:

    class SomeOtherClass(object):
        def __getattr__(self, attr_name):
            # do something nice and then call method that caller requested
            getattr(self.someclass_instance, attr_name)()
    
        17
  •  1
  •   Dave Clemmer manu    14 年前

    通过反射,您可以编写少量不需要经常更改的独立于域的代码,而不是编写更多需要更频繁更改的依赖域的代码(例如添加/删除属性时)。使用项目中已建立的约定,您可以基于某些属性、属性等的存在来执行公共函数。不同域之间对象的数据转换就是一个例子,其中反射非常有用。

    或者域中的一个更简单的示例,在该示例中,您希望在属性更改时将数据从数据库转换为数据对象,而不需要修改转换代码,只要保持约定(在本例中,匹配属性名和特定属性):

        ///--------------------------------------------------------------------------------
        /// <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;
            }
        }
    
        18
  •  1
  •   supercat    12 年前

    还没有提到的一个用法:虽然反射通常被认为是“慢的”,但是可以使用反射来提高使用类似接口的代码的效率 IEquatable<T> 当它们存在时,并在不存在时使用其他方法来检查相等性。在没有反射的情况下,要测试两个对象是否相等的代码必须使用 Object.Equals(Object) 否则检查 在运行时 对象是否实现 IEquatable<T> 如果是这样,则将对象强制转换到该接口。在这两种情况下,如果要比较的对象类型是值类型,则至少需要一个装箱操作。使用反射可以创建一个类 EqualityComparer<T> 自动构造特定于类型的 IEqualityComparer<T> 对于任何特定类型 T ,使用该实现 IEquatable<T> 如果定义了它,或使用 object.equals(对象) 如果不是。第一次使用 EqualityComparer<T>.Default 对于任何特定类型 T ,系统将需要进行比测试特定类型是否实现所需的更多的工作。 IEquatable<T> . 另一方面,一旦完成了这项工作,就不再需要运行时类型检查,因为系统已经生成了 均等比较器 对于相关类型。