属性是现代语言的一个非常好的特性,我最近在Kotlin中对它们产生了浓厚的兴趣。有一段时间,我对使用字段执行任意计算的想法感到不舒服,但在使用了几个月之后,我真的很享受它的简单性--特别是当我搬回Java的时候!
经过深思熟虑后,我想给出一个支持属性的案例,它强调了属性相对于直接访问字段的好处(主要是关于getter/setter,但尽管如此),以及我们如何才能建立一些关于何时不应该使用属性的指导方针,以避免导致副作用或其他意外行为的任意计算的问题。
简而言之,属性是看起来像字段的getter/setter方法的语法糖。无需使用方法item.getQuantity()和item.setQuantity(),我们可以访问属性item.Quantity并使用item.Quantity=1设置它,这与字段完全一样。编译器转换属性访问和赋值,以使用适当的getter/setter(因此,语法糖化)。
在我看来,这最值得注意的后果是,允许在分配时进行验证。例如,item.Quantity可以定义如下(以科特林为单位):
类项目{var Quantity:int=set(Value){Required(value>;=)//如果负字段=value}则抛出}。
属性的最大好处是用方法取代了具有大量缺点的现场使用。在C++和Java等没有属性的语言中,最佳实践是让所有字段保持私有,并手动创建getter和setter。更多的现代语言,如C#和Kotlin,使用属性来代替,这些属性可以自动完成。
下面的大多数要点也是使用getter/setter而不是字段的好处,然后属性介入提供额外的语法糖霜。
属性getter/setter可以有不同的访问修饰符,允许外部使用只读但在类内仍然可变的字段。例如:在Kotlin中,val的计数器=0x0定义了一个可以公开访问但不能修改的计数器域(除非在类中)。
Getters/setter可用于维护不变量和执行其他验证工作,例如开头示例中的数量为1>;=0。
因为getters/setter是方法,所以可以在不破坏API或ABI兼容性的情况下更改实现。例如:Vector类可以有x/y/z属性,但更改实现以使用组件数组[x,y,z]而不会出现问题。
此外,可以在优化期间内联getter/setter以牺牲ABI兼容性来提高性能。
属性可以在接口中使用,因为它们由方法支持,而字段需要为它们定义getter/setter。值得注意的是,这些getter/setter仍然需要在实现类中定义字段来处理它们的值,而属性可以自动处理。
属性可用于通常行为类似于字段的方法,但由于子类型需要计算或其他限制而无法使用字段实现。例如:list#size()通常不需要计算并返回字段的值,但有些实现(如cons list)可能会。
List#size()是否应该成为一个属性当然是有争议的,但考虑到集合的使用频率,我认为这几乎是必要的。
属性是一个单一的单元。在源代码中,getter/setter与属性声明位于相同的位置,与在不同的位置定义它们相比,更容易完全理解一个字段。
属性几乎总是使用语法来提供默认的get/set实现(如果它还没有自动完成的话)。在Kotlin中,val属性提供默认的getter,而var属性同时提供getter和setter。
使用getters/setters可以在没有专用调试器的情况下调试属性。对于大型语言来说,这可能是次要的,但如果您的语言没有IDE或仍在开发中,这将非常有用。
尽管有争议,但可读性往往会提高。我个人认为item.Quantity=n1比item.setQuantity(1)更具可读性,尤其是在大型连锁店中。
通常,属性暗示简单的行为,就像字段一样,而不是某些方法可能执行的复杂计算。我们稍后将详细讨论这个问题。
我认为拥有房产有两个值得注意的缺点。首先,字段访问和方法调用之间的性能差异可能很大,尤其是在低级语言中。其次,字段访问具有清晰、定义良好的行为,而方法调用可以有效地执行任何操作,因此更难对代码进行推理。让我们深入研究这两个问题。
字段访问几乎总是语言原始的,因此比执行方法调用(不管怎样,方法调用必须访问某些内容)的工作更快。
也就是说,像内联这样的编译器优化可以很容易地解决简单属性的这个问题。在更复杂的情况下,这实际上是属性最有用的地方--一个字段本身是不够的,所以另一种选择就是调用方法或牺牲验证。
在内存布局通常很重要的低级语言中,了解字段访问对应于内存查找(而不是任意计算)可能很重要。在更高级的语言中,尤其是那些经常将字段封装在方法后面的面向对象语言中,我不认为这种区别是必要的。
最后,我们谈到了我认为最核心的问题:因为属性可以执行任意计算,所以还不清楚获取/设置属性可能会有什么潜在行为。当然,这里最极端的例子是删除整个文件系统。另一方面,字段有明确定义的行为-获取或设置值,仅此而已。
然而,这些类型的假设并不是属性所特有的。我们也将同样的假设应用于方法(老实说,也适用于所有方法)。我经常假设getX()和setx(X)具有简单的行为,但它们可能使用懒惰的初始化、意外的验证检查或导致副作用和其他意外行为,如数据库getConnection()。命名是计算机科学中的两个难题之一,这可能是因为我们随身携带的假设可能并不总是合适的。
也就是说,这些假设仍然有价值,可以帮助我们在不必记住整个系统的API的情况下对代码进行推理。因此,我思考了我对房产的假设,以及可以制定什么样的指导方针来限制它们应该做什么。我没有足够的信心声称这些是规则(目前!),我欢迎这些指导方针没有涉及的任何反例。
属性应该是纯属性,不包括由属性本身管理的状态。这旨在允许延迟初始化,并根据需要设置后备字段或委托属性,但会限制修改无关状态或执行其他副作用。
接下来,让我们重新访问getConnection--这种方法不仅会带来副作用,而且如果数据库不可用(以及其他),也会失败。我认为这不只是一个属性,因为它正在做调用者可能需要恢复的计算工作(这里强调)。因此,下一个指导方针是……。
属性不应导致错误(不包括不可恢复的错误(故障))。这包括状态/参数验证和潜在的内部错误。
属性应用于摊销的常量时间操作,但不包括实现无法支持的继承属性。这是为了避免复杂的计算,同时仍然允许对诸如cons list之类的东西进行延迟初始化和list.size。(不过,如果缓存的话,可以摊销)。
这当然是为适应这些情况而设计的,但我认为这是我现在能给出的最好的解释,解释为什么list.size作为一个属性是可以接受的,而像list.sorted()这样的属性可能不是。可能还有一些我还没有想到的奇怪情况,比如文件。如果返回一个序列(惰性计算),行是可以的,但是列表结果可能不是一个好的选择。
牢记这些指导原则,我认为研究一些我认为非常有效的属性的示例用例会很有用。这两个Showcase属性都用于提供数据的不同视图,这些视图可以比通过方法更有效地使用。
此示例使用属性处理RGB十六进制颜色中的单个红色、蓝色和绿色组件。这些都是虚拟属性,因此内存中存储的唯一数据是十六进制值本身。可以对标志使用类似的方法。
数据类RgbColor(var hex:int){var red:int get()=he.and().shr()set(Value){Required(value in..。)。Hex=he.and().or(value.shl())}变量蓝:int=...。变量GREEN:INT=...}。
此示例更进一步,使用属性返回新对象。下面的示例使用了两次属性:第一次用于提供映射值的视图作为集合<;Int>;,第二次用于提供可用于改变原始映射的另一个视图。
VAL MAP=mapOf(";x&34;to,";y&34;to,";z&34;to);映射。价值观。Mutable e.map{it*it};println(Map);
出于某种原因,Kotlin的标准库不能做到这一点,所以如果你亲自尝试,上面的代码自然不会编译。也许有一天在罗瓦斯?
对我来说,使用属性对map.values.mutable.map进行此操作要比使用方法(map.values.mutable().map:{...})更清楚,因为方法调用丢失了原始映射和末尾可变视图之间的连接。虽然Kotlin确实支持map.mapValues和相关内容,但是需要为键和条目复制这些内容,并且可能会缺少其他有用的功能。也许这不是你应该做的事,但我认为这是一个有趣的想法,而且在上面的例子中效果足够好。
我真的很喜欢使用现代语言中的属性,我希望这能提供一个公平的案例,说明为什么当使用得当时,它们可以成为一种很好的语言功能(没有双关语的意思)。
简而言之,与字段相比,属性在正确性、语义能力和可用性方面提供了大量好处,而缺点相对较少。我们为避免属性出现意外行为而制定的准则如下:
你可以随时提出问题或评论,我很想听听大家对上述指南的反馈,以及这篇文章是否影响了你对房产的看法。