swift int转string_一文读懂Swift函数式编程_weixin_39827306的博客-程序员宅基地

技术标签: swift int转string  

最近在研究SwiftUI中的Combine框架,主要是学习这本书的内容:Using Combine,其中一个很重要的概念就是Functional Programming,也就是函数式编程。我相信这个概念大家肯定都听过,但要把它简单的讲明白,也不是一件容易的事儿,在这篇文章中,我将用一个实例来做一个讲解。

本文的主要目的就是弄明白下边几个关键词:

  • 函数式编程
  • 不可变状态和副作用
  • 一等公民和高阶函数
  • 偏函数和纯函数
  • 引用透明
  • 递归
  • 命令式和声明式编程

函数式编程

var name = "小明"
name = "小红"

上边这段代码,实在是太简单了,我们一般这样描述:创建了一个变量name,给它赋值为小明,再修改它的值为小红。从我们学习编程开始,就是这么写代码的,这有什么问题吗?

并无任何问题,大家可能不知道,这其实是一种命令式的编程风格,简而言之,你创建了一个变量,然后你主动为其赋值。变量这个概念,还是有点怪的,大家仔细想想,变量可以在程序运行过程中被改变,这就引入了一个时间的观念,因此,我们在阅读代码的时候,不得不考虑程序执行的时间因素,花费大量的精力来分析某个变量的生命周期,我相信大家一定都经历过这样痛苦。

举个 :

func eatApple() {
     
    print("我吃了一个大 ")
    name = "翠花"
}
eatApple()

也不知道哪位同学写的代码,在eatApple()中,修改了nameeatApple()就是为了吃苹果,为什么要修改name呢!!!,其实,你懂的,eatApple()早已隐藏在万千代码之中。

print("(name)不爱吃酸菜!!!")

翠花不爱吃酸菜!!!

某天,我心血来潮,打印这行数据,我本来想要的结果是小红不爱吃酸菜!!!,怎么变成了翠花不爱吃酸菜!!!,TMD,谁都知道翠花卖酸菜的,怎么可能不爱吃酸菜呢!!!

大家看到了没?这真心不是一个笑话,在项目中,绝对存在,所以跟我大声喊出来,这很不函数啊。

不可变状态和副作用

要想避免上边的问题,我们可以这么做:

let name2 = "小明"
name2 = "小红"

当我们把name2声明成let时,name2就变成了一个不可变状态的变量,在程序的任何地方都不可以修改其值,否则编译器会报错:Cannot assign to value: 'name2' is a 'let' constant

这时候,name2就变成了史上最固执的人,没有什么人比我更懂。。。不对,没有什么人能够改变我,就是这么任性。

当不希望某个变量改变状态时,我们可以为其增加限制,比如只读,静态等

上边的eatApple()函数就是一个有副作用的函数,这里的副作用通常指的是函数修改了其他的外部变量。

这种有副作用的函数特别像定时炸弹。

排序

有这样一个题目,在某个班级的考试统计系统中,记录了学生的姓名,年龄和分数,老师让小明打印一份名单,按照姓名中的字母进行排序。

小明是一名很有经验的程序猿,头脑中立马出现各种各样的排序算法,没想到有这么多的排序算法:

0cf4f9329d58e5a148b0318f1c97a17f.png

于是,小明选择了一个最简单的插入排序算法,他的原理大概是这样的:

a62198b2daac43640a7118be6b6e4088.png

小明先创建了一个Student的结构体,用来描述学生这个对象:

struct Student {
     
    let name: String
    let age: Int
    let score: Int
}

然后把学生的信息保存在一个数组中:

let students = [
    Student(name: "Calista", age: 18, score: 85),
    Student(name: "Griselda", age: 20, score: 88),
    Student(name: "Annabelle", age: 24, score: 92),
    Student(name: "Polly", age: 22, score: 93),
    Student(name: "Maud", age: 16, score: 95),
]

接下来,就是让人兴奋的算法编码环节了,小明喝了3杯来自俄罗斯的小鸟伏特加,撸起袖子,开始写代码:

func sortedNames(for students: [Student]) -> [String] {
     
    var sortedStudents = students
    var current: Student

    for i in (0..<sortedStudents.count) {
     
        current = students[i]
        for j in stride(from: i, to: -1, by: -1) {
     
            if current.name.localizedCompare(sortedStudents[j].name) == .orderedAscending {
     
                sortedStudents.remove(at: j + 1)
                sortedStudents.insert(current, at: j)
            }
        }
    }

    var names = [String]()
    for student in sortedStudents {
     
        names.append(student.name)
    }

    return names
}

小明只用了2个小时,就写完了这个算法,咱们验证一下,结果是否正确:

print(sortedNames(for: students))

["Annabelle", "Calista", "Griselda", "Maud", "Polly"]

没毛病啊 ,结果完全正确,小明抄起电脑,就走到老师的办公室,大声的讲起了上边代码的实现原理,那真是唾沫横飞啊,小明无意间看了一眼老师的脸,只见老师怒目圆睁,脸上斑斑点点,这像是爆发的边缘啊。

“滚出去!!! 这写的什么玩意,重写!!!”

一等公民和高阶函数

小明虽然不服气,但也没办法啊,官大一级压死人啊,和珅曾经说过:臣,胆战心惊,如履薄冰的伺候了你这么多年,如今官降三级,怎能不让人寒心!!!

于是小明上网搜集资料,发现了一等公民和高阶函数,这是怎么一回事呢?

在函数式编程中,函数是一等公民,不再把函数想象成一个处理过程,而是把它当作一个对象或者变量来对待。

思维一旦转变,就是另一片天,当函数是变量后,我们就可以把它作为参数或者返回值放到另一个函数中。

所谓的高阶函数,就是把函数作为函数参数的函数。我觉得我这句话说的挺不错的,挺绕的,嘿嘿。

1. Map

在Swift中的Collection类型,都有一个map(_:)方法,它接受一个函数作为参数,其目的是对原集合中的元素进行转换,因此map(_:)的输出结果也是一个数组。

我们可以把Map想象成一个包子生产机器,进去的是馅,出来的是包子,也可以想象成一个爆米花机器,进去的是玉米,出来的是爆米花。总之,这是一个转换过程,我更喜欢称之为映射过程。想想线性代数,集合从一个坐标空间映射到另一个坐标空间,实在是奇妙,人类的想象力真是伟大。

小明已经迫不及待的想要试试这个map(_:)了,他看了眼之前写的算法,有这样一段代码:

var names = [String]()
for student in sortedStudents {
     
    names.append(student.name)
}

目的是获取所有学生的name,那么用上map(_:),岂不是完美,说做就做,小明开始写代码:

func sortedNamesWithMap(for students: [Student]) -> [String] {
     
    var sortedStudents = students
    var current: Student

    for i in (0..<sortedStudents.count) {
     
        current = students[i]
        for j in stride(from: i, to: -1, by: -1) {
     
            if current.name.localizedCompare(sortedStudents[j].name) == .orderedAscending {
     
                sortedStudents.remove(at: j + 1)
                sortedStudents.insert(current, at: j)
            }
        }
    }

        /// 新增内容
    let names = sortedStudents.map{
      $0.name }

    return names
}
print(sortedNamesWithMap(for: students))

["Annabelle", "Calista", "Griselda", "Maud", "Polly"]

嗯,虽然结果完全相同,但是感觉代码一下子高级了不少啊,小明有一种奇怪的感觉,却无法用语言表达。如果把let names = sortedStudents.map{ $0.name }替换成let scores = sortedStudents.map{ $0.score }

那岂不是就得到了一个分数数组?

这种感觉就像,你告诉map(_:)你要什么东西,而不是直接命令式的编码。

Filter

在Swift中的Collection类型,都有一个filter(_:)方法,它接受一个函数作为参数,其目的是对原集合中的元素进行筛选,因此filter(_:)的输出结果也是一个数组。

我们可以把Filter想象成一个筛子,只有符合条件的豆粒才能通过。Filter的关键词是过滤。

小明立马想到了快速排序算法,快排的核心思想是:

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

bcef79d9145374eddba2288b55cc20e8.png

小明想到,如果把上边的插入排序替换成快速排序,速度就更快了,也正好能用上filter(_:),说干就干,小明开始飞速编码:

extension Array where Element: Comparable {
     
    func quickSorted() -> [Element] {
     
        if self.count > 1 {
     
            let (pivot, remaining) = (self[0], dropFirst())
            let lhs = remaining.filter{
      $0 <= pivot}
            let rhs = remaining.filter{
      $0 > pivot}
            return lhs.quickSorted() + [pivot] + rhs.quickSorted()
        }
        return self
    }
}

同学们,上边快排的Swift实现,看上去是如此的优雅啊。要想使用上边代码中的算法,还需要让Student实现Comparable协议。

extension Student: Comparable {
     
    static func < (lhs: Student, rhs: Student) -> Bool {
     
        lhs.name < rhs.name
    }

    static func == (lhs: Student, rhs: Student) -> Bool {
     
        lhs.name == rhs.name
    }
}
let sortedNames = students.quickSorted().map{
      $0.name }
print(sortedNames)

["Annabelle", "Calista", "Griselda", "Maud", "Polly"]

binggo,同样可以实现name的排序,由于把quickSorted()写到成了Array的扩展方法,它会直接修改原数组的内容,因此,我们需要把之前的代码:

let students = [
    Student(name: "Calista", age: 18, score: 85),
    Student(name: "Griselda", age: 20, score: 88),
    Student(name: "Annabelle", age: 24, score: 92),
    Student(name: "Polly", age: 22, score: 93),
    Student(name: "Maud", age: 16, score: 95),
]

=====>>>> 改成

var students = [
    Student(name: "Calista", age: 18, score: 85),
    Student(name: "Griselda", age: 20, score: 88),
    Student(name: "Annabelle", age: 24, score: 92),
    Student(name: "Polly", age: 22, score: 93),
    Student(name: "Maud", age: 16, score: 95),
]

3. Reduce

在Swift中的Collection类型,都有一个reduce(_:)方法,它接受一个函数作为参数,其目的是对原集合中的元素进行合并,因此reduce(_:)的输出结果也是一个数值。

Reduce最有意思的一点是,他入参函数的第一个参数是上一个的结果值,因此它才有了合并的意义。我们可以想象成拿着框剪苹果,左右是框,右手是苹果,就是这个意思。

小明聪明的大脑正在高速思考,Reduce有什么用呢?啊哈,太有用了,比如可以统计所有学生的总分数,虽然好像没什么意义。

let totalScore = students.map{
      $0.score }.reduce(0){
      $0 + $1}
print(totalScore)

425

偏函数和纯函数

小明实在是太兴奋了,Map,FilterReduce完全是一套变化无穷的剑法,就像打dota一样,大家使用的所有东西都是一样的,为何你是菜鸡?除了天赋,高手还有自己的小秘密,嘿嘿。

如果问你偏函数是什么?你嘿嘿一笑,说那不是微积分的知识吗?no,在编程的世界中,偏函数指的是该函数的返回值是一个函数,就好像在说,你以为这个函数是这样的,其实它是那样的。偏了。

老师又给小明提出了3个新的需求:

  • 筛选出age大于20岁的同学
  • 筛选出score大于90分的同学
  • 筛选出age大于20并且score大于90分的同学

小明觉得太简单了,用高阶函数来实现,十分easy啊,于是开始写代码:

/// 筛选 age > 20
print(students.filter{
      $0.age > 20}.map{
      $0.name })

/// 筛选 score > 90
print(students.filter{
      $0.score > 90}.map{
      $0.name })

/// 筛选 age > 20 && score > 90
print(students.filter{
      $0.age > 20 && $0.score > 90 }.map{
      $0.name })
["Annabelle", "Polly"]
["Annabelle", "Polly", "Maud"]
["Annabelle", "Polly"]

其实,到这里,我觉得应该点到为止了,哈哈,对于大部分同学来说,写出这样的代码已经足够优秀了,我很怕,如果再进一步的话,某些同学可能要爆炸了。

不过我们还要继续,我们把上边的代码改造成偏函数:

enum FilterType {
     
    case ageGreaterThan20
    case scoreGreaterThan90
    case ageGreaterThan20AndScoreGreaterThan90
}

func filter(for filterType: FilterType) -> ([Student]) -> [String] {
     
    return {
      students in
        switch filterType {
     
        case .ageGreaterThan20:
            return students.filter{
      $0.age > 20}.map{
      $0.name }
        case .scoreGreaterThan90:
            return students.filter{
      $0.score > 90}.map{
      $0.name }
        case .ageGreaterThan20AndScoreGreaterThan90:
            return students.filter{
      $0.age > 20 && $0.score > 90 }.map{
      $0.name }
        }
    }
}

其实,并不难,filter()函数的核心思想就是根据参数filterType返回不同的函数。当我们需要过滤数据的时候,就会像下边这样使用:

print(filter(for: .ageGreaterThan20)(students))
print(filter(for: .scoreGreaterThan90)(students))
print(filter(for: .ageGreaterThan20AndScoreGreaterThan90)(students))
["Annabelle", "Polly"]
["Annabelle", "Polly", "Maud"]
["Annabelle", "Polly"]

我们再研究一个有点极端的例子:

infix operator ^^
func ^^ (radix: Int, power: Int) -> Int {
     
    return Int(pow(Double(radix), Double(power)))
}

我们可以自定义操作符,在上边的代码中,infix表示要定义中间的操作符,在两个数中间。

print("2³ = (2 ^^ 3)")

2³ = 8

我们再简单聊聊什么是纯函数,只看名字我们也能猜个大概,它应该是一个很“纯”的函数,一般指的是很单一的东西。因此一个纯函数必须满足一下两个条件:

  • 相同的入参必定输出相同结果,也就是说它只依赖入参
  • 函数没有任何副作用

简而言之,纯函数是数据流模式的基础,在响应式编程的世界里,数据在pipline中随意流动,它就不应该存在任何副作用,pipline的输入输出存在确定性。

小明随手写下了一个纯函数:

func studentsWithAgeUnder(_ age: Int, from students: [Student]) -> [Student] {
     
    return students.filter{
      $0.age < age }
}

print(studentsWithAgeUnder(18, from: students))

[__lldb_expr_1.Student(name: "Maud", age: 16, score: 95)]

引用透明

网上的相关资料很多,在这里就不做更多解释了,对于纯函数,它就是引用透明的,编译器可以对引用透明的函数做优化,比如,如果某个函数是引用透明的,编译器可以缓存入参和输出,当遇到相同入参的时候,直接使用缓存的输出结果。

递归

说到递归,是一个令人头疼的玩意,要想精通算法,则必须要战胜递归。我们在上边的快排中就用到了递归:

extension Array where Element: Comparable {
     
    func quickSorted() -> [Element] {
     
        if self.count > 1 {
     
            let (pivot, remaining) = (self[0], dropFirst())
            let lhs = remaining.filter{
      $0 <= pivot}
            let rhs = remaining.filter{
      $0 > pivot}
            return lhs.quickSorted() + [pivot] + rhs.quickSorted()
        }
        return self
    }
}

在下个人觉得,理解递归需要从两个维度:

  • 宏观角度,以目的为导向,以上边的代码为例,宏观思想就是拿到一个数据,然后把小于它的数据放到左边,大于的数据放到右边
  • 微观角度,对局部再此应用宏观规则,当我们获得了左边的数据后,左边的数据并不是排好序的,因此,我们对这一局部数据再次应用宏观规则,于是就产生了递归

因此,我们得出这样的结论,递归产生在局部数据里,当然,别忘了给出打破递归的条件。

命令式和声明式编程

小明最近自学了SwiftUI和Flutter,已经深深爱上了这种声明式的编程风格。在上边的很多小节中,相信大家应该对声明式编程有了深刻的体会。

我在这里不想说太多,放上两段代码,大家自己对比下:

override func viewDidLoad() {
     
        super.viewDidLoad()
        let label = UILabel(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        label.text = "世界和平"
        label.textColor = .purple
        view.addSubview(label)
    }

vs

var body: some View {
     
     Text("世界和平")
            .color(.purple) 
    }
  • 声明式的核心思想是描述你想要什么?
  • 命令式的核心是想是如何去做?

参考

十大经典排序算法(动图演示)

Swift自定义操作符

An Introduction to Functional Programming in Swift

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_39827306/article/details/110499340

智能推荐

linux之EXT2文件系统--理解block/block group/索引结点inode/索引位图_网络安全研发随想的博客-程序员宅基地_block group

1. 基本概念我们都知道,磁盘是存储文件用的,但是磁盘必须先格式化为某种格式的文件系统,才能存储文件。文件系统的目的就是组织和管理磁盘中的文件。上图展示了在ext2文件系统下,磁盘的存储空间是如何被组织的。为了理解上图,我们先了解一些基本概念。1.1 块(block)磁盘中最小存储单元是扇区(1扇区=512Bytes),而文件系统的最小存储单元是Block(一般地,1Block = ...

班级聚会归来__棋棋的博客-程序员宅基地

高中同学聚会,见了很多多年不见的同学,很开心。希望以后能多见面。有件事情比较纠结,订阅了2014年的程序员杂志,付款之后没有任何提示,我不知道如何确定我已经订购成功。工作人员要7号才能上班,哎,纠结。很期待看到2月份的大数据专辑。

CSS:省略号_weixin_34381666的博客-程序员宅基地

2019独角兽企业重金招聘Python工程师标准&gt;&gt;&gt; ...

经典收藏 50个jQuery Mobile开发技巧集萃_weixin_34356555的博客-程序员宅基地

1、Backbone移动实例这是在Safari中运行的一款Backbone移动应用程序。想开始体验移动开发,一个好的出发点就是关注这个应用程序的构建方式。先不妨在你的浏览器中查看该应用程序。相关链接:http://bennolan.com/2010/11/24/backbone-jquery-demo.html2、使用媒体查询来锁定设备你可能会问如何使用CSS来锁定设备(根...

apk安装时报错 INSTALL PARSE FAILED MANIFEST MALFORMED_流沙的那岐的博客-程序员宅基地_installation failed due to: 'install_parse_failed_

出现这个问题一般有两种原因:1.是手机上有同一个包名但是不同签名的软件,可能一个用的debug签名 一个是正式签名。解决方式就是卸载手机上已经安装的版本2.就是AndroidManifest.xml中出错了,由于manifest中的代码一般都是自动生成的,所以出错的原因大概又分类两类:   2.1包名必须是小写的,下面的包名就是大写了,所以报错   

随笔小记(二)_花开 流年的博客-程序员宅基地_hexlify

class int(x, base=10):函数用于将一个字符串或数字转换为整型。x – 字符串或数字。base – 进制数,默认十进制。int(‘12’,16):如果是带参数base的话,12要以字符串的形式进行输入,12 为 16进制。结果为18(十六进制12的十进制形式是18)。hexlify和b2a_hex相似,建议使用hexlify。作用是返回的二进制数据的十六进制表示。每一个字节的数据转换成相应的2位十六进制表示。hexst = binascii.hexlify(content)按字

随便推点

玩转华为数据中心交换机系列 | 配置交换机双归接入IP网络示例_COCOgsta的博客-程序员宅基地_华为交换机配置双活

素材来源:华为数据中心交换机配置指南一边学习一边整理试验笔记,并与大家分享,侵权即删,谢谢支持!附上汇总贴:玩转华为数据中心交换机系列 | 汇总_COCOgsta的博客-程序员宅基地组网需求如图1所示,通过配置M-LAG双归接入IP网络可以满足以下要求:当一条接入链路发生故障时,流量可以快速切换到另一条链路,保证可靠性。为了高效利用带宽,两条链路同时处于active状态,可实现使用负载分担的方式转发流量。配置思路采用如下的思路配置M-LAG双归接入IP网络:在Swit

Go技术日报(2021-12-22)——Go101作者又出新作:Go优化101_韩亚军的博客-程序员宅基地

每日一谚:Simple, Poetic, Pithy .GOCN每日新闻--2021-12-22 1.使用 Go 和 SQLite 构建生产应用程序2.使用 context.Context 模拟 API 客户端 3.一种可嵌入的 Go 脚本语言,实现了逻辑编程语言 Prolog 4.SSA:终于知道编译器偷摸做了哪些事 5.从 Golang 调用 C 代码 gopherDaily--2021-12-22 Go101作者又出新作:Go优化101聊聊Go 1.18的三个小特性 纯Go实.

ulimit的说明_weixin_34342905的博客-程序员宅基地

ulimit工具是性能调优的简单工具而且也是Linux内置的一个功能,它的目的是用于控制由Shell运行的进程所能使用的系统最大资源。其实在生产环境中部署Linux后通常都会用这个工具去调整一些参数来交付使用,当然这个过程往往都是自动完成的,工具使用很简单,但是它涉及的知识还是比较多的。1ulimit-a查看所有限制,如果不...

Element 2.7.0 发布,基于 Vue 2.0 的桌面端组件库_weixin_33984032的博客-程序员宅基地

Element 2.7.0 发布了,Element 是一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库,提供了配套设计资源,帮助你的网站快速成型。由饿了么公司前端...

Android 5.1代码编译报错 error: unsupported reloc 43_进阶牛牛的博客-程序员宅基地_unsupported reloc 43

在编译Android 5.1代码时遇到如下报错prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6//x86_64-linux/bin/ld: error: out/host/linux-x86/obj32/SHARED_LIBRARIES/libnativehelper_intermediates/JNIHelp.o: unsupported reloc 43 against global symbol std::string::_Rep::_

Oracle 11g R2 for Win7旗舰版(64位)的安装步骤_南华的博客-程序员宅基地

原版网址: http://www.supportopensource.iteye.com/blog/1046171 Oracle 11g R2 for Win7旗舰版(64位)的安装步骤博客分类:Oracle基础Oracle配置管理WindowsHTML1、下载Oracle 11g R2 for Windows的版本下载地址:http://www.oracl

推荐文章

热门文章

相关标签