Swift进阶三——运算符相关

2020-12-11 11:02:40 浏览数 (1)

赋值和算数运算符

1,Swift中的赋值运算符(=)不会返回值。

而在OC中,赋值运算符(=)是有返回值的。

如下面的写法:

代码语言:javascript复制
        var a = "aaa"
        var b = "bbb"
        a = b = "ccc"

在Swift中会报错:

而同样的写法,在OC中是再常见不过的了。

再比如下面这样的写法:

代码语言:javascript复制
        var a = "aaa"
        var b = "bbb"
        
        if a = b {
            print("equal")
        }

我们本来是打算判断变量a和变量b是否相等,但是却手残,将a == b写成了a = b。这在Swift中,会报如下错误:

首先,赋值运算符并没有返回一个值;其次,即便是返回了值,那么的结果也是一个String,而Swift是强类型语言,if后面必须是Bool,因此也是编译不通过的。

在Objective-C中,a = b的运算结果是一个字符串,并且OC中有非空即真的概念,因此这样写是没有任何问题的。即便我手残写错了,也是可以编译运行通过,买下了一颗地雷。从这一点上讲,Swift确实是比OC安全。

在Swift里如何处理算数结果溢出

在默认情况下,当我们向一个整数赋超过他容量的值的时候,Swift会报错,而不是生成一个无效的数。这就给我们操作过大或者过小的数的时候提供了额外的安全性。

Swift中提供了三个算数溢出运算符,来让系统支持整数溢出运算:

溢出加法:&

溢出减法:&-

溢出乘法:&*

无论是对于有符号整型,还是无符号整型而言,当出现上溢的时候,它们会从数值所能容纳的最大数变成最小的数;同样的,当发生下溢时,它们会从所能容纳的最小数变成最大的数。

无符号8位整型的最大值是255,当发生上溢的时候,变成UInt8的最小值0:

无符号整型的最小值是0,当发生下溢的时候,变成UInt8所能表示的最大值255:

有符号整型的最小值是-128,当发生下溢的时候,变成Int8所能表示的最大值127:

看下面几个例子。

代码语言:javascript复制
        var a: UInt8 = 250
        var b = a   10
        print(b)

会报错:Arithmetic operation '250 10' (on type 'UInt8') results in an overflow

改成如下:

代码语言:javascript复制
        var a: UInt8 = 250
        var b = a &  10
        print(b) // 4

合并空值运算符

先来看一个例子:

代码语言:javascript复制
        var a: String? = "norman"
        print(a ?? "a是空值")

这里的??就是合并空值运算符。

合并空值运算符(a??b),如果可选项a有值则展开;如果可选项a没有值,那么a就是nil,a??b的返回值就是b。

这里的表达式a必须是一个可选类型,表达式b必须与a的存储类型相同。标红的文字是官方文档中对于合并空值运算符的解释说明,但是实际上,我们在写代码的时候,发现a可以不是可选型,b也不一定必须与a的存储类型相同。虽然实际与官方文档描述不符,但是我们在写代码的时候,还是要遵守官方文档中的说明,可以将上述标红当做代码规范去执行。

合并空值运算符实际上是三元运算符作用到可选型Optional上的一个缩写,即:a??b 等同于 a!=nil ? a! : b

强大的位运算符

位取反运算符

位取反运算符(~)是对所有位的数字进行取反操作,如下:

位与运算符

位与运算符(&)可以对两个数的比特位进行合并。它会返回一个新的数,只有当这两个数都是1的时候才会返回1。如下:

位或运算符

位或运算符(|)可以对两个比特位进行比较,然后返回一个新的数,只要两个操作位任意一个为1时,那么对应的位数就为1。如下:

位异或运算符

位异或运算符(^)可以对两个数的比特位进行比较,它返回一个新的数,当两个操作位的对应值不相等的时候,该操作位就是1。如下:

位左移和右移运算符

位左移运算符(<<)和位右移运算符(>>)可以把所有位数的数字向左或向右移动一个确定的位数。

位的左移和右移具有给整数乘以或者除以2的效果。将一个数左移一位相当于把这个数翻倍,将一个数右移一位相当于把这个数减半。

无符号整数的移位操作

  • 已经存在的比特位按指定的位数进行左移和右移
  • 任何超出整型存储边界的位都会被抛弃
  • 用0来填充向左或向右移动后产生的空白位

例如,UInt8的左移:

UInt8的右移:

有符号整数的移位操作

  • 有符号整数使用它的第一位(所谓的符号位)来表示这个整数是正数还是负数。符号位为0表示正数,1表示负数。
  • 其余的位数(所谓的数值位)存储了实际的值。有符号正整数和无符号整数的存储方式是一样的,都是从0开始算起。
  • 但是负数的存储方式略有不同。它存储的是2的n次方减去它的绝对值,这里的n为数值位的位数。这就是所谓的补码表示法

例如,下图中,2的8次方减去(-4)的绝对值=124:

补码表示的优点

前面我们已经知道了,在Swift中,有符号整数的负数是通过补码表示的。接下来我们来说说补码表示的优点。

比如,我如果想给一个-4加个-1,那么就只需要将这两个数的全部八个比特位(包括符号位)相加,并且将计算结果中超出的部分丢弃。如下:

使用二进制补码可以使使负数的位左移和右移操作得到跟正数同样的效果,即每向左移一位就将自身数值乘以2,每向右移一位就将自身的数值除以2。要达到此目的,对有符号整数的右移有一个额外的规则:每当整数进行位右移操作时,遵循与无符号整数相同的规则,但对于位移产生的空白位使用符号位进行填充,而不是0。如下:

下面来看看具体代码。

首先是无符号整型的位运算:

代码语言:javascript复制
        let number: UInt8 = 255
        
        print(~number)//取反,0
        
        print(number & 1)//与操作,1
        print(number & 0)//与操作,0
        
        print(number | 1)//或操作,255
        print(number | 0)//或操作,255
        
        print(number ^ 3)//异或操作,252
        
        
        let number2: UInt8 = 4
        
        print(number2 << 1)//左移,8
        print(number2 >> 1)//右移,2

然后是有符号整型的位运算:

代码语言:javascript复制
let number: Int8 = 127

print(~number)//取反,-128(有符号二进制10000000表示的是-128)

print(number & 1)//与操作,1
print(number & 0)//与操作,0

print(number | 1)//或操作,127
print(number | 0)//或操作,127

print(number ^ 3)//异或操作,124


let number2: Int8 = 4

print(number2 << 1)//左移,8
print(number2 >> 1)//右移,2

位运算符应用举例

举例1

不借助临时变量,来实现两个变量值的交换。代码如下:

代码语言:javascript复制
var a = 10
var b = 8
a = a ^ b
b = a ^ b
a = a ^ b
print(a) // 8
print(b) // 10

举例2:求无符号整型二进制中1的个数

给定一个无符号整型(UInt)变量,求其二进制表示中“1”的个数,要求算法的执行效率尽可能的高。

比如说,现在有一个八位整数10100001,先判断最后一位是否是1,而“与”操作可以达到此目的。可以将这个八位数字与00000001进行“与”操作,如果结果为1,则表示当前八位数的最后一位是1,否则为0。那么怎么判断原8位数的倒数第二位呢?只需要将原八位数向右移位,然后延续前面的判断即可。

代码语言:javascript复制
func countOfOnes(num: UInt) -> UInt {
    var count: UInt = 0
    var temp = num
    while temp != 0 {
        count  = temp & 1
        temp = temp >> 1
    }
    
    return count
}

上面的算法是可以实现目的的,但是它有一个问题:如果整数中二进制有较多的0,那么我们每一次都右移一位做判断就会很浪费。针对这一点,如何改进前面的算法呢?

为了简化这个问题,我们考虑只有高位有1的情况。例如:11000000,如何跳过前面低位的6个0,而直接判断第7位的1呢?我们可以设计将11000000和10111111(即11000000-1)做“与”操作,消去最低位的1。但是二进制中还有其他的1,因此我们的计数器需要 1,然后继续上面的操作。

代码语言:javascript复制
func countOfOnes2(num: UInt) -> UInt {
    var count: UInt = 0
    var temp = num
    while temp != 0 {
        count  = 1 // 当外层temp不等于0的时候,说明temp中是包含1的,因此先给数量 1
        temp = temp & (temp - 1)// 计数 1之后,将temp中最后一个1给置为0,然后进入下一循环,直到temp中没有1(即等于0)的时候终止
    }
    
    return count
}

举例3:判断一个整数是2的整数次幂

实际上举例3是上面?举例2的延伸问题。

给定一个无符号整型变量(UInt),判断是否为2的整数次幂。

思路:一个整数如果是2的整数次方,那么它的二进制表示中有且只有一位是1,而其他所有位都是0。因此,只需要判断该整数的二进制表示中是否只包含一个1即可。

代码语言:javascript复制
func isPowerOfTwo(num: UInt) -> Bool {
    if num == 0 {
        return false
    }
    return (num & (num - 1)) == 0
}

⚠️注意兼容传入值为0的情形

举例4:寻找缺失的数字

现在有什么成对出现的正整数保存在磁盘文件中,这些成对的数字不一定是相邻的,比如2,3,4,5,2,4,3,5......,此时由于意外有一个数字消失了,那么如何尽快地找到是哪个数字消失了呢?

思路如下:

所谓“异或”,就是说当两个二进制对应位不相等的时候,该对应位得到结果1;相等的时候对应位得到结果0。如果说人话的话呢,就是说:如果两个相等的数字“异或”,那么等到结果0;0与任意数字“异或”,结果都是那个数字本身。

因此我们考虑将所有的数字做“异或”操作,因为只有一个数字消失,那么其他两两出现的数字“异或”后就是0,0与仅有的一个数字做“异或”,我们就得到了消失的数字是哪个。我们可以将其理解成是消消乐

代码如下:

代码语言:javascript复制
func findLostNum(nums: [UInt]) -> UInt {
    var lostNum: UInt = 0
    for num in nums {
        lostNum = lostNum ^ num
    }
    return lostNum
}


print(findLostNum(nums: [1,3,4,2,3,2,4]))  // 1

举例5:消失的数字(续)

现在我们对上面的举例4做一个扩展:如果是有两个数字意外丢失了(丢失的不是相等的数字),该如何找到丢失的两个数字呢?

思路如下:

假设题目中这两个只出现一次的数字分别是A和B,如果将A、B分开到两个数组中,此时每个数组中只有一个不成对出现的数字了,那么就可以使用举例4中的异或消消乐思路来获得这个落单的数字。所以,这个题目的关键就是如何将A、B分开到不同的数组中。由于A和B肯定是不相等的,因此它们在二进制位上肯定有一位是不相同的,那么我们就可以根据这一位是0还是1将A和B分开到A组和B组。再对A组和B组分别执行异或操作就可以得到A和B了。而要判断A和B在哪一位上不相同,只要根据“A异或B”的结果就可以知道了,这个结果在二进制上为1 的位都是A、B在这一位上不相同的位。

代码如下:

代码语言:javascript复制
func findTwoLostNums(nums: [UInt]) -> (UInt, UInt) {
    var lostNum1: UInt = 0
    var lostNum2: UInt = 0
    var temp: UInt = 0
    
    //计算两个数的异或结果
    for num in nums {
        temp = temp ^ num
    }
    
    //找到第一个为1的二进制位(即两个数不相等的第一个二进制位)
    var flag: UInt = 1
    while ((flag & temp) == 0) {
        flag = flag << 1
    }
    
    //找两个丢失的数字
    for num in nums {
        if (num & flag) == 0 {
            lostNum1 = lostNum1 ^ num
        } else {
            lostNum2 = lostNum2 ^ num
        }
    }
    
    return (lostNum1, lostNum2)
}


print(findTwoLostNums(nums: [1,3,4,2,3,2,4,5])) // (1, 5)

运算符重载

类和结构体可以为现有的运算符提供自定义的实现,我们将这种情形称为运算符重载。

计算运算符( 、-、*、/、%)的重载

代码语言:javascript复制
struct Vector2D {
    var x = 0.0, y = 0.0
}

extension Vector2D {
    static func   (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x   right.x, y: left.y   right.y)
    }
}

let vector = Vector2D(x: 3.0, y: 1.0)
let anotherVector = Vector2D(x: 5.0, y: 6.0)
let combineVector = vector   anotherVector

如上面代码,Vector2D是我自定义的一个二维矢量结构体,正常情况下它是没有加法运算的,但是我使用运算符重载给Vector2D定义了加法运算,这样就可以是两个Vector2D对象直接相加了。

一元运算符重载

类和结构体也能提供标准一元运算符(比如正负号)的实现。

要实现前缀后者后缀运算符,需要在声明运算符函数的时候在func关键字之前指定prefix或者postfix限定符

代码语言:javascript复制
extension Vector2D {
    static prefix func - (vector: Vector2D) -> Vector2D {
        return Vector2D(x: -vector.x, y: -vector.y)
    }
}

let positive = Vector2D(x: 3.0, y: 1.0)
let negative = -positive // (-3, -1)

组合赋值运算符重载

组合赋值运算符将赋值运算符(=)与其他计算运算符( 、-、*、/、%)进行结合。

在实现的时候,需要将运算符的左参数设置成 inout 类型(声明为inout的参数是可以在函数体内部修改,并且可以在外界生效的),因为这个参数的值会在运算符函数内直接被修改。

代码语言:javascript复制
extension Vector2D {
    static func   (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x   right.x, y: left.y   right.y)
    }
}

extension Vector2D {
    static func  = (left: inout Vector2D, right: Vector2D) {
        left = left   right;
    }
}

var original = Vector2D(x: 1, y: 2)
let vectorToAdd = Vector2D(x: 3, y: 4)
original  = vectorToAdd // original:(4, 6)

等价运算符重载

自定义的类和结构体是不接收等价运算符的默认实现的。所谓等价运算符,也就是所谓的“等于”运算符(==)和“不等于”运算符(!=)。

要想使用等价运算符来检查你自己类型的等价,需要提供一个“等于”运算符重载,并且遵循标准库的Equatable协议

代码语言:javascript复制
extension Vector2D: Equatable {
    static func == (left: Vector2D, right: Vector2D) -> Bool {
        return (left.x == right.x) && (left.y == right.y)
    }
}

也并不是所有的自定义类型都不会有默认的等价运算符实现,Swift为以下自定义类型自动提供等价运算符合成实现

  • 遵循Equatable协议且只拥有存储属性的结构体
  • 遵循Equatable协议且只拥有关联类型的枚举
  • 没有关联类型的枚举

自定义运算符

在Swift中,除了实现标准的运算符以外,我们还可以声明和实现自定义运算符(Custom Operators)。

新的运算符要在全局作用域内,使用operator关键字进行声明,同时还要指定prefix(前缀)、infix(中缀)或者postfix(后缀)限定符。

代码语言:javascript复制
struct Vector2D {
    var x = 0.0, y = 0.0
}

extension Vector2D {
    static func   (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x   right.x, y: left.y   right.y)
    }
}

extension Vector2D {
    static func  = (left: inout Vector2D, right: Vector2D) {
        left = left   right;
    }
}

// 自定义运算符
prefix operator   

extension Vector2D {
    static prefix func    (vector: inout Vector2D) -> Vector2D {
        vector  = vector
        return vector
    }
}

var vector = Vector2D(x: 3, y: 4)
  vector // (6, 8)

自定义中缀运算符的优先级和结合性

自定义的中缀(infix)运算符也可以指定优先级和结合性。

每一个自定义的中缀运算符都属于一个优先级组,而优先级组指定了自定义中缀运算符和其他中缀运算符的关系。

代码语言:javascript复制
precedencegroup MyPrecedence {
    associativity: left //结合性:左结合
    lowerThan: AdditionPrecedence //优先级:低于加法优先级组
}
代码语言:javascript复制
// 自定义中缀运算符

infix operator  -: AdditionPrecedence
extension Vector2D {
    static func  - (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x   right.x, y: left.y - right.y)
    }
}

infix operator *^: MultiplicationPrecedence
extension Vector2D {
    static func *^ (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x * right.x, y: left.y * left.y   right.y * right.y)
    }
}

let firstVector = Vector2D(x: 1, y: 2)
let secondVector = Vector2D(x: 3, y: 7)
let plusMinusVector = firstVector  - secondVector // (4, -5)
let thirdVector = Vector2D(x: 2, y: 2)
let vector = firstVector  - secondVector *^ thirdVector // (7, -51)

MultiplicationPrecedence优先级组比AdditionPrecedence优先级组优先级高,所以 *^ 运算符先执行, - 运算符后执行。

现在我对*^ 运算符进行优先级组的变更:

代码语言:javascript复制
// 自定义中缀运算符

infix operator  -: AdditionPrecedence
extension Vector2D {
    static func  - (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x   right.x, y: left.y - right.y)
    }
}

infix operator *^: MyPrecedence
precedencegroup MyPrecedence {
    associativity: left // 左结合
    lowerThan: AdditionPrecedence // 优先级低于AdditionPrecedence
}
extension Vector2D {
    static func *^ (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x * right.x, y: left.y * left.y   right.y * right.y)
    }
}

let firstVector = Vector2D(x: 1, y: 2)
let secondVector = Vector2D(x: 3, y: 7)
let plusMinusVector = firstVector  - secondVector // (4, -5)
let thirdVector = Vector2D(x: 2, y: 2)
let vector = firstVector  - secondVector *^ thirdVector // (8, 29)

MyPrecedence是我自定义的优先级组,它的优先级低于AdditionPrecedence,此时, -运算符先执行,*^运算符后执行。

以上。

0 人点赞