前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用 Map 为你的属性做代理

用 Map 为你的属性做代理

作者头像
bennyhuo
发布2020-02-20 13:15:34
5810
发布2020-02-20 13:15:34
举报
文章被收录于专栏:Bennyhuo

微信公众号 Kotlin 是去年 10 月底开的,到现在,每周最少一篇文章的节奏,把我能想到的的一些关于 Kotlin 的好玩的东西都记录下来告诉大家,结果,我发现一个严重的问题:题目越来越难找了。所以如果大家有好的题目或者想了解的方向、知识点之类的,可以通过公众号直接发给我,只要是历史文章里没有涉及的,我尽量在后面形成文字推送给大家~

1. 引子

话说,Kotlin 里面有两个语法用到了 by 这个关键字,一个是接口代理,一个是属性代理(不知道这俩东西是神马的,去 https://kotlincn.net 查官方文档)。你应该知道属性代理其实本质上就是用一个对象接管属性的 get/set 操作,这个东西可以用来实现一些 Observable 相关的操作,也可以用来封装简化一些复杂的读写操作,总之是一款非常好用却有点儿容易让人懵逼的特性。

下面我们看个例子:

代码语言:javascript
复制
 inline fun <reified R, T> R.pref(default: T) = Preference(AppContext, default, R::class.jvmName)
object Settings {
    var lastPage by pref(0)
} 

前面的这段小代码其实是基于 Preference 这个类(完整代码见后面的附录)做出的扩展,它能实现什么效果呢?lastPage 尽管看上去就是一个很普通的属性,不过如果我们对它进行写操作,那么值会被直接存入 SharedPreference 当中,读操作也会从 SharedPreference 当中读取。

不瞒各位说,Preference 这个类的源码来自于《Kotlin for Android Developers》这本书,我在初学 Kotlin 的时候一下子就被这个特性惊艳到了,有这样好用的扩展,请问你还有什么理由用 sp.edit().putXXX().commit() 呢?最要命的是,官方提供的 SharedPreference 的 api 在使用过程中,不仅难用,而且还经常因为丢掉 commit 而导致错误。

通过这个例子我们可以看出,属性代理这一特性很牛逼,不会的抓紧时间学,会的抓紧时间学着用,用了的抓紧时间出来吹牛逼啊!

2. 属性背后的 Map

如果大家用过 Python,大家就会知道,Python 类有个叫做 __dict__ 的东西(好吧,我实在不知道该怎么称呼它),它以 key-value 的形式存储了一个 Python 对象当中的可写属性,key 就是这个属性的名字,value 就是这个属性的值。

这么看来,我们在访问一个类的属性的时候,实际上就是那属性名去从一个类似 Map 的数据结构中获取相应的值而已。不管各个语言在语法层面做了怎样的封装和简化,背后的实现机制大概也就是如此了。其实有时候如果能够用一个 Map 来 backup 一个类的属性,那会意见非常酷的事情,下面我们就给大家看一个例子。

在访问 GitHub 的 list 请求时,分页问题是一个不得不考虑的问题。GitHub 的 RESTful Api 是如何做分页的呢?通过 Response 的 Header 中设置 link 来告诉客户端分页的情况,例如:

代码语言:javascript
复制
 Link: <https://api.github.com/resource?page=2>; rel="next",
      <https://api.github.com/resource?page=5>; rel="last" 

这表明当前页是第一页,下一页的地址和最后一页的地址都告诉我们了,后面可以按需请求。

关于 Link 的值,rel 的值有 next/last/first/prev 四种可能,如果我们写个类来解析这段文字,大概会写出下面的代码:

代码语言:javascript
复制
 data class GitHubPaging(var first: String = "",  
       var last: String = "",  
       var next: String = "",
       var prev: String = "") 

解析的时候怎么解析呢?

代码语言:javascript
复制
 //假设 rels 就是解析 link 之后得到的数组 
val paging = GitHubPaging()
rels.map{
   when(it.rel){
       "first" -> paging.first = it.url
       "last" -> paging.last = it.url
       "prev" -> paging.prev = it.url  
       "next" -> paging.next = it.url
   }
} 

这里面有几个问题:

  1. 如果 rel 的值有更多,那么我们的 when 表达式就要进一步变长了
  2. GitHubPaging 这个类中的成员实际上都应该是不可变的,但由于我们在初始化过程中需要依次为其赋值,如果用 val 修饰其成员,那么我们只能在解析的时候先有中间变量暂存诸如 first/last 这样的值然后再实例化 GitHubPaging,就像这样: data class GitHubPaging(val first: String, val last: String, val next: String, val prev: String) var first: String = "" var last: String = "" var next: String = "" var prev: String = "" rels.map{ when(it.rel){ "first" -> first = it.url "last" -> last = it.url "prev" -> prev = it.url "next" -> next = it.url } } val paging = GitHubPaging(first, last, next, prev)

实际上如果我们用 Map 代理 GitHubPaging 这个类的属性,那么问题就要简单多了:

代码语言:javascript
复制
 class GitHubPaging(link: String){
    companion object {
        const val URL_PATTERN = """(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"""
    }
    val relMap = HashMap<String, String?>()
    val first by relMap
    val last by relMap
    val next by relMap
    val prev by relMap
    init{
        Regex("""<($URL_PATTERN)>; rel="(\w+)"""").findAll(link).asIterable().map {
            matchResult ->
            relMap[matchResult.groupValues[3]] = matchResult.groupValues[1]
        }
    }
} 

我们用 relMap 来代理这几个属性,在初始化 GitHubPaging 的时候对 link 进行解析,那么问题就简单了,对于所有的 rel 的值,最终都会被存入 relMap,而我们在访问GitHubPaging 的属性的时候,其实是从 relMap 中取值,解析过程就这么愉快的结束了。

如果 rel 哪天又要增加或者修改,我们只需要在 GitHubPaging 中增加或修改相应的属性即可,解析的代码根本不需要改。而如果你想做一个更加通用的代码,还可以为 GitHubPaging 实现一个 get 运算符,获取相应的 url 就如同从 Map 中获取值那样简单:

代码语言:javascript
复制
 class GitHubPaging(...){
   ...
   operator fun get(key: String): String?{
       return relMap[key]
   }
}
val paging = ...
val firstUrl = paging.first
val nextUrl = paging["next"] 

3. Map 缘何可代理属性?

Map 可以代理属性,这个问题其实并不难想到答案。

一个对象想要能够代理属性,只需要根据被代理的属性的读写能力实现 setValue/getValue (如果是只读变量那么实现 getValue 即可),这样看来,Map 应该也是有这样的方法的。

MutableMap 自然是可以代理可读写的属性的,下面的扩展方法印证了这一点:

代码语言:javascript
复制
 public inline operator fun <V> MutableMap<in String, in V>.getValue(thisRef: Any?, property: KProperty<*>): V
        = @Suppress("UNCHECKED_CAST") (getOrImplicitDefault(property.name) as V)
public inline operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property: KProperty<*>, value: V) {
    this.put(property.name, value)
} 

而非 MutableMap 呢,因为是不可修改的 Map(注意这一点,Kotlin 的 Map 尽管在 Jvm 上编译成了 java.util.Map,但在语言层面却没有修改的方法),所以只能代理只读变量了:

代码语言:javascript
复制
 public inline operator fun <V, V1: V> Map<in String, @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1
        = @Suppress("UNCHECKED_CAST") (getOrImplicitDefault(property.name) as V1) 

你以为就简单贴一下源码就完事儿了?当然不,仔细看看 MutableMapMapgetValue 有什么不同?

我在前面有篇讲泛型的文章:Kotlin 泛型(修订版) 提到过可变集合与不可变集合的型变,前者是不变的,而后者是协变的,所以 MapgetValue 版本的返回值可以是 V 的子类,而 MutableMap 的版本则不可以。

4. Map 中没有这个属性对应的 Key?

这种情况是会发生的。仔细看下我们在前面给出的 GitHubPaging 的例子,其中的任何一个属性在从 relMap 中取值时,都将会面临找不到值的情形。

有细心的朋友可能会看出来,我们定义 relMap 时,value 的类型为 String?,也就是说找不到的时候返回 null 不就可以了嘛。但事实呢?当然要问问 getValue 里面的那个函数咯:

代码语言:javascript
复制
 internal fun <K, V> Map<K, V>.getOrImplicitDefault(key: K): V {
    if (this is MapWithDefault)
        return this.getOrImplicitDefault(key)
    return getOrElseNullable(key, { throw NoSuchElementException("Key $key is missing in the map.") })
} 

这段代码很明显地告诉我们,如果没有这个 key,对不起,异常走你。不过,有一种情况例外,那就是,如果你的 Map 类型为 MapWithDefault —— 顾名思义,就是有默认值的 Map

那么我们的 Map 会有默认值吗?如果你觉得有,那么我就像知道你哪儿来的自信保证HashMap 有默认值呢?

HashMap 确实没有默认值,那我定义一个 MapWithDefault 总可以了吧?

结果。。结果。。。。

代码语言:javascript
复制
 private interface MapWithDefault<K, out V>: Map<K, V> {
   ...
} 

居然是 private!是不是想打人?打人也没用,异常走你~

其实这事儿也不难,我们顺藤摸瓜很容易就发现这么个函数:

代码语言:javascript
复制
 public fun <K, V> MutableMap<K, V>.withDefault(defaultValue: (key: K) -> V): MutableMap<K, V> =
        when (this) {
            is MutableMapWithDefault -> this.map.withDefault(defaultValue)
            else -> MutableMapWithDefaultImpl(this, defaultValue)
        } 

只需要在我们自己的 HashMap 后面加一句,就可以把它变成 MapWithDefault,哦也!

代码语言:javascript
复制
 class GitHubPaging{
   val relMap = HashMap<String, String?>().withDefault { null }
   ...    
} 

这回如果找不到 key,那么就返回 null,妥妥的了。

附录

代码语言:javascript
复制
 class Preference<T>(val context: Context, val name: String, val default: T, val prefName: String = "default") : ReadWriteProperty<Any?, T> {
    constructor(context: Context, default: T, prefName: String = "default"): this(context, "", default, prefName)
    val prefs by lazy { context.getSharedPreferences(prefName, Context.MODE_PRIVATE) }
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return findPreference(findProperName(property), default)
    }
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        putPreference(findProperName(property), value)
    }
    private fun findProperName(property: KProperty<*>) = if(name.isEmpty()) property.name else name
    private fun <U> findPreference(name: String, default: U): U = with(prefs) {
        val res: Any = when (default) {
            is Long -> getLong(name, default)
            is String -> getString(name, default)
            is Int -> getInt(name, default)
            is Boolean -> getBoolean(name, default)
            is Float -> getFloat(name, default)
            else -> throw IllegalArgumentException("Unsupported type")
        }
        res as U
    }
    private fun <U> putPreference(name: String, value: U) = with(prefs.edit()) {
        when (value) {
            is Long -> putLong(name, value)
            is String -> putString(name, value)
            is Int -> putInt(name, value)
            is Boolean -> putBoolean(name, value)
            is Float -> putFloat(name, value)
            else -> throw IllegalArgumentException("Unsupported type")
        }.apply()
    }
} 

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-07-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Kotlin 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引子
  • 2. 属性背后的 Map
  • 3. Map 缘何可代理属性?
  • 4. Map 中没有这个属性对应的 Key?
  • 附录
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档