Swift函数式编程六(不可变性的价值)

Posted 酒茶白开水

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift函数式编程六(不可变性的价值)相关的知识,希望对你有一定的参考价值。

代码地址

变量

Swift中的变量有两种,一种是不可变变量(用let申明),另一种是可变变量(用var申明)。

很显然,不可变变量会限制变量的能力,通常可变变量的使用更加广泛。但是往往事实恰恰相反,例如当一个变量在很多地方被使用时,申明为不可变变量就更好,这样就无需当心该变量当前值是什么,也不用当心在赋值过程中对其他部分的不可变性造成破坏。

值类型和引用类型

Swift类型分为值类型和引用类型,最典型的例子就是结构体和类:

struct PointStruct 
    var x: Int
    var y: Int


var point = PointStruct(x: 1, y: 2)
var samePoint = point
samePoint.x = 3
print(point, samePoint)

执行上述代码,会发现point仍然保持原始值。这就是值类型和引用类型之间的关键区别:当被赋以一个新值或者作为参数传递给函数时,值类型会被复制。

用类替代结构体:

class PointClass: CustomStringConvertible 
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) 
        self.x = x
        self.y = y
    
    var description: String 
        return "x: \\(x), y: \\(y)"
    


var pointClass = PointClass(x: 1, y: 2)
var samePointClass = pointClass
samePointClass.x = 3
print(pointClass, samePointClass)

执行上述代码,发现给samePointClass.x赋值之后既修改了samePointClass,也修改了pointClass。理解值类型和引用类型之间的区别尤其重要——可以预测赋值行为将会如何修改数据,同时哪些代码会受到影响。

在调用函数时,值类型和引用类型的区别同样是显而易见的:

func setStructToOrigin(point: PointStruct) -> PointStruct 
    var newPoint = point
    newPoint.x = 0
    newPoint.y = 0
    return newPoint

func setClassToOrigin(point: PointClass) -> PointClass 
    point.x = 0
    point.y = 0
    return point

var strutOrigin = setStructToOrigin(point: point)
var classOrigin = setClassToOrigin(point: pointClass)
print(point, strutOrigin, pointClass, classOrigin)

把一个值类型赋值给新的变量,或者传递给函数时,值类型总是会被复制,而引用类型并不会被复制。对于引用类型的对象来说,只是复制对象的引用并不会复制对象的本身。

Swift为结构体提供了mutating方法,只能在申明为var的结构体变量上使用:

extension PointStruct 
    mutating func setStructToOrigin() 
        x = 0
        y = 0
    

var myPoint = PointStruct(x: 100, y: 100)
let otherPoint = myPoint
myPoint.setStructToOrigin()
print(myPoint, otherPoint)
// PointStruct(x: 0, y: 0) PointStruct(x: 100, y: 100)

相较于类,结构体的mutating有其优势,它不存在类似的副作用。mutating方法只作用于单一变量,完全不影响其他变量。

结构体和类是否可变

  • let定义的结构体常量,不能被赋值,并且无论结构体的常量属性还是变量属性都不能被赋值。
  • var定义的结构体变量,能被赋值,并且结构体的变量属性能够被赋值,但是常量属性不能被赋值
  • let定义的类实例常量,不能被赋值,并且类实例的常量属性不能被赋值,但是变量属性能被赋值。
  • var定义的类实例变量,能被赋值,并且类实例变量的变量属性能被赋值,但是常量属性不能被赋值。

Objective-C

Core Foundation和Foundation框架提供的许多数据结构存在可变和不可变两个版本,比如NSArray和NSMutableArray,NSString和NSMutableString等等。大多数情况下不可变类型是首选,就像在Swift中优先使用值类型一样。

如果在结构体中保存了一个对象,引用是不可变的,但是对象本身却可以 改变。Swift 数组就是这样的:它们使用低层级的可变数据结构,但提供一个高效且不可变的接 口。这里使用了一个被称为写入时复制 (copy-on-write) 的技术。

讨论

共享实例变量而产生耦合 的情况十分常⻅。其结果就是,修改变量的同时可能会改变类中方法的行为。通常,这是一件 好事 —— 如果你改变了存储在对象中的值,它的所有方法都将使用新值。不过同时,这样的 共享实例变量在类的方法之间建立了耦合关系。一旦有方法或是外部函数将这个共享状态弄错,
所有类方法都有可能表现出错误行为。由于它们彼此耦合,独立测试任意一个方法也变得十分 困难。

那些函数的输出值都只取决 于输入值。像这样只要输入值相同则得到的输出值一定相同的函数有时被称为引用透明函数。引用透明函数在它所存在环境中是松耦合的:除了函数的参数,不存在任何隐式依 赖的状态或变量。因此,引用透明函数更容易单独测试和理解。此外创建、调用和 组装引用透明函数,其结果也将是引用透明的。引用透明性是模块化和可重用性的重要保证。

然而 在 Swift 中,教条式地不惜一切代价避开 var 并不⻅得会使你的代码更好。在不少情况下,函 数会在其内部使用一些可变状态。不妨看看下面这个求所有数组元素之和的例子:

func sum(integers: [Int]) -> Int 
    var result = 0
    for value in integers 
        result += value
    
    return result

sum 函数使用的变量 result 是可变的,它反复被更新。但是暴露给用戶的接口却隐瞒了这个事 实。sum 函数依然是引用透明的,甚至比一个为了避开可变变量而不惜一切代价所进行的繁琐 定义更容易理解。

虽然这样的方法尽可能避免了可变引用的使用,但它也会带来额外的内存开销,无法运行在常数量 级 (O(1)) 的内存中。如下面的快速排序方法,它为组成返回值的新数组 lesser 和 greater 分配了内存:

func qsort(array: [Int]) -> [Int] 
    var data = array
    if data.isEmpty 
        return []
    
    let pivot = data.removeFirst()
    let lesser = data.filter  $0 < pivot 
    let gretter = data.filter  $0 >= pivot 
    
    return qsort(array: lesser) + [pivot] + qsort(array: gretter)

以上是关于Swift函数式编程六(不可变性的价值)的主要内容,如果未能解决你的问题,请参考以下文章

Swift函数式编程八(纯函数式数据结构)

Swift函数式编程八(纯函数式数据结构)

Swift函数式编程八(纯函数式数据结构)

浅谈函数式编程与大数据

Swift 结构体何时使用 mutating 函数

函数式编程 - 不变性昂贵吗? [关闭]