美文网首页
Kotlin运算符重栽及其他约定

Kotlin运算符重栽及其他约定

作者: e小e | 来源:发表于2018-07-02 19:35 被阅读21次

    重载算数运算符

    下面以一个栗子开始,我们先定义一个Point
    data class Point(val x : Int, val y : Int) 下面给Point定义一些算术运算符.(在java中算术运算符只能用于基本数据类型,但是在kotlin中可以在任何类型下面使用)
    定义一个plus运算符

    data class Point(val x: Int, val y : Int){
        operator fun plus(other : Point) : Point{
            return Point(x+other.x, y+other.y)
        }
    }
    >>> val p1 = Point(10,20)
    >>> val p2 = Point(30,40)
    >>> println(p1 + p2)
    Point(x = 40, y = 60)
    

    事实上它调用的是a.plus(b).
    还可以定义成扩展函数

    operator fun Point.plus(other : Point) : Point{
        return Point(x + other.x , y + other.y)
    }
    

    Kotlin限定了你能重载哪些运算符,以及你需要在你的类中定义对应名字的函数,如下:
    a * b times
    a / b div
    a % b mod
    a + b plus
    a - b minus
    定义运算符的时候也可以不要求两个运算数是相同的类型。

    operator fun Point.times(scale : Double) : Point{
        return Point((x * scale).toInt(),(y * scale).toInt())
    }
    

    定义一个返回结果不同的运算符

    operator fun Char.times(count : Int) : String{
        return toString().repeat(count)
    }
    >>> println(‘a’ * 3)
    aaa
    

    这个运算符,接收一个Char作为左值,Int作为右值,然后返回一个String类型.

    重载复合赋值运算符

    通常情况下,当定义像plus这样运算符函数时,kotlin不止支持+号运算,也支持+=. 像+=,-=等这些运算符被称为复合赋值运算符.

    >>> var point = Point(1,2)
    >>> point += Point(3,4)
    >>> println(point)
    Point(x=4,y=6)
    

    这等同于point = point + Point(3,4)的写法。
    在一些情况下,定义+=运算可以修改使用它变量所引用的对象,但是不会重新分配引用。将一个元素添加到可变集合,就是一个很好的例子:

    >>> val numbers = ArrayList<Int>()
    >>> numbers += 42
    >>> println(numbers[0])
    

    如何定义这种复合赋值运算符呢,拿+=来举例,Kotlin标准库为可变集合定义了plusAssign函数,在前面的例子中可以这样使用:

    operator fun<T> MutableCollection<T>.plusAssign(element : T){
        this.add(element)
    }
    

    不过在代码中用到+=的时候,理论上plus和plusAssign都可能被调用。如果在这种情况下啊,两个函数有定义且适用,编译器会报错。一种可行的解决方法是, 不要使用运算符,使用普通函数调用. 另外一个办法是,用val替换var, 这样plusAssign运算就不再适用。
    但是一般来说,最好一致地设计出新的类:尽量不要同时给一个类添加plus和plusAssign运算. 如果像前面一个示例中的Point, 这个类是不可变的,那么只需要提供plusAssign和类似的运算就够了.
    kotlin标准库支持集合的这两种方法。+和-运算符总是返回一个新的集合。+=和-=运算符用于可变集合时,始终在一个地方修改它们。
    下面来看一个栗子

    >>> val list = arrayListOf(1,2)
    >>> list += 3               //+=修改”list"
    >>> val newList = list + listof(4,5)    //+返回一个包含所有元素的新列表
    >>> println(list)
    [1,2,3]     
    >>> println(newList)
    [1,2,3,4,5]
    

    重载一元运算符

    重载一元运算符的过程与你在前面看到的方式相同:用预先定义的一个名称来声明函数(成员函数或扩展函数),并用operator标记。下面举个栗子

    operator fun Point.unaryMinus():Point{
        return Point(-x,-y)
    }
    >>>val p = Point(10,20)
    >>>println(-p)
    Point(x=-10,y=-20)
    

    可以用于重载的一元算法的运算符
    +a unaryPlus
    -a unaryMinus
    !a not
    ++a,a++ inc
    --a,a— dec

    重载比较运算符

    与算术运算符一样,在Kotlin中,可以对任何对象使用比较运算符(==,!=,>,<等),而不仅仅限于基本数据类型。不用像Java那样调用equals或compareTo函数。
    等号运算符:”equals”
    在kotlin中使用==运算符,它将被转换成equals方法调用. 这只是我们要讨论的约定原则中的一个。使用!=运算符也会被转换成equals函数调用,明显的差异在于,它们的结果是相反的。注意,和所有其他运算符不同的是,==和!=可以用于可空运算数,因为这些运算符事实上会检查运算数是否为null.比较 a == b会检查a是否为非空,如果不是,就调用a.equals(b) : 否则,只有两个参数都是空引用,结果才是true
    a == b -> a?.equals(b) ?: (b == null)
    下面我们来重写equals函数

    class Point(val x : Int, val y : Int){
        override fun equals(obj : Any?) : Boolean{
            if(obj === this) return true
            if(obj !is Point) return false
            return obj.x == x && obj.y == y
        }
    }
    >>> println(Point(10,20) == Point(10,20))
    true
    >>> println(Point(10,20) != Point(5,5))
    true
    >>> println(null == Point(1,2))
    false
    

    kotlin中的===为恒等运算符(===)来检查参数与调用equals的对象是否相同。恒等运算符与Java中的==运算符是完全相同的: 检查两个参数是否是同一个对象的引用(如果是基本数据类型,检查他们是否是相同的值). 注意 === 运算符不能被重载.
    排序运算符:compareTo
    通常在Java中实现Comparable接口,来自定义两个对象之前通过调用compareTo的比较逻辑,注意必须明确写为element1.compareTo(element2).
    在Kotlin中实现comparable接口后,比较运算符(<,>,<=和>=)的使用讲转换成compareTo. compareTo的返回类型必须为Int. p1 < p2表达式等价于p1.compareTo(p2) < 0.
    下面举个栗子

    class Person(val firstName : String , val lastName : String) : Comparable<Person>{
        override fun compareTo(other : Person) : Int{
            return compareValuesBy(this,other,Person::lastName, Person::firstName)
        }
    }
    >>> val p1 = Person(“Alice”,”Smith”)
    >>> val p2 = Person(“Bob”,”Johnson”)
    >>> println(p1 < p2)
    false 
    

    另外所有Java实现了Comparable接口的类,都可以在Kotlin中使用简洁的运算符语法,不用再增加扩展函数.

    集合与区间的约定

    集合操作中最常见的就是通过下标获取和设置元素,以及检查元素是否属于当前集合。 所有的这些操作都支持运算符语法ab。可以使用in运算符来检查元素是否在集合或区间内,也可以迭代集合.
    通过下标访问元素 : “get” 和 “set"
    在kotlin中,可以用Java中数组的方式来访问map中的元素-使用方括号:

    val value = map[key]
    

    也可以用同样的运算符来改变一个可变map的元素:

    mutableMap[key] = newValue
    

    在Kotlin中下标运算符是一个约定。使用下标运算符读取元素被转换成get运算符方法的调用,并且写入元素将调用set. Map和MutableMap的接口已经定义了这些方法。
    给自己的类添加类似的方法.

    operator fun Point.get(index : Int) : Int{
        return when(index){
            0 -> x
            1 -> y
            else ->
                throw IndexOutOfBoundsException(“Invalid coordinate $index")
        }
    }
    >>> val p = Point(10,20)
    >>> println(p[1])
    

    只需要定义一个名为get的函数,并标记operator. 向p[1]这样将被转换为get方法的调用.

    x[a,b] -> x.get(a,b)
    

    注意,get参数可以是任意的类型,而不只是Int. 例如,当你对map使用下标运算符时,参数类型是键的类型,它可以是任意类型。还可以定义多个参数的get方法. 例如,如果要实现一个类来表示二维数组或矩阵,可以定义一个方法

    operator fun get(rowIndex : Int, colIndex : Int),
    

    然后matrix[row , col] 来调用. 另外get方法也支持重载使用不同的键类型访问集合.
    我们可以重写set函数来更改给定的下标值。例如

    data class MutablePoint(var x : Int, var y : Int)
    operator fun MutablePoint.set(index : Int, value : Int){
        when(index){
            0 -> x = value
            1 -> y = value
            else ->
                throw IndexOutOfBoundsException(“Invalid coordinate $index")
        }
    }
    >>> val p = MutablePoint(10,20)
    >>> p[1] = 42
    >>> println(p)
    MutablePoint(x = 10 , y = 42)
    

    ”in”的约定
    集合支持另外一个运算符就是in运算符,用于检查某个对象是否属于集合。相应的函数叫做contains.

    data class Rectangle(val upperLeft : Point , val lowerRight : Point)
    operator fun Rectangle.contains(p : Point) : Boolean{
        return p.x in upperLeft.x until lowerRight.x &&
             p.y in upperLeft.y until lowerRight.y 
    }
    >>> val rect = Rectangle(Point(10,20),Point(50,50))
    >>> println(Point(20,30) in rect)
    true
    >>> println(Point(5,5) in rect)
    false
    

    这里需要值得注意的是until是表示一个开区间,10 until 20 包含从10到19的数字,但不含20,闭区间用10..20表示.
    rangeTo的约定
    要创建一个区间,请使用..语法: 举个例子, 1..10 代表有从1到10的数字,现在来说说创建它的约定.
    ..运算符是调用rangeTo函数的一个简洁方法
    start .. end -> start.rangeTo(end)
    rangeTo函数返回一个区间。你可以为自己的类定义这个运算符。但是如果该类实现了Comparable接口,那么不需要了: 因为这个库定义了可以用于任何比较元素的rangeTo函数

    operator fun <T: Comparable<T>> T.rangeTo(that : T) : CloseRange<T>
    

    这个函数返回一个区间用来检查其他一些元素是否属于它, 下面用LocalData举个例子

    >>> val now = LocalDate.now()
    >>> val vacation = now..now.plusDays(10)
    >>> println(now.plusWWeeks(1) in vacation)
    

    now..now.plusDays(10) 表达式将会被编译器转换为now.rangeTo(now.plusDays(10)). rangeTo并不是LocalDate的成员函数,而是Comparable的一个扩展函数.
    rangeTo运算符优先级低于算术运算符,不过作为一个良好的编码习惯,通常也用括号括起来以免混淆

    >>> val n = 9
    >>> println(0..(n+1))
    0..10
    

    在”for”循环中使用”iterator”的约定
    kotlin中for循环使用in运算符来执行迭代。这意味着一个诸如for(x in list){…}将被转换成list.iterator()的调用,然后就想在Java中一样,重复调用hasNext和next方法.

    解构声明和组件函数

    这个功能允许展开单个复合值,并使用它来初始化多个单独变量.

    >>> val p = Point(10,20)
    >>> val (x,y) = p
    >>> println(x)
    10
    >>> println(y)
    20
    

    要在解构声明中初始化每个变量,将调用名为componentN的函数,其中N是声明变量的位置。

    val (a,b) = p 
    -> val a = p.component1()
    -> val b = p.component2()
    class Point(val x : Int, val y : Int){
        operator fun component1() = x
        operator fun component2() = y
    }
    

    解构声明主要使用场景之一,是从一个函数返回多个值,这个非常有用。 举个例子,编写一个简单函数,来将一个文件名分割成名字和扩展名.

    data class NameComponents(val name : String, val extension : String)
    fun splitFilename(fullName : String) : NameComponents{
        val result = fullName.split(‘.’,limit = 2)
        return NameComponents(result[0],result[1])
    }
    >>> val (name,ext) = splitFilename(“example.kt”)
    >>> println(name)
    example
    >>> println(ext)
    kt
    

    componentN函数在数组和集合上也有定义,可以进一步改进这个代码。下面使用解构声明来处理集合

    data class NameComponents(val name : String, val extension : String)
    fun splitFilename(fullName : String) : NameComponents{
        val(name,extension) = fullName.split(‘.’,limit = 2)
        return NameComponents(name, extension)
    }
    

    解构声明和循环
    解构声明还可以用于in循环,一个例子,是枚举map中的条目. 下面是一个小例子,使用这个语法打印给定map中的所有条目

    fun printEntries(map : Map<String,String>){
        for((key,value) in map){
            println(“$key -> $value")
        }
    }
    >>> val map = mapOf(“Oracle” to “Java” , “JetBrains” to “Kotlin”)
    >>> printEntries(map)
    Oracle -> Java
    JetBrains -> Kotlin
    

    重用属性访问的逻辑:委托属性

    委托属性的基本语法:

    class Foo{
        var p : Type by Delegate()
    }
    

    属性p将它的访问器逻辑委托给了另一个对象:这里是Delegate类的一个新的实例。
    编译器创建一个隐藏的辅助属性,并使用委托对象的实例进行初始化,初始属性p会委托给该实例。为了简单起见,我们把它称为delegate:

    class Delegate{
        operator fun getValue(…) {…}
        operator fun setValue(…,value : Type){...}
    }
    class Foo{
        var p : Type by Delegate()
    }
    >>> val foo = Foo()
    >>> val oldValue = foo.p
    >>> foo.p = newValue
    

    使用委托属性:惰性初始化和”by lazy()”
    惰性初始化是一种常见的模式,直到第一次访问该属性的时候,才根据需要创建对象的一部分。
    举个栗子,一个Person类,可以用来访问一个人写的邮件列表。邮件存储在数据库中,访问比较耗时。你希望只有在首次访问时才加载邮件,并只执行一次。假设你已经有函数loadEmails,用来从数据库中检查电子邮件:

    class Email{ /* … */}
    fun loadEmails(person : Person) : List<Email>{
        println(“Load emails for ${person.name}")
    }
    

    下面展示如何使用额外_emailds属性来实现惰性加载,在没有加载之前为null, 然后加载为邮件列表.

    class Person(val name : String){
        private var_emails : List<Email>? = null
        
        val emails : List<Email>
            get(){
                if(_emails == null){
                    _emails = loadEmails(this)
                }
                return _emails!!
            }
    }
    >>> val p = Person(“Alice”)
    >>> p.emails
    Load emails for Alice
    >>> p.emails
    

    这里使用了所谓的支持属性技术。你有一个属性,_emails, 用来存储这个值,而另一个emails, 用来提供属性的读取访问.
    但是上面这个代码有点啰嗦:要是有几个惰性属性那得有多长。而且,它并不总是正常运行:这个实现不是线程安全。使用委托属性会让代码变得简单很多,可以用于封装存储值的支持和确保该值只被初始化一次的逻辑。在这里可以使用标准函数lazy返回的委托.

    class Person(val name : String){
        val emails by lazy{ loadEmails(this) }
    }
    

    lazy的参数是一个lambda,可以调用它来初始化这个值。

    委托属性的原理

    在java中存在一个PropertyChangeSupport类用来监听属性的变化. 这意味着当属性发生变化的时候会收到相应的通知,来看看下面示例:

    public class SomeBean {
        private String property;
        private PropertyChangeSupport changeSupport;
        public void setProperty(String newValue) {
            String oldValue = property;
            property = newValue;
            changeSupport.firePropertyChange("property", oldValue, newValue);
        }
    
        public void addPropertyChangeListener(PropertyChangeListener l) {
            changeSupport.addPropertyChangeListener(l);
        }
    
        public void removePropertyChangeListener(PropertyChangeListener l) {
            changeSupport.removePropertyChangeListener(l);
        }
    }
    

    这意味着当调用setProperty后会通知addPropertyChangeListener中的PropertyChangeListener. 而在kotlion我们也可以利用该特性来实现属性修改的通知

    open class PropertyChangeAware{
        protected val changeSupport = PropertyChangeSupport(this)
    
        fun addPropertyChangeListener(listener: PropertyChangeListener){
            changeSupport.addPropertyChangeListener(listener)
        }
    
        fun removePropertyChangeListener(listener: PropertyChangeListener){
            changeSupport.removePropertyChangeListener(listener)
        }
    }
    class Person_ONE(val name : String, age : Int, salary : Int) : PropertyChangeAware(){
        var age : Int = age
            set(newvalue){
                val oldValue = field
                field = newvalue
                changeSupport.firePropertyChange("age",oldValue,newvalue)
            }
        var salary : Int = salary
            set(newvalue){
                val oldValue = field
                field = newvalue
                changeSupport.firePropertyChange("salary",oldValue,newvalue)
            }
    }
    fun main(args: Array<String>) {
        val p = Person_ONE("Dmitry",34,2000)
        p.addPropertyChangeListener(
                PropertyChangeListener { evt: PropertyChangeEvent? ->
                    println("Property ${evt?.propertyName} changed "+ "from ${evt?.oldValue} to ${evt?.newValue}")
                }
        )
        p.age = 35
        p.salary = 2100
    }
    >>>
    Property age changed from 34 to 35
    Property salary changed from 2000 to 2100
    >>>
    

    另外也可以利用辅助类实现上面的属性修改的通知

    open class ObservableProperty(val propNmae : String, var propValue : Int, val changeSupport: PropertyChangeSupport){
        fun getValue():Int = propValue
        fun setValue(newvalue: Int){
            val oldValue = propValue
            propValue = newvalue
            changeSupport.firePropertyChange(propNmae,oldValue,newvalue)
        }
    }
    class Person_TWO(val name : String, age : Int, salary : Int) : PropertyChangeAware(){
    
        val _age = ObservableProperty("age",age,changeSupport)
    
        var age : Int
            get() = _age.getValue()
            set(value){
                _age.setValue(value)
            }
        val _salary = ObservableProperty("salary",salary,changeSupport)
        var salary : Int
            get() = _salary.getValue()
            set(value) {_salary.setValue(value)}
    }
    

    可以看到你需要非常多的样板,但是Kotlin的属性功能可以让你摆脱这些样板代码。但是在此之前你需要更改ObservableProperty方法的签名,来匹配Kotlin约定所需的方法.
    下面来看看ObservableProperty来作为属性委托

    class ObservableProperty(var propValue : Int, val changeSupport: PropertyChangeSupport){
        operator fun getValue(p : Person, prop : KProperty<*>) : Int = propValue
        operator fun setValue(p : Person, prop : KProperty<*> , newValue : Int){
            val oldValue = propValue
            propValue = newValue
            changeSupport.firePropertyChange(prop.name,oldValue,newValue)
        }
    }
    

    下面可以见识kotlin委托属性的神奇了.来看看代码变短多少?

    class Person(val name : String, age : Int, salary : Int) : PropertyChangeAware(){
        var age : Int by ObservableProperty(age,changeSupport)
        var salary : Int by ObservableProperty(salary,changeSupport)
    }
    

    by后面的对象称为委托。Kotlin会自动将委托存储在隐藏属性中,并在访问或修改属性时调用委托的getValue和setValue.
    在kotlin中你并需要手动去实现一个ObservableProperty,你只需要传递一个lambda,来告诉它如何通知属性值的更改.

    class Person_Four(val name : String, age: Int, salary: Int) : PropertyChangeAware(){
        private val observer = {
            prop : KProperty<*>, oldValue : Int, newValue : Int ->
            changeSupport.firePropertyChange(prop.name,oldValue,newValue)
        }
        var age : Int by Delegates.observable(age,observer)
        var salary : Int by Delegates.observable(salary,observer)
    }
    

    by右边的表达式不一定是新创建的实例,也可以是函数调用,另一个属性或其它表达式。

    委托属性的变换规则

    class C{
        var prop : Type by MyDelegate()
    }
    val c = C()
    

    MyDelegate实例会被保存到一个隐藏的属性中,它被称为<delegate>. 编译器也将用一个KProperty类型的对象来代表这个属性,它被称为<property>.
    编译器生成的代码如下:

    class C{
        private val <delegate> = MyDelegate()
        var prop : Type
            get() = <delegate>.getValue(this,<property>)
            set(value : Type) = <delegate>.setValue(this, <property>, value)
    }
    

    在map中保存属性值

    委托属性发挥作用的另一种常见用法,是用在有动态定义的属性集的对象中。

    class Person{
        private val _attributes = hashMapOf<String,String>()
        fun setAttribute(attrName : String, value : String){
            _attributes[attrName] = value
        }
    
        val name : String
            get() = _attributes["name"]!!
    }
    >>> val p = Person()
    >>> val data = mapOf("Oracle" to "Java" , "company" to "JetBrains")
    >>> for ((attrName,value) in data){
    >>>    p.setAttribute(attrName,value)
    >>> }
    >>> println(p.name)
    Dmitry
    

    使用委托属性把值存到map中

    class Person{
        private val _attributes = hashMapOf<String,String>()
        fun setAttribute(attrName : String, value : String){
            _attributes[attrName] = value
        }
        val name : String by _attributes
    }
    

    小结:
    1,Kotlin允许使用对应名称的函数来重载一些标准的数学运算,但是不能定义自己的运算符
    2,比较映射为equals和compareTo方法的调用
    3, 通过定义名为get,set和contains的函数,就可以让你自己的类与Kotlin的集合一样,使用[]和in运算符
    4, 可以通过约定来创建区间,以及迭代集合和数组
    5, 解构声明可以展开单个对象用来初始化多个变量,这可以方便地用来从函数返回多个值。它们可以自动处理数据类, 可以通过给自己的类定义名为componentN的函数
    6,委托属性可以用来重用逻辑, 这些逻辑控制如何存储,初始化,访问和修改属性值,这是用来构建框架的一个强大的工具
    7, lazy标准库函数提供了一种实现惰性初始化属性的简单方法
    8, Delegates.observable 函数可以用来添加属性更改的观察者
    9,委托属性可以使用任意map来作为委托属性委托,来灵活处理具有可变属性集的对象

    相关文章

      网友评论

          本文标题:Kotlin运算符重栽及其他约定

          本文链接:https://www.haomeiwen.com/subject/yugoyftx.html