用 Map 为你的属性做代理

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

1. 引子

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

下面我们看个例子:

 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 来告诉客户端分页的情况,例如:

 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 四种可能,如果我们写个类来解析这段文字,大概会写出下面的代码:

 data class GitHubPaging(var first: String = "",  
        var last: String = "",  
        var next: String = "", 
        var prev: String = "") 

解析的时候怎么解析呢?

 //假设 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 这个类的属性,那么问题就要简单多了:

 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 中获取值那样简单:

 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 自然是可以代理可读写的属性的,下面的扩展方法印证了这一点:

 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,但在语言层面却没有修改的方法),所以只能代理只读变量了:

 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) 

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

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

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

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

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

 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 总可以了吧?

结果。。结果。。。。

 private interface MapWithDefault<K, out V>: Map<K, V> { 
    ... 
 } 

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

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

 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,哦也!

 class GitHubPaging{ 
    val relMap = HashMap<String, String?>().withDefault { null } 
    ...     
 } 

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

附录

 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() 
     } 
 } 



如果你有兴趣加入我们,请直接关注公众号 Kotlin ,或者加 QQ 群:162452394 联系我们。

0?wx_fmt=jpeg


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值