【9159.com】下面的代码重载了++运算符,我们将介

作者: 编程  发布:2019-11-05

operator

使用 operator 关键字重载内置运算符,或在类或结构声明中提供用户定义的转换。

假设场景,一个Student类,有语文和数学两科成绩,Chinese Math,加减两科成绩,不重载运算,代码如下。

    class Student
    {
        /// <summary>
        /// 语文成绩
        /// </summary>
        public double Chinese { get; set; }

        /// <summary>
        /// 数学成绩
        /// </summary>
        public double Math { get; set; }
    }

比较两个成绩差距

            var a = new Student
            {
                Chinese = 90.5d,
                Math = 88.5d
            };

            var b = new Student
            {
                Chinese = 70.5d,
                Math = 68.5d
            };

            //a的语文比b的语文高多少分
            Console.WriteLine(a.Chinese - b.Chinese);
            //a的数学比b的数学高多少分
            Console.WriteLine(a.Math - b.Math);

使用operator 重载 -

    class Student
    {
        /// <summary>
        /// 语文成绩
        /// </summary>
        public double Chinese { get; set; }

        /// <summary>
        /// 数学成绩
        /// </summary>
        public double Math { get; set; }

        public static Student operator -(Student a, Student b)
        {
            return new Student
            {
                Chinese = a.Chinese - b.Chinese,
                Math = a.Math - b.Math
            };
        }
    }

比较成绩差距的代码可以改为

    class Program
    {
        static void Main(string[] args)
        {
            var a = new Student
            {
                Chinese = 90.5d,
                Math = 88.5d
            };

            var b = new Student
            {
                Chinese = 70.5d,
                Math = 68.5d
            };

            var c = a - b;
            //a的语文比b的语文高多少分
            Console.WriteLine(c.Chinese);
            //a的数学比b的数学高多少分
            Console.WriteLine(c.Math);
        }
    }

参考:运算符(C# 参考)

  在这篇博客中,我们将介绍如下内容:

C++ 碎知识点

9159.com 1

  • ==运算符与基元类型
  • ==运算符与引用类型
  • ==运算符与String类型
  • ==运算符与值类型
  • ==运算符与泛型

23. 不能被重载的运算符

  • ** sizeof **:sizeof 运算符
  • ** . **9159.com,:成员运算符
  • ** .* **:成员指针运算符
  • ** :: **:作用域解析运算符
  • ** ? : **:条件运算符
  • ** typeid **:一个 RTT 运算符
  • ** const_cast **:强制类型转换运算符
  • ** dynamic_cast **:强制类型转换运算符
  • ** reinterpret_cast **:强制类型转换运算符
  • ** static_cast **:强制类型转换运算符

操作符重载其实很有意思!但这个概念却很少有人知道,使用操作符重载在某种程度上会给代码的阅读带来一定的麻烦。因此,慎用操作符被认为是一个好习惯。的确,操作符重载是一把双刃剑,既能削铁如泥,也能“引火烧身”,这篇文章将从实用的角度来讲解操作符重载的基本用法。

 

24. 重载限制

  • 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。
  • 使用运算符时不能违反运算符原来的句法规则。同样,不能修改运算符的优先级和结合性。
  • 不能创造新的运算符。
  • 不能重载上面问题 23 中的那些运算符。
  • 大多数运算符都可以通过成员函数或非成员函数进行重载,但下面的运算符只能通过成员函数进行重载:
    • ** = **:赋值运算符
    • ** ( ) **:函数调用运算符
    • ** [ ] **:下标运算符
    • ** -> **:通过指针访问类成员的运算符

支持重载的操作符类型

Kotlin语言支持重载的操作符类型比较多。以最新版本1.2.21为准,目前支持重载的操作符可以归纳为以下几类:

==运算符与基元类型

  我们分别用两种方式比较两个整数,第一个使用的是Equals(int)方法,每二个使用的是==运算符:  

 1 class Program
 2 {
 3     static void Main(String[] args)
 4     {
 5         int num1 = 5;
 6         int num2 = 5;
 7 
 8         Console.WriteLine(num1.Equals(num2));
 9         Console.WriteLine(num1 == num2);
10     }
11 }

  运行上面的示例,两个语句出的结果均为true。我们通过ildasm.exe工具进行反编译,查看IL代码,了解底层是如何执行的。

  9159.com 2

  如果您以前从来没有接触过IL指令,不过没关系,在这里您不需要理解所有的指令,我们只是想了解这两个比较方式的差异。

  您可以看到这样一行代码:

1   IL_0008:  call       instance bool [mscorlib]System.Int32::Equals(int32)

  在这里调用的是int类型Equals(Int32)方法(该方法是IEquatable<Int>接口的实现)。

  现在再来看看使用==运算符比较生成的IL指令:

1   IL_0015:  ceq

  您可以看到,==运行符使用的是ceq.aspx)指令,它是使用CPU寄存器来比较两个值。C#==运算符底层机制是使用ceq指令对基元类型进行比较,而不是调用Equals方法。

 

25. 如何重载前置++ 和后置++ 运算符?

C++中规定重载后置运算时需要添加一个整型参数进行标识。例如,下面的代码重载了++运算符,实现了前置运算和后置运算。

class CEnty
{
public:
    int count;
    CEnty operator++(int) //重载后置++运算符
    {
        CEnty enty = *this;
        this->count++;
        return enty;
    }
    CEnty operator++() //重载前置++运算符
    {
        this->count++;
        CEnty enty = *this;
        return enty;
    }
    CEnty() //默认构造函数
    {
        count = 1;
    }
};
int main(int argc, char* argv[])
{
    CEnty a;
    CEnty b = a++; //调用后置++运算符重载函数
    CEnty c = ++a; //调用前置++运算符重载函数
    return 0;
}

一元操作符

==运算符与引用类型

  修改上面的示例代码,将int类型改为引用类型,编译后通过ildasm.exe工具反编译查看IL代码。

 1 class Program
 2 {
 3     static void Main(String[] args)
 4     {
 5         Person p1 = new Person();
 6         p1.Name = "Person1";
 7 
 8         Person p2 = new Person();
 9         p2.Name = "Person1";
10 
11         Console.WriteLine(p1.Equals(p2));
12         Console.WriteLine(p1 == p2);
13     }
14 }

  上述C#代码的IL代码如下所示: 

  9159.com 3

  我们看到p1.Equals(p2)代码,它是通过调用Object.Equals(Object)虚方法来比较相等,这是在意料之中的事情;现在我们来看==运算符生成的IL代码,与基元类型一致,使用的也是ceq指令。

 

26. 重载 == 运算符实现两个对象的比较

请在 CArea 类中添加重载 == 运算符的代码,当两个对象的 Length 和 Height 数据成员完全相等时,则认为两个对象相等,否则认为不相等。

class CArea
{
public:
    int Length;
    int Height;
    CArea()
    {
        Length = 0;
        Height = 0;
    }
    CArea(int len, int height)
    {
        Length = len;
        Height = height;
    }
};

我们可以定义一个布尔类型的 == 运算符重载函数。
例如参考代码:

class CArea
{
public:
    int Length;
    int Height;
    CArea() //默认构造函数
    {
        Length = 0;
        Height = 0;
    }
    CArea(int len, int height) //自定义构造函数
    {
        Length = len;
        Height = height;
    }
    bool operator==(CArea &area) //运算符重载
    {
        if (area.Length = Length && area.Height==Height)
        {
            cout <<"两个对象相等!" << endl;
            return true;
        }
        else
        {
            cout <<"两个对象不相等!" << endl;
            return false;
        }
    }
};
int main(int argc, char* argv[])
{
    CArea area1(30, 25);
    CArea area2(20, 30);
    if (area1 == area2) //调用重载的==运算符
    {
        ;
    }
    return 0;
}

参考资料:
C++ Primer Plus (第6版)

一元前缀操作符

操作符 对应方法
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

以上三个操作符在日常使用中频率很高,第一个操作符在基本运算中很少使用,第二个操作符就是常见的取反操作,第三个操作符是逻辑取反操作。接下来,我们使用扩展的方式重载这三个操作符:

/**
 * 一元操作符
 *
 * @author Scott Smith 2018-02-03 14:11
 */
data class Number(var value: Int)

/**
 * 重载一元操作符+,使其对Number中实际数据取绝对值
 */
operator fun Number.unaryPlus(): Number {
    this.value = Math.abs(value)
    return this
}

/**
 * 重载一元操作符-,使其对Number中实际数据取反
 */
operator fun Number.unaryMinus(): Number {
    this.value = -value
    return this
}

/**
 * 这个操作符通常是用于逻辑取反,这里用一个没有意义的操作,来模拟重载这个操作符
 * 结果:始终返回Number中实际数据的负值
 */
operator fun Number.not(): Number {
    this.value = -Math.abs(value)
    return this
}

fun main(args: Array<String>) {
    val number = Number(-3)
    println("Number value = ${number.value}")
    println("After unaryPlus: Number value = ${(+number).value}")
    println("After unaryMinus: Number value = ${(-number).value}")

    number.value = Math.abs(number.value)
    println("After unaryNot: Number value = ${(!number).value}")
}

运行上述代码,将得到如下结果:

Number value = -3
After unaryPlus: Number value = 3
After unaryMinus: Number value = -3
After unaryNot: Number value = -3

==运算符与String类型

   接来下来看String类型的例子:  

 1 class Program
 2 {
 3     static void Main(String[] args)
 4     {
 5         string s1 = "Sweet";
 6         string s2 = String.Copy(s1);
 7 
 8         Console.WriteLine(ReferenceEquals(s1, s2));
 9         Console.WriteLine(s1 == s2);
10         Console.WriteLine(s1.Equals(s2));
11     }
12 }

  上面的代码与我们以前看过的非常相似,但是这次我们使用String类型的变量。我们建一个字符串,并付给s1变量,在下一行代码我们创建这个字符串的副本,并付给另一个变量名称s2

  运行上面的代码,在控制台输出的结果如下:

  9159.com 4

  您可以看到ReferenceEquals返回false,这意味着这两个变量是不同的实例,但是==运算符和Equals方法返回的均是true。在String类型中,==运算符执行的结果与Equals执行的结果一样。

  同样我们使用过ildasm.exe工具反编译查看生成IL代码。

  9159.com 5

  在这里我们没有看到ceq指令,对String类型使用==运算符判断相等时,调用的是一个op_equality(string,string)的新方法,该方法需要两个String类型的参数,那么它到底是什么呢?

  答案是String类型提供了==运算符的重载。在C#中,当我们定义一个类型时,我们可以重载该类型的==运算符;例如,对于以前的例子中我们实现的Person类,如果我们为它重载==运算符,大致的代码如下:

 1 public class Person
 2 {
 3 
 4     public string Name { get; set; }
 5 
 6     public static bool operator ==(Person p1, Person p2)
 7     {
 8         // 注意这里不能使用==,否则会导致StackOverflowException
 9         if (ReferenceEquals(p1, p2))
10             return true;
11 
12         if (ReferenceEquals(p1, null) || ReferenceEquals(p2, null)) 
13             return false; 
14 
15           return p1.Name == p2.Name;
16     }
17 
18     public static bool operator !=(Person p1, Person p2)
19     {
20         return !(p1 == p2);
21     }
22 }

  上面的代码很简单,我们实现了==运算符重载,这是一个静态方法,但这里要注意的是,方法的名称是perator ==,与静态方法的相似性;事实上,它们会被由编译器成一个名称为op_Equality()的特殊静态方法。

  为了使用事情更加清楚,我们查看微软实现的String类型。

  9159.com 6

  在上面的截图中,我们可以看到,有两个运算符的重载,一个用于相等,另一个是不等式运算符,其运算方式完全相同,但是否定等于运算符输出。需要注意的一点是,如果您想重载一个类型的==运行符的实现,那么您还需要重载!=操作符的实现,否则编译会报错。

 

自增和自减操作符

操作符 对应方法
a++/++a a.inc()
a--/--a a.dec()

重载这个操作符相对比较难理解,官方文档有一段简短的文字解释,翻译成代码可以这样表示:

// a++
fun increment(a: Int): Int {
  val a0 = a
  a = a + 1
  return a0
}

// ++a
fun increment(a: Int): Int {
  a = a + 1
  return a
}

看懂上面的代码后,我们换成需要重载的Number类,Kotlin最终会这样处理:

// Number++
fun increment(number: Number): Number {
  val temp = number
  val result = number.inc()
  return result
}

// Number++
fun increment(number: Number): Number {
  return number.inc()
}

因此,重载Number类自加操作符,我们可以这样做:

operator fun Number.inc(): Number {
    return Number(this.value + 1)
}

重载自减操作符同理,完整代码请参考我的Git版本库:kotlin-samples

==运算符与值类型

   在演示值类型的示例前,我们先将Person类型从引用类型改为值类型,Person定义如下:

 1 public struct Person
 2 {
 3     public string Name { get; set; }
 4 
 5     public Person(string name)
 6     {
 7         Name = name;
 8     }
 9 
10     public override string ToString()
11     {
12 
13         return Name;
14     }
15 }

  我们将示例代码改为如下:

 1  class Program
 2  {
 3      static void Main(String[] args)
 4      {
 5          Person p1 = new Person("Person1");
 6          Person p2 = new Person("Person2");
 7 
 8          Console.WriteLine(p1.Equals(p2));
 9          Console.WriteLine(p1 == p2);
10      }
11  }

   当我们在尝试编译上述代码时,VS将提示如下错误:

9159.com 7

  根据错误提示,我们需要实现Person结构体的==运算符重载,重载的语句如下(忽略具体的逻辑):

1  public static bool operator ==(Person p1, Person p2)
2  {
3  }
4  public static bool operator !=(Person p1, Person p2)
5  {
6  }

   当添加上面代码后,重新编译程序,通过ildasm.exe工具反编译查看IL代码,发现值类型==运算符调用也是op_Equality方法。

  关于值类型,我们还需要说明一个问题,在不重写Equals(object)方法时,该方法实现的原理是通过反射遍历所有字段并检查每个字段的相等性,关于这一点,我们不演示;对于值类型,最好重写该方法。

 

二元操作符

==运算符与泛型

  我们编写另一段示例代码,声明两个String类型变量,通过4种不同的方式比较运算:

 1 public class Program
 2 {
 3     public static void Main(string[] args)
 4     {
 5         string str = "Sweet";
 6         string str1 = string.Copy(str);
 7 
 8         Console.WriteLine(ReferenceEquals(str, str1));
 9         Console.WriteLine(str.Equals(str1));
10         Console.WriteLine(str == str1);
11         Console.WriteLine(object.Equals(str, str1));
12     }
13 }

  输出的结果如下:

  9159.com 8

  首先,我们使用ReferenceEquals方法判断两个String变量都引用相同,接下来我们再使用实例方法Equals(string),在第三行,我们使用==运算符,最后,我们使用静态方法Object.quals(object,object)(该方法最终调用的是String类型重写的Object.Equals(object)方法)。我们得到结论是:

  • ReferenceEquals方法返回false,因为它们不是同一个对象的引用;
  • String类型的Equals(string)方法返回也是true,因为两个String类型是相同的(即相同的序列或字符);
  • ==运算符也将返回true,因为这两个String类型的值相同的;
  • 虚方法Object.Equals也将返回true,这是因为在String类型重写了方法,判断的是String是否值相同。

  现在我们来修改一下这个代码,将String类型改为Object类型:

 1 public class Program
 2 {
 3     public static void Main(string[] args)
 4     {
 5         object str = "Sweet";
 6         object str1 = string.Copy((string)str);
 7 
 8         Console.WriteLine(ReferenceEquals(str, str1));
 9         Console.WriteLine(str.Equals(str1));
10         Console.WriteLine(str == str1);
11         Console.WriteLine(object.Equals(str, str1));
12     }
13 }

 

  运行的结果如下:

  9159.com 9

  第三种方法返回的结果与修改之前不一致,==运算符返回的结果是false,这是为什么呢?

  这是因为==运算符实际上是一个静态的方法,对一非虚方法,在编译时就已经决定用调用的是哪一个方法。在上面的例子中,引用类型使用的是ceq指令,而String类型调用是静态的op_Equality方法;这两个实例不是同一个对象的引用,所以ceq指令执行后的结果是false

  再来说一下==运算符与泛型的问题,我们创建一个简单的方法,通过泛型方法判断两个泛型参数是否相等并在控制台上打印出结果:

1 static void Equals<T>(T a, T b)
2 {
3     Console.WriteLine(a == b);
4 }

  但是当我们编译这段代码时,VS提示如下错误:

9159.com 10

  上面显示的错误很简单,不能使用==运算符比较两个泛型T。因为T可以是任何类型,它可以是引用类型、值类型,不能提供==运算符的具体实现。

  如果像下面这样修改一下代码:

1 static void Equals<T>(T a, T b) where T : class
2 {
3     Console.WriteLine(a == b);
4 }

  当我们将泛型类型T改为引用类型,能成功编译;修改Main方法中的代码,创建两个相同的String类型,和以前的例子一样:  

 1 public class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         string str = "Sweet";
 6         string str1 = string.Copy(str);
 7 
 8         Equals(str, str1);
 9     }
10 
11     static void Equals<T>(T a, T b) where T : class
12     {
13         Console.WriteLine(a == b);
14     }
15 }

 

  输出的结果如下:  

  9159.com 11

  结果与您预期的结果不一样吧,我们期待的结果是true,输出的结果是false。不过仔细思考一下,也许会找到答案,因为泛型的约束是引用类型,==运算符对于引用类型使用的是引用相等,IL代码可以证明这一点:

  9159.com 12

  如果我们泛型方法中的==运算符改为使用Equals方法,代码如下:  

1 static void Equals<T>(T a, T b)
2 {
3     Console.WriteLine(object.Equals(a, b));
4 }

   我们改用Equals,也可以去掉class约束;如果我们再次运行代码,控制台打印的结果与我们预期的一致,这是因为调用是虚方法object.Equals(object)重写之后的实现。

  但是其它的问题来了,如果对于值类型,这里就会产生装箱,有没有解决的办法呢?关于这一点,我们直接给出答案,有时间专门来讨论这个问题。

  将比较的值类型实现IEquatable<T>.aspx)接口,并将比较的代码改为如下,这样可以避免装箱(关于这一点,可以参考老赵的博客:):

1 static void Equals<T>(T a, T b)
2 {
3     Console.WriteLine(EqualityComparer<T>.Default.Equals(a, b));
4 }

   

算术运算符

操作符 对应方法
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a..b a.rangeTo(b)

前5个操作符相对比较好理解,我们以a + b为例,举个一个简单的例子:

// 重载Number类的加法运算符
operator fun Number.plus(value: Int): Number {
    return Number(this.value + value)
}

fun main(args: Array<String>) {
       println((Number(1) + 2))
}
// 输出结果:
Number value = 3

相对比较难理解的是第六个范围运算符,这个操作符主要用于生成一段数据范围。我们认为Number本身就代表一个整型数字,因此,重载Number是一件有意义的事情。直接看例子:

operator fun Number.rangeTo(to: Number): IntRange {
    return this.value..to.value
}

fun main(args: Array<String>) {
    val startNumber = Number(3)
    val endNumber = Number(9)

    (startNumber..endNumber).forEach {
        println("value = $it")
    }
}

// 运行结果:
value = 3
value = 4
value = 5
value = 6
value = 7
value = 8
value = 9

总结

  对于基元类型==运算符的底层机制使用的是ceq指令,通过CPU寄存器进行比较;

  对于引用类型==运算符,它也使用的ceq指令来比较内存地址;

  对于重载==运算符的类型,实际上调用的是op_equality这个特殊的方法;

  尽量保证==操作符重载和Object.Equals(Object)虚方法的写返回的是相同的结果;

  对于值类型,Equals方法默认是通过反射遍历所有字段并检查每个字段的相等性,为了提高性能,我们需要重写该方法;

  值类型默认情况下不能使用==运算符,需要实现==运算符的重载;

  由于==运算符重载实现实际上是一个静态的方法,在泛型类或方法中使用时与实际的结果可能存在差别,使用Equals方法可以避免这个问题。

  

  转载请注明出自,原文链接:

“In”运算符

操作符 对应方法
a in b b.contains(a)
a !in b !b.contains(a)

这个操作符相对比较好理解,重载这个操作符可以用于判断某个数据是否在另外一个对象中。我们用一个非常简单的自定义类来模拟集合操作:

class IntCollection { 
    val intList = ArrayList<Int>()
}

// 重载"in"操作符
operator fun IntCollection.contains(value: Int): Boolean {
    return this.intList.contains(value)
}

fun main(args: Array<String>) {
    val intCollection = IntCollection()
    intCollection.add(1, 2, 3)
    println(3 in intCollection)
}

// 输出结果:
true

索引访问运算符

操作符 对应方法
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ..., i_n] a.get(i_1, ..., i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ..., i_n] = b a.set(i_1, ..., i_n, b)

这个操作符很有意思,例如,如果你要访问Map中某个数据,通常是这样的map.get("key"),使用索引运算符你还可以这样操作:

val value = map["key"]

我们继续以IntCollection类为例,尝试重写a[i]a[i] = b两个运算符,其它运算符同理。

// 重载a[i]操作符
operator fun IntCollection.get(index: Int): Int {
    return intList[index]
}

// 重载a[i] = b操作符
operator fun IntCollection.set(index: Int, value: Int) {
    intList[index] = value
}

fun main(args: Array<String>) {
    val intCollection = IntCollection()
    intCollection.add(1, 2, 3)
    println(intCollection[0])

    intCollection[2] = 4
    print(intCollection[2])
}

接下来,我们用索引运算符来做一点更有意思的事情!新建一个普通的KotlinUser

class User(var name: String,
           var age: Int) {

}

使用下面的方式重载索引运算符:

operator fun User.get(key: String): Any? {
    when(key) {
        "name" -> {
            return this.name
        }
        "age" -> {
            return this.age
        }
    }

    return null
}

operator fun User.set(key: String, value:Any?) {
    when(key) {
        "name" -> {
            name = value as? String
        }
        "age" -> {
            age = value as? Int
        }
    }
}

接下来,你会神奇地发现,一个普通的Kotlin类居然也可以使用索引运算符对成员变量进行操作了,是不是很神奇?

fun main(args: Array<String>) {
    val user = User("Scott Smith", 18)
    println(user["name"])
    user["age"] = 22
    println(user["age"])
}

因此,索引运算符不仅仅可以对集合类数据进行操作,对一个普通的Kotlin类也可以发挥同样的作用。如果你脑洞足够大,你还可以发现更多更神奇的玩法。

调用操作符

操作符 对应方法
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ……, i_n) a.invoke(i_1, ……, i_n)

重载这个操作符并不难,理解它的应用场景却有一定的难度。为了理解它的应用场景,我们来举一个简单的例子:

class JsonParser {

}

operator fun JsonParser.invoke(json: String): Map<String, Any> {
    val map = Json.parse(json)
    ...
    return map
}

// 可以这样调用
val parser = JsonParser()
val map = parser("{name: "Scott Smith"}")

这里的调用有点像省略了一个解析Json数据的方法,难道它仅仅就是这个作用吗?是的,调用操作符其实就这一个作用。如果一个Kotlin类仅仅只有一个方法,直接使用括号调用的确是一个不错的主意。不过,在使用的时候还是要稍微注意一下,避免出现歧义。

广义赋值操作符

操作符 对应方法
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)

这个操作符相对比较好理解,我们以Number类为例,举一个简单的例子:

// 广义赋值运算符
operator fun Number.plusAssign(value: Int) {
    this.value += value
}

fun main(args: Array<String>) {
    val number = Number(1)
    number += 2
    println(number)
}

// 输出结果:
Number value = 3

相等与不等操作符

操作符 对应方法
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

重载这个操作符与Java重写equals方法是一样的。不过,这里要注意与Java的区别,在Java端==用于判断两个对象是否是同一对象(指针级别)。而在Kotlin语言中,如果我们不做任何处理,==等同于使用Java对象的equals方法判断两个对象是否相等。

另外,这里还有一种特殊情况,如果左值等于null,这个时候a?.equals(b)将返回null值。因此,这里还增加了?:运算符用于进一步判断,在这个情况下,当且仅当b

null的时候,a、b才有可能相等。因此,才有了上面的对应关系,这里以User类为例举一个简单的例子:

class User(var name: String?,
           var age: Int?) {

    operator override fun equals(other: Any?): Boolean {
        if(other is User) {
            return (this.name == other.name) && (this.age == other.age)
        }
        return false
    }
}

注意:这里有一个特殊的地方,与其它操作符不一样的地方是,如果使用扩展的方式尝试重载该操作符,将会报错。因此,如果要重载该操作符,一定要在类中进行重写。

比较操作符

操作符 对应方法
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

比较操作符是一个在日常使用中频率非常高的操作符,重载这个操作符只需要掌握以上表格中几个规则即可。我们以Number类为例举一个简单的例子:

operator fun Number.compareTo(number: Number): Int {
    return this.value - number.value
}

属性委托操作符

属性委托操作符是一种非常特殊的操作符,其主要用在代理属性中。关于Kotlin代理的知识,如果你还不了解的话,请参考这篇文章
Delegation。这篇文章介绍的相对简略,后面会出一篇更详细的文章介绍代理相关的知识。

中缀调用

看到这里,可能有一些追求更高级玩法的同学会问:Kotlin支持自定义操作符吗?

答案当然是:不能!不过,别失望,infix也许适合你,它其实可以看做一种自定义操作符的实现。这里我们对集合List新增一个扩展方法intersection用于获取两个集合的交集:

// 获取两个集合的交集
fun <E> List<E>.interSection(other: List<E>): List<E> {
    val result = ArrayList<E>()
    forEach {
        if(other.contains(it)) {
            result.add(it)
        }
    }

    return result
}

接下来,我们就可以在List及其子类中使用点语法调用了。但,它看起来仍然不像一个操作符。为了让它更像一个操作符,我们继续做点事情:

  • 添加infix关键词
  • 将函数名修改为∩(这是数学上获取交集的标记符号)
    然而,万万没想到,修改完成后居然报错了。Kotlin并不允许直接使用特殊符号作为函数名开头。因此,我们取形近的字母n用于表示函数名:
// 获取两个集合的交集
infix fun <E> List<E>.n(other: List<E>): List<E> {
    val result = ArrayList<E>()
    forEach {
        if(other.contains(it)) {
            result.add(it)
        }
    }

    return result
}

接下来,我们就可以这样调用了val interSection = list1 n list2,怎么样?是不是很像自定义了一个获取交集的操作符n?如果你希望自定义操作符,可以尝试这么做。

其实infix的应用场景还不止这些,接下来,我们再用它完成一件更有意思的事情。

在实际项目开发中,数据库数据到对象的处理是一件繁琐的过程,最麻烦的地方莫过于思维的转换。那我们是否可以在代码中直接使用SQL语句查询对象数据呢?例如这样:

val users = Select * from User where age > 18

纸上学来终觉浅,觉知此事需躬行。有了这个idea,接下来,我们就朝着这个目标努力。
一、先声明一个Sql类,准备如下方法:

   infix fun select(columnBuilder: ColumnBuilder): Sql {

   infix fun from(entityClass: Class<*>): Sql 

   infix fun where(condition: String): Sql 

   fun <T> query(): T 

二、我们的目的是:最终转换到SQL语句形式。因此,增加如下实现:

class ColumnBuilder(var columns: Array<out String>) {

}

class Sql private constructor() {
    var columns = emptyList<String>()
    var entityClass: Class<*>? = null
    var condition: String? = null

    companion object {
        fun get(): Sql {
            return Sql()
        }
    }

    infix fun select(columnBuilder: ColumnBuilder): Sql {
        this.columns = columnBuilder.columns.asList()
        return this
    }

    infix fun from(entityClass: Class<*>): Sql {
        this.entityClass = entityClass
        return this
    }

    infix fun where(condition: String): Sql {
        this.condition = condition
        return this
    }

    fun <T> query(): T {
        // 此处省略所有条件判断
        val sqlBuilder = StringBuilder("select ")

        val columnBuilder = StringBuilder("")
        if(columns.size == 1 && columns[0] == "*") {
            columnBuilder.append("*")
        } else {
            columns.forEach {
                columnBuilder.append(it).append(",")
            }
            columnBuilder.delete(columns.size - 1, columns.size)
        }

        val sql = sqlBuilder.append(columnBuilder.toString())
                            .append(" from ${entityClass?.simpleName} where ")
                            .append(condition)
                            .toString()
        println("执行SQL查询:$sql")

        return execute(sql)
    }

    private fun <T> execute(sql: String): T {
        // 仅仅用于测试
        return Any() as T
    }
}

三、为了看起来更形似,再增加如下两个方法:

// 使其看起来像在数据库作用域中执行
fun database(init: Sql.()->Unit) {
    init.invoke(Sql.get())
}

// 因为infix限制,参数不能直接使用可变参数。因此,我们增加这个方法使参数组装看起来更自然
fun columns(vararg columns: String): ColumnBuilder {
    return ColumnBuilder(columns)
}

接下来,就是见证奇迹的时刻!

fun main(args: Array<String>) {
    database {
        (select (columns("*")) from User::class.java where "age > 18").query()
    }
}

// 输出结果:
执行SQL查询:select * from User where age > 18

为了方便大家查看,我们提取完整执行代码段与SQL语句对比:

select          *       from User             where  age > 18
select  (columns("*"))  from User::class.java where "age > 18"

神奇吗?
至此,我们就可以直接在代码中愉快地使用类似SQL语句的方式进行方法调用了。

总结

本篇文章从操作符重载实用的角度讲解了操作符重载的所有相关知识。如文章开头所说,操作符重载是一把双刃剑。用得好事半功倍,用不好事倍功半。因此,我给大家的建议是:使用的时候一定要保证能够自圆其说,简单来说,就是自然。我认为相对于古老的语言C++来说,Kotlin语言操作符重载的设计是非常棒的。如果你知道自己在做什么,我非常推荐你在生产环境中使用操作符重载来简化操作。

本篇文章例子代码点这里:kotlin-samples


我是欧阳锋,一个热爱Kotlin语言编程的学生。如果你喜欢我的文章,请在文章下方留下你爱的印记。如果你不喜欢我的文章,请先喜欢上我的文章。然后再留下爱的印记!

下次文章再见,拜拜!


本文由9159.com发布于编程,转载请注明出处:【9159.com】下面的代码重载了++运算符,我们将介

关键词:

上一篇:没有了
下一篇:没有了