Kotlin编译器如何判定val为属性或函数?val与var的深层差异
示例复现
先看问题对应的代码场景:
val nameHash get() = name.hashCode()
这段代码编译后的等价Java实现如下:
public final int getNameHash() { return name.hashCode(); }
编译产物里确实不存在nameHash对应的成员字段,只生成了getter方法。如果把修饰符从val改成var,编译器会直接抛出Property must be initialized错误。
先纠正一个常见误解
不管有没有生成对应的存储字段,val nameHash在Kotlin的语义里永远是属性,不是函数。你觉得它像函数,只是因为编译后没有分配存储字段,访问时会实时执行计算逻辑而已,本质和函数的语义约定完全不同。
编译器判定是否生成属性存储字段(Backing Field)的规则
Kotlin的属性默认会生成隐藏的field标识符作为存储载体,也就是常说的backing field,只有满足以下全部条件时,编译器才会跳过字段生成:
- 属性定义了自定义的getter(对于var来说是同时自定义getter和setter)
- 自定义访问器(getter/setter)的实现里,完全没有引用内置的
field标识符
你写的val nameHash get() = name.hashCode()刚好符合要求:自定义了getter,且getter里根本没用到field,编译器没必要额外分配一块内存存一个随时可以计算出来的值,自然就不会生成对应字段。
val和var的深层差异
别只停留在“val只读、var可写”的表层描述,二者的核心区别是编译器对二者的契约要求完全不同:
- 对于
val修饰的属性:编译器只要求访问时能返回合法值,只需要实现getter逻辑即可,不强制要求有可写入口,也不强制要求有独立的存储字段。不管返回值是存在字段里的固定值,还是实时计算出来的动态值,都符合val的契约,所以写了自定义getter的val可以直接通过编译,不需要初始化。 - 对于
var修饰的属性:编译器要求必须同时支持读和写两套完整逻辑。如果没有自定义setter,编译器会自动生成默认setter,逻辑就是给隐藏的field赋值——这时候如果没有初始化field,甚至因为getter没引用field导致编译器根本没生成这个字段,默认setter就没有可赋值的目标,自然会抛出“属性必须初始化”的错误。
如果给var补上完全不依赖field的自定义setter,编译器也不会要求初始化,也不会生成backing field,比如下面的写法是完全合法的:
var nameHash get() = name.hashCode() set(value) { name = value.toString() }
最后补一个语义层面的区分,避免把计算属性和无参函数搞混:
- 无参函数的语义是“执行一段动作/逻辑”,允许产生副作用,不保证多次调用返回值一致
- 计算属性的语义是“获取对象的一个特征值”,约定上不应该产生副作用,在对象本身状态没变化的前提下,多次访问应该返回相同结果
内容的提问来源于stack exchange,提问作者Kevin Paul
相关产品推荐
相关产品推荐

