依值类型是一个很有用的概念,它可以使函数的输入或者输出,根据输入的参数性质来产生变化,让强类型系统能类型安全地使用一些动态类型性质。其中一种用法是类型安全的属性列表。我是在 Ktor 和 Netty 的代码中学习到了这种用法。
例子
在以前,Java 的世界里,像 ServletContext 或者一些 PropertyMap,它都是基于两个繁星参数的 Map 来作为存储表(或者说,注册表)。
1 |
|
以上的例子,使用了一个 key 为 String 的表来存储各样类型的数值,在取出的时候,就需要显式地作类型转换。
这时候潜在的危险就来了:如果我不知道该 key 对应的含义,使用了错误的类型来转换,那么就会出现 ClassCaseException。而在有 IDE 的编程环境下,单纯一个字符串,无法在智能提示下显现其对应值的类型。在编译器看来,这完全是合法的 ── 显式转换意味着编译器相信编码者的决定,忽略对这句表达式的类型判定,这导致错误要在运行时才能看出来,这是明显的执行错误隐患。
分析
编码者已经作了忽略 Map 的值类型的选择:Map<String, Object>,这告诉编译器“我不在乎 Value 的值,我当它们是 Object”。
而后在取出的时候,值都是 Object,而我们却要求编译器将它看成是我们期望的类型,这是毫无根据的。
要编译器这么做,就需要明确地给出根据,最简单的根据,便是显式的指令:强制转换。
强制转换带来的问题是,它仅仅是告诉编译器不要去理会类型错误,运行时的类型错误是无法避免的。而如果运行时也不在乎,就会出现逻辑冲突,程序运行便不正确,导致非法操作,这在程序执行中是不被允许的。
Key 的作用是从表中索引出对应的数据,Key 不同,Value 也将不同,而在强类型系统中,Value 都有其实际的类型。通过简单的 String 取出的 Object 无类型信息,产生了代码编写问题而违背运行时逻辑的可能性,而这种问题在客观上很难被发现的。针对这情况,可以设计一个机制,在编写时就解决这个问题,并在编译时确保无问题。
实现
我们可以通过创建一种 Key 类型,在编译时有一个类型的参照。并创建一种新的容器,管理对一个 Map 取值时的类型转换操作。
1 |
|
这样,在使用的时候,就可以避免到处转换类型的危险操作了。
1 |
|
这里使用的基本技巧就是泛型。
限制
可以看得出来,它是在属性存储无需被序列化的情况下使用的,并不适用于数据传输。
这种用法大多出现在一个程序中需要中央地存储状态的时候使用,像 Netty、Ktor 等等面向过程的场景下就大量出现这种用法。