开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Cats(一):从函数式编程思维谈起

发表于 2017-11-30

本文由 Yison 发表在 ScalaCool 团队博客。

Cats logo

如果你看到一个开源类库,logo 是四只猫 + 五个箭头,可能会略感不适 — 这是工程代码里可以使用的玩意儿吗?

没错,这是 TypeLevel 设计的一个函数式类库,一群推崇函数式编程的家伙注入到 Scala 生态中的重磅炸弹,它是 Cats,源于 Category(范畴)的缩写。

在 水滴 的系统中,我们大规模使用了 Cats 来解决一些业务问题,并且从中受益。但显然这并不是 Scala 标准库所支持的打法,所以本系列旨在系统地介绍这个类库,让更多人了解它。

我们最开始使用的是 Scalaz,它是 Cats 的前身,由于语法问题导致很多吐槽,之后采用 Cats 替代。

当然,很多了解过 Cats 的人知道,关于它已经有以下这些优秀的学习资料:

  • herding cats,同样也是 learning scalaz 的作者。
  • Scala with Cats Book,出自 underscore.io

但显然,如果之前并没有函数式编程经验的同学,可能会在首次阅读这些资料的时候犯困。因此,该系列希望在正式介绍 Cats 这个神器之前,先友好地探讨一些关于「函数式编程」的基本问题。如:

  • 什么是函数式编程
  • 函数式编程有哪些特点
  • 函数式编程有哪些优点

函数式编程?

那么,什么才是函数式编程呢?其实,关于这点并没有准确权威的定义,本文也不想就此给出一个答案。

但是,我们希望来讨论下什么是「函数式编程思维」,它跟我们熟知的「命令式编程」到底有哪些不同。

经常上知乎的朋友发现了,这是知乎上一个非常好的问题。

什么是函数式编程思维?— 知乎

本文推荐以下的回答:

@nameoverflow:
函数式编程关心数据的映射,命令式编程关心解决问题的步骤。

更数学化的版本:
@parker liu
函数式编程关心类型(代数结构)之间的关系,命令式编程关心解决问题的步骤。

总结

函数式编程的思维就是如何将类型(代数结构)之间的关系组合起来,用数学的构造主义构造出你设计的程序。

疑问

我们来解剖这个结论,直观上函数式编程是一件非常简单的事情,我们只需做一件事情就够了,那就是「组合」。

但此时,我们肯定又变得一头雾水,以下问题紧接着就来了:

  • 真的有这么简单吗?
  • 到底什么是「组合」呢?
  • 在理论上,它真能做到完备性吗?
  • 什么才是所谓的「关系」呢?

别急,我们先来讨论一个基本的问题 — 什么是过程和数据?

过程和数据

看过 SICP 的朋友肯定已经发现,这是这本书第一章和第二章所研究的内容。

简单来说,数据是一种我们希望去操作的东西,而过程就是有关操作这些数据的规则的描述。它们构成了程序设计的基本元素。

在 Lisp 这种函数式编程语言中,过程和数据一样,属于第一级状态,这也就意味着:

  • 可以用变量命名
  • 可以提供给过程作为参数
  • 可以作为过程的结果返回
  • 可以包含在其它的数据结构中

举个例子,我们可以定义一个过程,它接受的参数是一个过程,返回的是另外一个过程,这似乎看起来有点怪。
换个例子,定义一个过程,它接受的参数是一个数,返回的是另外一个数,这是不是就熟悉多了?

在函数式编程中,我们会发现其实「过程」和「数据」的界限有时候是模糊的。也就是说,有时我们可以把它们当作一个东西。

回到我们刚才的结论:「函数式编程关心类型(代数结构)之间的关系」。

我们于是可以这么理解,函数式编程要解决的第一个问题:就是需要足够高的抽象能力,能对各种数据和过程进行抽象,提供类型(代数结构)。

这也同样是后续我们在学习 Cats 过程中要获得解答的一个疑问,它是如何帮助我们实现这一点。

推荐阅读 @shaw 写的 如何在 Scala 中利用 ADT 良好地组织业务

图灵完备与 Lambda 演算

其次,我们再来讨论下,到底什么是所谓的「关系」?

我们肯定有这样子的疑惑,如果函数式编程思维仅靠「组合」就能够描述所有的程序,那么在理论上它必须具备完备性。

不少朋友知道所谓的 [图灵完备](Turing completeness)。它听起来逼格很高,其实这是一个很简单的概念,意味着用图灵机能做到的所有事情,可以解决所有的可计算问题。

另外一个可支持解决所有可计算问题的方案就是「Lambda 演算」。在 Lisp 这种函数式编程语言中的基石,就是这个理论。

函数式编程中的 lambda 可以看成是两个类型之间的关系,一个输入类型和一个输出类型。lambda 演算就是给 lambda 表达式一个输入类型的值,则可以得到一个输出类型的值,这是一个计算,计算过程满足 \alpha -等价和 \beta -规约。

关于图灵完备和 Lambda 演算,有机会我们可以在后续的文章中继续讨论。

面向组合子编程

我们再来聊聊核心,所谓的「组合」。

「面向组合子编程」是十年前 javaeye 的牛人 @Ajoo 提出的概念。

首先,我们可以引用哲学的基本方法来比喻我们常见的「面向对象编程」与「面向组合子编程」差异。

前者是归纳法,后者是演绎法。

也就说,我们在用 Java 这些面向对象的语言进行程序设计的时候,通常采用的是总结的方法,然而函数式编程语言提倡的「组合」,更贴近数学的思维,它是一种推导。

所以,函数式编程所关心的组合,更多做的是先高度抽象类型关系,然后对这些关系的转化,所谓的 transformer。

于是,我们得出第二个关键的问题:即 Cats 如何提供足够的 transformer,来帮助我们实现各种关系之间的组合。

举例

对于第一次接触这些概念的朋友来说,还是有点抽象,下面我们来举一个实际的例子来加深认识。

假设我们现在要设计一个抽奖活动的参与过程,涉及以下逻辑:

  • 获取活动奖品数据
  • 判断活动的开始、进行、结束、奖品是否抢光等状态

命令式风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码import org.joda.time.DateTime
import scala.concurrent.Future

case class Activity(id: Long, start: DateTime, end: DateTime)
case class Prize(id: Long, name: String, count: Int)

val activity = syncGetActivity()
val prizes = syncGetPrizes(activity.id)

if (activity.start.isBefore(DateTime.now())) {
println("activity not starts")
} else if (activity.end.isBefore(DateTime.now())) {
println("activity ends")
} else if (prizes.map(_.count).sum < 1) {
println("activity has no prizes")
} else {
println("activity is running")
}

函数式风格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码import org.joda.time.DateTime
import scala.concurrent.Future

case class Activity(id: Long, start: DateTime, end: DateTime)
case class Prize(id: Long, name: String, count: Int)

sealed trait ActivityStatus {
val activity: Activity
val prizes: Seq[Prize]
}
case class ActivityNotStarts(activity: Activity, prizes: Seq[Prize]) extends ActivityStatus
case class ActivityEnds(activity: Activity, prizes: Seq[Prize]) extends ActivityStatus
case class ActivityPrizeEmpty(activity: Activity, prizes: Seq[Prize]) extends ActivityStatus
case class ActivityRunning(activity: Activity, prizes: Seq[Prize]) extends ActivityStatus

def getActivityStatus(): Future[ActivityStatus] = {
for {
activity <- asyncGetActivity()
prizes <- asyncGetPrizes(activity.id)
} yield (activity, prizes) match {
case (a, pzs) if a.start.isBefore(DateTime.now()) => ActivityNotStarts(a, pzs)
case (a, pzs) if a.end.isBefore(DateTime.now()) => ActivityNotStarts(a, pzs)
case (a, pzs) if pzs.map(_.count).sum < 1 => ActivityPrizeEmpty(a, pzs)
case (a, pzs) => ActivityRunning(a, pzs)
}
}

以上,我们可以发现函数式风格,会倾向于基于更高的业务层次进行抽象,直觉上是一个 describe what 的设计,而不是 how to do。

值得一提的是,asyncGetActivity 这个从数据库异步获取活动数据过程,它的类型是一个高阶类型 Future[Activity],这也就是我们之前提到的对过程进行抽象,定义类型。

通过对 asyncGetActivity 和 asyncGetPrizes 两个异步过程的组合,我们最终转化得到了 ActivityStatus 这个类型的对象结果。

总结

Scala 是一门结合「面向对象」和「函数式」的编程语言,我们用它可以写出截然不同的代码风格。很多人把它当作 better Java 来使用,但如果结合 Cats 这个函数式类库,我们就可以更好地采用函数式编程思维来设计程序,从而发挥 Scala 更大的威力。

通过该篇文章,我们对函数式编程有了直觉上的感受。当然,你可能依旧云里雾里,不要紧,我们会在后续的文章里进一步的讨论。在下一篇文章中,我们会介绍下函数式编程所带来的优势。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

谷歌开源Docker镜像分析比对工具container-di

发表于 2017-11-30

谷歌发布了一个叫作container-diff的工具,用于分析比对Docker镜像。它支持文件系统比对,并能够感知到由apt、npm和pip这些包管理器所带来的变更。

Dockerfile用于创建容器镜像,一旦Dockerfile发生变更,就需要重新创建新的镜像。Dockerfile是普通的文本文件,使用源码控制系统的diff工具就可以比较出它们之间的区别。不过,要对Dockerfile文件里的命令所产生的镜像变更记进行可视化,或者列出具体的镜像变更内容就很困难。应用程序被打包到镜像之后,如果依赖了第三方特定版本的依赖项,事情就会变得复杂,况且,下游的依赖项也会让跟踪变得更加复杂。未被跟踪的依赖项会导致镜像体积膨胀,让镜像下载时间变长。

container-diff会分析镜像的“语义”差别,将结果以一种用户可理解的格式呈现给用户,这样用户就可以采取相应的行动。container-diff支持Python的pip包管理器、Linux上的apt工具以及node.js包管理器npm。另外,它还可以用于分析文件系统的变化。该工具可以一次性分析一个或几个甚至所有包管理器的内容。

在分析镜像时,可以指定本地的Docker后台路径、远程的镜像仓库地址或文件路径。如果已经使用Docker的保存命令导出镜像,那么可以使用后者。在使用该工具分析后台镜像时,镜像不需要处于运行状态。该工具还能输出单个镜像的修改历史。

其他类似的工具还有Anchore的diff工具以及Atomic项目的“atomic diff”命令。Docker的“docker
history”命令只能列出每个Dockerfile的变更历史,这个只需要检查一下Dockerfile就知道了。一些反向工程可以揭示底层的一些细节,但很难将其抽取成事件,比如之前安装了哪些包。Atomic的工具可以列出文件系统的差别,而且可以用在RPM上,也就是说,它可以列出安装了哪些RPM包。另外,“atomic
diff”命令可以用于比较两个容器、容器和镜像、两个镜像之间的差别。

根据谷歌的文章所述,container-diff可以成为开发流程的一部分,可以与持续集成系统集成起来,提供自动化的变更日志管理,而且它的输出结果是JSON格式的。如果镜像处于仓库当中,不管是私有仓库还是像Google Container Registry这样的仓库,container-diff都为它们提供了认证机制,这个认证机制是通过docker-credentials-helpers包来实现的。这个包使用本地程序(比如OSX上的osxkeychain)来保存Docker认证信息。

查看英文原文:container-diff - an Open Source Tool from Google for Analyzing Differences Between Docker Images

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

中小型研发团队架构实践:Redis快速入门及应用

发表于 2017-11-30

Redis的使用难吗?不难,Redis用好容易吗?不容易。Redis的使用虽然不难,但与业务结合的应用场景特别多、特别紧,用好并不容易。我们希望通过一篇文章及Demo,即可轻松、快速入门并学会应用。

一、Redis 简介

Redis是一个开源的Key-Value存储,但又不仅仅是Key-Value存储,用官网上的话来说,Redis是一个数据结构存储,可用作数据库、缓存和消息中间件。相对于传统的Key-Value存储Memcached来说,Redis具有如下特点:

  • 速度快
  • 丰富的数据结构,除String之外,还有List、Hash、Set、Sorted Set
  • 单线程,避免了线程切换和锁的性能消耗
  • 原子操作
  • 可持久化(RDB与AOF)
  • 发布/订阅
  • 支持Lua脚本
  • 分布式锁
  • 事务
  • 主从复制与高可用(Redis Sentinel)
  • 集群(3.0版本以上)

二、Redis 数据结构

1、String

这是最简单的Redis类型。如果只使用这种类型,Redis就像一个可持久化的Memcached服务器。

2、List

Redis的List是基于双向链表实现的,可以支持反向查找和遍历。

常用案例:聊天系统、社交网络中获取用户最新发表的帖子、简单的消息队列、新闻的分页列表、博客的评论系统。

3、Hash

Hash是一个String类型的field和value之间的映射表,请见下图,类似于.NET中的Hashtable和Dictionary。主要用来存储对象,可以避免序列化的开销和并发修改控制的问题。

4、Set

Set也是一个列表,不过它的特殊之处在于它是可以自动排重的:当需要存储一个列表数据,而又不希望出现重复的时候,Set是一个很好的选择(比如ID的集合)。并且Set提供了判断某个成员是否在一个Set集合内的接口,这也是List所没有的。

5、Sorted Set

Sorted Set和Set的使用场景类似,区别是Sorted Set会根据提供的score参数来进行自动排序。当你需要一个有序的并且不重复的集合列表,那么就可以选择Sorted Set数据结构。常用案例:游戏中的排行榜。

三、 Redis 重要特性

以下特性请重点看管道和事务。

1、管道

Redis管道是指客户端可以将多个命令一次性发送到服务器,然后由服务器一次性返回所有结果。管道技术在批量执行命令的时候可以大大减少网络传输的开销,提高性能。

2、事务

Redis事务是一组命令的集合。一个事务中的命令要么都执行,要么都不执行。如果命令在运行期间出现错误,不会自动回滚。

管道与事务的区别:管道主要是网络上的优化,客户端缓冲一组命令,一次性发送到服务器端执行,但是并不能保证命令是在同一个事务里面执行;而事务是原子性的,可以确保命令执行的时候不会有来自其他客户端的命令插入到命令序列中。

3、分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作,如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

4、地理信息

从Redis 3.2版本开始,新增了地理信息相关的命令,可以将用户给定的地理位置信息(经纬度)存储起来,并对这些信息进行操作。

四、 使用方法

步骤1、在需要使用Redis的项目中引用FxCommon.dll和Redis.dll。

步骤2、在App.config或Web.config文件中添加如下配置:

1
2
3
4
5
6
复制代码__Thu Nov 30 2017 09:57:28 GMT+0800 (CST)____Thu Nov 30 2017 09:57:28 GMT+0800 (CST)__<add key="RedisServerIP" value="redis:uuid845tylabc123@139.198.13.12:4125"/>
<!-- 提供的 Redis 环境是单机版配置。如果 Redis 是主从配置,则还需设置 RedisSlaveServerIP-->
<!--<add key="RedisSlaveServerIP" value="redis:uuid845tylabc123@139.198.13.13:4125"/>-->

<!--Redis 数据库。如果不需要指定 Redis 数据库,就配置默认值 0-->
<add key="RedisDefaultDb" value="0"/>__Thu Nov 30 2017 09:57:28 GMT+0800 (CST)____Thu Nov 30 2017 09:57:28 GMT+0800 (CST)__

步骤 3、使用 PooledRedisClientManager 类创建 Redis 连接池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
复制代码__Thu Nov 30 2017 09:57:28 GMT+0800 (CST)____Thu Nov 30 2017 09:57:28 GMT+0800 (CST)__// 读取 Redis 主机 IP 配置信息
string[] redisMasterHosts = ConfigurationManager.ConnectionStrings["RedisServerIP"].ConnectionString.Split(',');

// 如果 Redis 服务器是主从配置,那么还需要读取 Redis Slave 机的 IP 配置信息
string[] redisSlaveHosts = null;
var slaveConnection = ConfigurationManager.ConnectionStrings["RedisSlaveServerIP"];
if (slaveConnection != null && !string.IsNullOrWhiteSpace(slaveConnection.ConnectionString))
{
string redisSlaveHostConfig = slaveConnection.ConnectionString;
redisSlaveHosts = redisSlaveHostConfig.Split(',');
}

// 读取 RedisDefaultDb 配置
int defaultDb = 0;
string defaultDbSetting = ConfigurationManager.AppSettings["RedisDefaultDb"];
if (!string.IsNullOrWhiteSpace(defaultDbSetting))
{
int.TryParse(defaultDbSetting, out defaultDb);
}

var redisClientManagerConfig = new RedisClientManagerConfig
{
MaxReadPoolSize = 50,
MaxWritePoolSize = 50,
DefaultDb = defaultDb
};

// 创建 Redis 连接池
Manager = new PooledRedisClientManager(redisMasterHosts, redisSlaveHosts, redisClientManagerConfig)
{
PoolTimeout = 2000,
ConnectTimeout = 500
};__Thu Nov 30 2017 09:57:28 GMT+0800 (CST)____Thu Nov 30 2017 09:57:28 GMT+0800 (CST)__

步骤4、通过PooledRedisClientManager的实例获取Redis客户端,然后就可以开始通过Redis客户端的API进行操作。

五、其它

5.1、 Redis Key命名规范

Redis Key命名规范:AppID:KeyName。

可能有很多人习惯用英文状态的点号来作为AppID和KeyName的分隔符,而笔者建议使用冒号作为AppID和KeyName的分隔符,其原因是:这么写会使Redis Key会以AppID作为分类显示在Redis Desktop Manager中,方便你能够快速查到要查阅的Redis Key对应的Redis Value值,请见下图:

)

但如果使用英文状态的点号来作为分隔符的话,那么在Redis Desktop Manager中,Redis Key就不会被分类了,请见下图:

)

5.2、常见应用问题

  • 缓存穿透处理:什么是缓存穿透?当根据Redis key在缓存中查询后,不存在对应Value,就应该会在后端系统如DB中去查找,该Key的并发请求量一旦变大,那么就会对DB造成很大的压力。解决办法有:a.前端风险控制,将恶意穿透情况排除在外;b.对查询结果为空的情况依然进行缓存,但缓存时间会设置得很短,一般是几分钟。
  • 缓存雪崩处理:什么是缓存雪崩?当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。解决办法有:后端连接数限制,错误阈值限制,超时处理,缓存失效时间均匀分布,前端永不失效及后端主动更新。
  • 缓存时长:策略定位复杂,需要多维度的计算。
  • 缓存失效:按时失效,事件失效,后端主动更新。
  • 缓存Key:Hash、规则、前缀+Hash,异常情况可人工干预。
  • Lua脚本:服务端批量处理及事务能力,有条件逻辑的可扩展脚本。使用它的好处有:减少网络开销、原子操作、可复用。
  • Limit:可滑动时间窗口,如应用于Session,Memcached需每次传Key和Value。

六、Demo 下载及更多资料

  • RedisDemo下载地址:github.com/das2017/Red…
  • RedisDesktopManage下载地址:redisdesktop.com/
  • Redis官网:redis.io/
  • ServiceStack.Redis客户端:github.com/ServiceStac…
  • Redis命令大全:www.redis.cn/commands.ht…

本系列文章涉及内容清单如下,其中有感兴趣的,欢迎关注:

  • 开篇:中小型研发团队架构实践三要点
  • 缓存 Redis
  • 消息队列 RabbitMQ:如何用好消息队列RabbitMQ?
  • 集中式日志 ELK
  • 任务调度 Job:中小型研发团队架构实践之任务调度Job
  • 应用监控 Metrics:应用监控怎么做?
  • 微服务框架 MSA
  • 搜索利器 Solr
  • 分布式协调器 ZooKeeper
  • 小工具:Dapper.NET/EmitMapper/AutoMapper/Autofac/NuGet
  • 发布工具 Jenkins
  • 总体架构设计:电商如何做企业总体架构?
  • 单个项目架构设计
  • 统一应用分层:如何规范公司所有应用分层?
  • 调试工具 WinDbg
  • 单点登录
  • 企业支付网关
  • 结篇

作者介绍

张辉清,10 多年的 IT 老兵,先后担任携程架构师、古大集团首席架构、中青易游 CTO 等职务,主导过两家公司的技术架构升级改造工作。现关注架构与工程效率,技术与业务的匹配与融合,技术价值与创新。

杨丽,拥有多年互联网应用系统研发经验,曾就职于古大集团,现任职中青易游的系统架构师,主要负责公司研发中心业务系统的架构设计以及新技术积累和培训。现阶段主要关注开源软件、软件架构、微服务以及大数据。

感谢雨多田光对本文的审校。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 8 习惯用语 级联 lambda 表达式 级联 l

发表于 2017-11-29

Java 8 习惯用语

级联 lambda 表达式

可重用的函数有助于让代码变得非常简短,但是会不会过于简短呢?

系列内容:

此内容是该系列的一部分:Java 8 习惯用语

在函数式编程中,函数既可以接收也可以返回其他函数。函数不再像传统的面向对象编程中一样,只是一个对象的工厂或生成器,它也能够创建和返回另一个函数。返回函数的函数可以变成级联
lambda 表达式
,特别值得注意的是代码非常简短。尽管此语法初看起来可能非常陌生,但它有自己的用途。本文将帮助您认识级联 lambda 表达式,理解它们的性质和在代码中的用途。

神秘的语法

您是否看到过类似这样的代码段?

1
复制代码x -> y -> x > y

如果您很好奇“这到底是什么意思?”,那么您并不孤单。对于不熟悉使用 lambda 表达式编程的开发人员,此语法可能看起来像货物正从快速行驶的卡车上一件件掉下来一样。

幸运的是,我们不会经常看到它们,但理解如何创建级联 lambda 表达式和如何在代码中理解它们会大大减少您的受挫感。

高阶函数

在谈论级联 lambda 表达式之前,有必要首先理解如何创建它们。对此,我们需要回顾一下高阶函数(已在本系列第 1
篇文章
中介绍)和它们在函数分解中的作用,函数分解是一种将复杂流程分解为更小、更简单的部分的方式。

首先,考虑区分高阶函数与常规函数的规则:

常规函数

  • 可以接收对象
  • 可以创建对象
  • 可以返回对象

高阶函数

  • 可以接收函数
  • 可以创建函数
  • 可以返回函数

开发人员将匿名函数或 lambda 表达式传递给高阶函数,以让代码简短且富于表达。让我们看看这些高阶函数的两个示例。

示例 1:一个接收函数的函数

在 Java™ 中,我们使用函数接口来引用 lambda 表达式和方法引用。下面这个函数接收一个对象和一个函数:

1
2
3
4
5
6
7
复制代码public static int totalSelectedValues(List<Integer> values, 
  Predicate<Integer> selector) {
   
  return values.stream()
    .filter(selector)
    .reduce(0, Integer::sum); 
}

totalSelectedValues 的第一个参数是集合对象,而第二个参数是 Predicate 函数接口。 因为参数类型是函数接口 (Predicate),所以我们现在可以将一个 lambda 表达式作为第二个参数传递给 totalSelectedValues。例如,如果我们想仅对一个 numbers 列表中的偶数值求和,可以调用 totalSelectedValues,如下所示:

1
复制代码totalSelectedValues(numbers, e -> e % 2 == 0);

假设我们现在在 Util 类中有一个名为 isEven 的 static 方法。在此情况下,我们可以使用 isEven 作为 totalSelectedValues 的参数,而不传递 lambda 表达式:

1
复制代码totalSelectedValues(numbers, Util::isEven);

作为规则,只要一个函数接口显示为一个函数的参数的类型,您看到的就是一个高阶函数。

示例 2:一个返回函数的函数

函数可以接收函数、lambda 表达式或方法引用作为参数。同样地,函数也可以返回 lambda 表达式或方法引用。在此情况下,返回类型将是函数接口。

让我们首先看一个创建并返回 Predicate 来验证给定值是否为奇数的函数:

1
2
3
4
复制代码public static Predicate<Integer> createIsOdd() {
  Predicate<Integer> check = (Integer number) -> number % 2 != 0;
  return check;
}

为了返回一个函数,我们必须提供一个函数接口作为返回类型。在本例中,我们的函数接口是 Predicate。尽管上述代码在语法上是正确的,但它可以更加简短。 我们使用类型引用并删除临时变量来改进该代码:

1
2
3
复制代码public static Predicate<Integer> createIsOdd() {
  return number -> number % 2 != 0;
}

这是使用的 createIsOdd 方法的一个示例:

1
2
3
复制代码Predicate<Integer> isOdd = createIsOdd();
 
isOdd.test(4);

请注意,在 isOdd 上调用 test 会返回 false。我们也可以在 isOdd 上使用更多值来调用 test;它并不限于使用一次。

创建可重用的函数

现在您已大体了解高阶函数和如何在代码中找到它们,我们可以考虑使用它们来让代码更加简短。

设想我们有两个列表 numbers1 和 numbers2。假设我们想从第一个列表中仅提取大于 50 的数,然后从第二个列表中提取大于 50 的值并乘以 2。

可通过以下代码实现这些目的:

1
2
3
4
5
6
7
8
复制代码List<Integer> result1 = numbers1.stream()
  .filter(e -> e > 50)
  .collect(toList());
   
List<Integer> result2 = numbers2.stream()
  .filter(e -> e > 50)
  .map(e -> e * 2)
  .collect(toList());

此代码很好,但您注意到它很冗长了吗?我们对检查数字是否大于 50 的 lambda 表达式使用了两次。 我们可以通过创建并重用一个 Predicate,从而删除重复代码,让代码更富于表达:

1
2
3
4
5
6
7
8
9
10
复制代码Predicate<Integer> isGreaterThan50 = number -> number > 50;
 
List<Integer> result1 = numbers1.stream()
  .filter(isGreaterThan50)
  .collect(toList());
   
List<Integer> result2 = numbers2.stream()
  .filter(isGreaterThan50)
  .map(e -> e * 2)
  .collect(toList());

通过将 lambda 表达式存储在一个引用中,我们可以重用它,这是我们避免重复 lambda 表达式的方式。如果我们想跨方法重用 lambda 表达式,也可以将该引用放入一个单独的方法中,而不是放在一个局部变量引用中。

现在假设我们想从列表 numbers1 中提取大于 25、50 和 75 的值。我们可以首先编写 3 个不同的 lambda 表达式:

1
2
3
4
5
6
7
8
9
10
11
复制代码List<Integer> valuesOver25 = numbers1.stream()
  .filter(e -> e > 25)
  .collect(toList());
 
List<Integer> valuesOver50 = numbers1.stream()
  .filter(e -> e > 50)
  .collect(toList());
 
List<Integer> valuesOver75 = numbers1.stream()
  .filter(e -> e > 75)
  .collect(toList());

尽管上面每个 lambda 表达式将输入与一个不同的值比较,但它们做的事情完全相同。如何以较少的重复来重写此代码?

创建和重用 lambda 表达式

尽管上一个示例中的两个 lambda 表达式相同,但上面 3 个表达式稍微不同。创建一个返回 Predicate 的 Function 可以解决此问题。

首先,函数接口 Function<T, U> 将一个 T 类型的输入转换为 U 类型的输出。例如,下面的示例将一个给定值转换为它的平方根:

1
复制代码Function<Integer, Double> sqrt = value -> Math.sqrt(value);

在这里,返回类型 U 可以很简单,比如 Double、String 或 Person。或者它也可以更复杂,比如 Consumer 或 Predicate 等另一个函数接口。

在本例中,我们希望一个 Function 创建一个 Predicate。所以代码如下:

1
2
3
4
5
6
7
复制代码Function<Integer, Predicate<Integer>> isGreaterThan = (Integer pivot) -> {
  Predicate<Integer> isGreaterThanPivot = (Integer candidate) -> {
    return candidate > pivot;
  };
   
  return isGreaterThanPivot;
};

引用 isGreaterThan 引用了一个表示 Function<T, U>— 或更准确地讲表示 Function<Integer, Predicate<Integer>> 的 lambda 表达式。输入是一个 Integer,输出是一个 Predicate<Integer>。

在 lambda 表达式的主体中(外部 {} 内),我们创建了另一个引用 isGreaterThanPivot,它包含对另一个 lambda 表达式的引用。这一次,该引用是一个 Predicate 而不是 Function。最后,我们返回该引用。

isGreaterThan 是一个 lambda 表达式的引用,该表达式在调用时返回另一个 lambda 表达式 — 换言之,这里隐藏着一种 lambda 表达式级联关系。

现在,我们可以使用新创建的外部 lamba 表达式来解决代码中的重复问题:

1
2
3
4
5
6
7
8
9
10
11
复制代码List<Integer> valuesOver25 = numbers1.stream()
  .filter(isGreaterThan.apply(25))
  .collect(toList());
 
List<Integer> valuesOver50 = numbers1.stream()
  .filter(isGreaterThan.apply(50))
  .collect(toList());
 
List<Integer> valuesOver75 = numbers1.stream()
  .filter(isGreaterThan.apply(75))
  .collect(toList());

在 isGreaterThan 上调用 apply 会返回一个 Predicate,后者然后作为参数传递给 filter 方法。

尽管整个过程非常简单(作为示例),但是能够抽象为一个函数对于谓词更加复杂的场景来说尤其有用。

保持简短的秘诀

我们已从代码中成功删除了重复的 lambda 表达式,但 isGreaterThan 的定义看起来仍然很杂乱。幸运的是,我们可以组合一些 Java 8 约定来减少杂乱,让代码更简短。

我们首先重构以下代码:

1
2
3
4
5
6
7
复制代码Function<Integer, Predicate<Integer>> isGreaterThan = (Integer pivot) -> {
  Predicate<Integer> isGreaterThanPivot = (Integer candidate) -> {
    return candidate > pivot;
  };
   
  return isGreaterThanPivot;
};

可以使用类型引用来从外部和内部 lambda 表达式的参数中删除类型细节:

1
2
3
4
5
6
7
复制代码Function<Integer, Predicate<Integer>> isGreaterThan = (pivot) -> {
  Predicate<Integer> isGreaterThanPivot = (candidate) -> {
    return candidate > pivot;
  };
   
  return isGreaterThanPivot;
};

目前,我们从代码中删除了两个单词,改进不大。

接下来,我们删除多余的 (),以及外部 lambda 表达式中不必要的临时引用:

1
2
3
4
5
复制代码Function<Integer, Predicate<Integer>> isGreaterThan = pivot -> {
  return candidate -> {
    return candidate > pivot;
  };
};

代码更加简短了,但是仍然看起来有些杂乱。

可以看到内部 lambda 表达式的主体只有一行,显然 {} 和 return 是多余的。让我们删除它们:

1
2
3
复制代码Function<Integer, Predicate<Integer>> isGreaterThan = pivot -> {
  return candidate -> candidate > pivot;
};

现在可以看到,外部 lambda 表达式的主体也只有一行,所以 {} 和 return 在这里也是多余的。在这里,我们应用最后一次重构:

1
2
复制代码Function<Integer, Predicate<Integer>> isGreaterThan = 
  pivot -> candidate -> candidate > pivot;

现在可以看到 — 这是我们的级联 lambda 表达式。

理解级联 lambda 表达式

我们通过一个适合每个阶段的重构过程,得到了最终的代码 - 级联 lambda 表达式。在本例中,外部 lambda 表达式接收 pivot 作为参数,内部 lambda 表达式接收 candidate 作为参数。内部 lambda 表达式的主体同时使用它收到的参数 (candidate) 和来自外部范围的参数。也就是说,内部 lambda 表达式的主体同时依靠它的参数和它的词法范围或定义范围。

级联 lambda 表达式对于编写它的人非常有意义。但是对于读者呢?

看到一个只有一个向右箭头 (->) 的 lambda 表达式时,您应该知道您看到的是一个匿名函数,它接受参数(可能是空的)并执行一个操作或返回一个结果值。

看到一个包含两个向右箭头 (->) 的 lambda 表达式时,您看到的也是一个匿名函数,但它接受参数(可能是空的)并返回另一个 lambda 表达式。返回的 lambda 表达式可以接受它自己的参数或者可能是空的。它可以执行一个操作或返回一个值。它甚至可以返回另一个 lambda 表达式,但这通常有点大材小用,最好避免。

大体上讲,当您看到两个向右箭头时,可以将第一个箭头右侧的所有内容视为一个黑盒:一个由外部 lambda 表达式返回的 lambda 表达式。

结束语

级联 lambda 表达式不是很常见,但您应该知道如何在代码中识别和理解它们。当一个 lambda 表达式返回另一个 lambda 表达式,而不是接受一个操作或返回一个值时,您将看到两个箭头。这种代码非常简短,但可能在最初遇到时非常难以理解。但是,一旦您学会识别这种函数式语法,理解和掌握它就会变得容易得多。

相关主题

  • 使用 lambda 表达式进行 Java 编程
  • Java 8 语言变更
  • Java 中的函数式编程:The Pragmatic Bookshelf,2014 年

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何高效地学习 Laravel 框架?

发表于 2017-11-29

学习策略

Laravel 是个功能齐全的全栈框架,学习她相当于你在学习成为全栈工程师。如果你之前没有学习过类似的全栈框架,你会发现你很快会被埋进大量的技术概念和专有名词里。这并不是你不够聪明,而是:

人类短时间内的记忆和信息处理能力都是有限的,当短时间内暴露在大量的信息面前时,你的注意力会被严重分散,带来的是挫折感和烦躁不安。

所以,我们需要一套更加聪明的学习策略。

我将框架知识分类为以下:

  • 底层实现知识 —— 如服务容器、服务提供器、Facades、
    Contracts、Repository 等
  • 框架使用知识 —— 如用户注册登录、邮件发送、数据模型的 CRUD、用户数据获取等

每一个分类下都有非常多的概念需要学习,但是很明显,学习框架的使用要比学习底层实现原理要简单有趣多了,并且因为学习的愉悦性高了,我们能记得更牢固。

当你有一定的框架使用经验以后,再去学习底层实现的概念,你能更好地理解这些技术概念的来龙去脉,最终达到会事半功倍的学习效果。并且这时候学习底层实现,也会让你对框架的理解更加深入,你会发现你对框架使用技巧会变得更加灵活。

用比较简单的话来讲,就是在一开始学习的时候,先不管底层实现,利用框架提供的功能,先建造一些可用的项目,等熟悉掌握了这些框架功能的使用以后,再去学习底层实现概念。

即使是做了分类,并且有了先后顺序还不够。因为单单框架使用这部分的知识,涉及的概念也是非常多,很容易陷入信息过载的情况。所以我们需要有一个循序渐进的方案,先学习简单的,常用的概念,然后再慢慢学复杂的,并且在学习的过程中要注意重复学习,这样概念才能记得越牢固。

推荐学习路径

基于以上的思想,我创建了 《Laravel 实战课程》,计划中有三本(也有可能更多),分别是:

  • 第一本 —— 《Laravel 入门教程 - 从零到部署上线》
  • 第二本 —— 《Laravel 进阶课程 - 从零开始构建论坛系统》
  • 第三本 —— 《Laravel 高级课程 - 构架 API 服务器》

第一本书教授如何使用 Laravel 一步一步构建一个类似新浪微博的应用,书中很多技术话题会被一带而过,这是有意而为之的,我们希望让读者保持对编码线索的专注,不被篇幅悠长的名词解释分心。通过阅读本教程,你将学到如 HTML、CSS、JavaScript、PHP 和 Laravel 等 Web 开发相关的基础知识。不仅如此,本书还会对这些基础知识点进行延伸扩展,为你讲解一些在 Web 开发中更为专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流、Bootstrap 框架基本使用等。这些知识将为你未来的编程开发奠定下坚实的基础。

第二本以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。编码规范遵循 Laravel 项目开发规范 ,应用程序架构思路贴近 Laravel 框架的设计哲学。在论坛系统的构建中,我们将学到多角色用户权限系统、管理员后台、注册验证码、图片上传、图片裁剪,XSS 防御、自定义命令行、自定义中间件、任务调度、队列系统的使用、应用缓存、Redis、模型事件监控、表单验证、消息通知、邮件通知、模型修改器等知识。在本课程的学习中,你不仅能学到使用 Laravel 开发一个论坛项目,还能学到安全优先、高扩展性的大型项目架构经验。

第三本将以构建 API 服务器为目标,来展开。目前本课程正在紧张撰写中,敬请期待。

学完了以上三本书,你将拥有一定的项目开发经验,对框架的功能使用也会有一个比较全面的系统性理解。这时候,会是学习『底层实现』的好时机。

底层实现的知识学习,可以从文档开始,打开 Laravel 的文档中心 —— d.laravel-china.org ,找到最新版本的 Laravel 文档,然后仔细阅读 2、3 遍。因为有了上面的项目经验,此时的文档阅读啃起来会轻松多了。

阅读文档后,可以尝试看下 Laravel 底层的源码,看看这些框架的功能都是怎么实现的。

学习过程中可以适当做笔记,例如:

  • zhangbao 同学的 Laravel 文档阅读笔记
  • leoyang 同学的 Laravel 源码分析笔记

错误的学习方法

一上来就开始啃文档 d.laravel-china.org 。

如果你是新手,有太多的新概念你需要学习,你会发现学习起来非常艰难,甚至怀疑文档是不是写的太烂了(社区里经常出现这种抱怨)。

事实上,不是文档写的太烂,而是你把文档用错了。文档的『目的』是快速查阅,一份优秀文档的标准是语言简练,释义,这个 Laravel 的文档做的很棒。但是,文档并不适合做入门学习使用,上面我们已经讲过,原因是信息量太大。

寻找网络上零散的课程进行学习。

如果你想学习单个概念,这些零散的小课程会很方便。但是,如果是想以阅读大量课程来达到系统性学习的目的,你将会很失望。很多时候你会感觉 —— 你好像学了很多,学了很久,以为自己学会了,但是心里还是没底气。

你需要的是通过项目,完整的项目,将所有的知识串起来去记忆。你的作品,清清楚楚摆在面前,看着你一步步构建出来的一套系统,自信心也会有所增加。

一开始就学习高级话题,如 服务容器、服务提供器、Facades、
Contracts、Repository 等

很多时候你会发现这些话题晦涩难懂,很难学习。并且即使你毅力比较好,死记硬背,很快也会忘记,学习效率非常低下。然后最重要的,学会这些概念,并无法使你掌握构建一个完整项目的能力。

EOF

讨论请前往: 如何高效地学习 Laravel 框架?

–

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JSONLINT:PYTHON的JSON数据验证库

发表于 2017-11-29

随着前后端分离和 REST APIs 的火热,开发者不断寻找着一种灵活的、优雅的方式验证 json 数据。有直接手动获取数据验证的,也有使用 json scheme 验证的。前者容易使得函数变得冗长,还可能存在不少重复的验证;后者验证又不灵活。

本文介绍的 jsonlint 启发自 python 的表单验证工具 wtforms,wtforms 通过继承 Form 类也能进行 json 数据验证,但是 wtforms 对于 json 的数组(Array)类型处理有着很诡异的行为,需要通过 a-1 、 a-2 这样来传递数组数据,常常不能有效的处理数组数据。 jsonlint 大部分代码来着 wtforms,可以视为 wtforms 的一个分支。但 jsonlint 删去了 wtforms 的表单渲染部分,更改了传入的数据格式,最重要的是使用正确的逻辑验证数组(Array)和对象(Object)类型。下面是一些例子:

基本的字符串类型json验证

对于基本的字符串类型,我们只需要创建一个 Json 的子类,填写对应的 Field 即可。使用方式和 wtforms 类型:

1
2
3
4
5
6
7
8
9
10
angelscript复制代码from jsonlint import Json
from jsonlint.fields import StringField
from jsonlint.validators import DataRequired

class MyLint(Json):
name = StringField(validators=[DataRequired()])

mylint = MyLint({'name': 'demo'})
print mylint.validate() # True
print mylint.name.data # demo

更灵活的验证 json 数据

jsonlint 继承了 wtforms 的优点,可以进行一些更灵活的自定义json数据验证,只要将 field 类的实例名写成函数 validate_fieldname ,即可自定义验证改字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码from jsonlint import Json
from jsonlint.fields import IntegerField
from jsonlint.validators import ValidationError

class AgeLint(Json):
age = IntegerField()

def validate_age(form, field):
if field.data < 13:
raise ValidationError("We're sorry, you must be 13 or older to register")

agelint = AgeLint({'age': 12})
print agelint.validate() # False
print agelint.age.errors # ["We're sorry, you must be 13 or older to register"]

对数组类型进行验证

jsonlint 诞生可以说主要就是为了解决如何验证数组类型的问题,在jsonlint这很容易实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码from jsonlint import Json
from jsonlint.fields import StringField, ListField
from jsonlint.validators import DataRequired, ValidationError

class ListLint(Json):
cars = ListField(StringField(validators=[DataRequired()]))

def validate_cars(form, field):
if 'BMW' in field.data:
raise ValidationError("We're sorry, you cannot drive BMW")

listlint = ListLint({'cars': ['Benz', 'BMW', 'Audi']})
print listlint.validate() # False
print listlint.cars.errors # ["We're sorry, you cannot drive BMW"]

ListField 类作为一个 Field 容器,容纳其它类型 Field 的数组,将对应类型的数组直接传入,即可有效的验证;ListField 同样也可以进行自定义验证。

对对象类型进行验证

对象类型在一些 REST APIs 的 web 应用中也经常存在,对此 jsonlint 也作了支持。只要将 Json 子类传入 ObjectField 中即可进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
angelscript复制代码from jsonlint import Json
from jsonlint.fields import ObjectField, IntegerField, BooleanField

class T(Json):
status = BooleanField()
code = IntegerField()

class DataLint(Json):
data = ObjectField(T)

datalint = DataLint({'data': {'status': True, 'code': 200}})
print datalint.validate() # False
print datalint.data.code.data # 200

写在最后

jsonlint 诞生初衷就是因为本人想用类似 wtforms 的方式来验证json,这样不但有着良好的验证方式,还可以分割业务,避免接口主函数变得十分冗长。例如,可以定义类:

1
2
3
4
5
6
7
8
9
10
11
ruby复制代码class RegisterLint(UserLint):
def validata_nickname(self, field):
...

def validate_account(self, field):
...

def create_user(self):
...

user = RegisterLint()

这样既可以使用 RegisterLint 的实例 user 验证数据,同时又能直接执行 user.create_user() 进行数据库操作,将数据库逻辑更好的封装。这样可以说是在 MVC 设计模式的基础上独立出了一层。

想要尝试使用 jsonlint 可以直接使用 pip 安装:

1
mipsasm复制代码pip install jsonlint

最后,jsonlint 开源在 Github : github.com/tangwz/json…

jsonlint 现阶段仅由我一人维护,虽然单元测试覆盖率尽可能的全覆盖,但也不代表没有bug,希望您提出您宝贵的意见,或一起维护、迭代jsonlint:

github.com/tangwz/json…

如果使用 Flask 进行 web 开发,也可以使用封装好的结合了 Flask 和 jsonlint 的库: Flask-Lint

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python函数的作用域规则和闭包

发表于 2017-11-29

作用域规则

命名空间是从名称到对象的映射,Python中主要是通过字典实现的,主要有以下几个命名空间:

  • 内置命名空间,包含一些内置函数和内置异常的名称,在Python解释器启动时创建,一直保存到解释器退出。内置命名实际上存在于一个叫__builtins__的模块中,可以通过globals()[‘__builtins__‘].__dict__查看其中的内置函数和内置异常。
  • 全局命名空间,在读入函数所在的模块时创建,通常情况下,模块命名空间也会一直保存到解释器退出。可以通过内置函数globals()查看。
  • 局部命名空间,在函数调用时创建,其中包含函数参数的名称和函数体内赋值的变量名称。在函数返回或者引发了一个函数内部没有处理的异常时删除,每个递归调用有它们自己的局部命名空间。可以通过内置函数locals()查看。

python解析变量名的时候, 首先搜索局部命名空间。如果没有找到匹配的名称,它就会搜索全局命名空间。如果解释器在全局命名空间中也找不到匹配值,最终会检查内置命名空间。如果仍然找不到,就会引发NameError异常。

不同命名空间内的名称绝对没有任何关系,比如:

?

1 2 3 4 5 6 7 8 a = 42 def foo(): a = 13 print "globals: %s" % globals() print "locals: %s" % locals() return a foo() print "a: %d" % a

结果:

?

1 2 3 globals``: {``'a'``: 42 , '__builtins__'``: <module '__builtin__' (built -``in``)>, '__file__'``: 'C:\\Users\\h\\Desktop\\test4.py' , '__package__'``: None``, '__name__' : '__main__'``, 'foo'``: <function foo at 0x0000000002C17AC8``>, '__doc__'``: None } locals``: {``'a'``: 13 } a: 42

可见在函数中对变量a赋值会在局部作用域中创建一个新的局部变量a,外部具有相同命名的那个全局变量a不会改变。

在Python中赋值操作总是在最里层的作用域,赋值不会复制数据,只是将命名绑定到对象。删除也是如此,比如在函数中运行del a,也只是从局部命名空间中删除局部变量a,全局变量a不会发生任何改变。

如果使用局部变量时还没有给它赋值,就会引发UnboundLocalError异常:

?

1 2 3 4 5 a = 42 def foo(): a +``= 1 return a foo()

上述函数中定义了一个局部变量a,赋值语句a += 1会尝试在a赋值之前读取它的值,但全局变量a是不会给局部变量a赋值的。

要想在局部命名空间中对全局变量进行操作,可以使用global语句,global语句明确地将变量声明为属于全局命名空间:

?

1 2 3 4 5 6 7 8 9 a = 42 def foo(): global a a = 13 print "globals: %s" % globals``() print "locals: %s" % locals``() return a foo() print "a: %d" % a

输出:

?

1 2 3 globals``: {``'a'``: 13 , '__builtins__'``: <module '__builtin__' (built -``in``)>, '__file__'``: 'C:\\Users\\h\\Desktop\\test4.py' , '__package__'``: None``, '__name__' : '__main__'``, 'foo'``: <function foo at 0x0000000002B87AC8``>, '__doc__'``: None } locals``: {} a: 13

可见全局变量a发生了改变。

Python支持嵌套函数(闭包),但python 2只支持在最里层的作用域和全局命名空间中给变量重新赋值,内部函数是不可以对外部函数中的局部变量重新赋值的,比如:

?

1 2 3 4 5 6 7 8 9 10 def countdown(start): n = start def display(): print n def decrement(): n -``= 1 while n > 0``: display() decrement() countdown(``10``)

运行会报UnboundLocalError异常,python 2中,解决这个问题的方法是把变量放到列表或字典中:

?

1 2 3 4 5 6 7 8 9 10 11 def countdown(start): alist = [] alist.append(start) def display(): print alist[``0 ] def decrement(): alist[``0``] -``= 1 while alist[``0``] > 0``: display() decrement() countdown(``10``)

在python 3中可以使用nonlocal语句解决这个问题,nonlocal语句会搜索当前调用栈中的下一层函数的定义。:

?

1 2 3 4 5 6 7 8 9 10 11 def countdown(start): n = start def display(): print n def decrement(): nonlocal n n -``= 1 while n > 0``: display() decrement() countdown(``10``)

闭包

闭包(closure)是函数式编程的重要的语法结构,Python也支持这一特性,举例一个嵌套函数:

?

1 2 3 4 5 6 def foo(): x = 12 def bar(): print x return bar foo()()

输出:12

可以看到内嵌函数可以访问外部函数定义的作用域中的变量,事实上内嵌函数解析名称时首先检查局部作用域,然后从最内层调用函数的作用域开始,搜索所有调用函数的作用域,它们包含非局部但也非全局的命名。

组成函数的语句和语句的执行环境打包在一起,得到的对象就称为闭包。在嵌套函数中,闭包将捕捉内部函数执行所需要的整个环境。

python函数的code对象,或者说字节码中有两个和闭包有关的对象:

  • co_cellvars: 是一个元组,包含嵌套的函数所引用的局部变量的名字
  • co_freevars: 是一个元组,保存使用了的外层作用域中的变量名

再看下上面的嵌套函数:

?

1 2 3 4 5 6 7 8 9 10 11 >>> def foo(): x = 12 def bar(): return x return bar >>> foo.func_code.co_cellvars (``'x'``,) >>> bar = foo() >>> bar.func_code.co_freevars (``'x'``,)

可以看出外层函数的code对象的co_cellvars保存了内部嵌套函数需要引用的变量的名字,而内层嵌套函数的code对象的co_freevars保存了需要引用外部函数作用域中的变量名字。

在函数编译过程中内部函数会有一个闭包的特殊属性__closure__(func_closure)。__closure__属性是一个由cell对象组成的元组,包含了由多个作用域引用的变量:

?

1 2 >>> bar.func_closure (<cell at 0x0000000003512C78``: int object at 0x0000000000645D80``>,)

若要查看闭包中变量的内容:

?

1 2 >>> bar.func_closure[``0``].cell_contents 12

如果内部函数中不包含对外部函数变量的引用时,__closure__属性是不存在的:

?

1 2 3 4 5 6 7 8 9 >>> def foo(): x = 12 def bar(): pass return bar >>> bar = foo() >>> print bar.func_closure None

当把函数当作对象传递给另外一个函数做参数时,再结合闭包和嵌套函数,然后返回一个函数当做返回结果,就是python装饰器的应用啦。

延迟绑定

需要注意的一点是,python函数的作用域是由代码决定的,也就是静态的,但它们的使用是动态的,是在执行时确定的。

?

1 2 3 4 5 >>> def foo(n): return n * i >>> fs = [foo for i in range``(``4``)] >>> print fs[``0``](``1 )

当你期待结果是0的时候,结果却是3。

这是因为只有在函数foo被执行的时候才会搜索变量i的值, 由于循环已结束, i指向最终值3, 所以都会得到相同的结果。

在闭包中也存在相同的问题:

?

1 2 3 4 5 6 7 def foo(): fs = [] for i in range (``4``): fs.append(``lambda x: x *``i) return fs for f in foo(): print f(``1``)

返回:

?

1 2 3 4 3 3 3 3

解决方法,一个是为函数参数设置默认值:

?

1 2 3 >>> fs = [``lambda x, i``= i: x * i for i in range``(``4``)] >>> for f in fs: print f(``1``)

另外就是使用闭包了:

?

1 2 3 4 5 6 >>> def foo(i): return lambda x: x * i >>> fs = [foo(i) for i in range``(``4``)] >>> for f in fs: print f(``1``)

或者:

?

1 2 >>> for f in map``( lambda i: lambda x: i``*``x, range``(``4``)): print f(``1``)

使用闭包就很类似于偏函数了,也可以使用偏函数:

?

1 2 3 >>> fs = [functools.partial(``lambda x, i: x * i, i) for i in range (``4``)] >>> for f in fs: print f(``1``)

这样自由变量i都会优先绑定到闭包函数上。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【译】Go TCP Socket的实现

发表于 2017-11-29

原文: TCP Socket Implementation On Golang by Gian Giovani.

译者注: 作者并没有从源代码级别去分析Go socket的实现,而是利用strace工具来反推Go Socket的行为。这一方法可以扩展我们分析代码的手段。
源代码级别的分析可以看其实现: net poll,以及一些分析文章:The Go netpoller,
The Go netpoller and timeout

Go语言是我写web程序的首选, 它隐藏了很多细节,但仍然不失灵活性。最新我用strace工具分析了一下一个http程序,纯属手贱但还是发现了一些有趣的事情。

下面是strace的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
91.24 0.397615 336 1185 29 futex
4.13 0.018009 3 7115 clock_gettime
2.92 0.012735 19 654 epoll_wait
1.31 0.005701 6 911 write
0.20 0.000878 3 335 epoll_ctl
0.12 0.000525 1 915 457 read
0.02 0.000106 2 59 select
0.01 0.000059 0 170 close
0.01 0.000053 0 791 setsockopt
0.01 0.000035 0 158 getpeername
0.01 0.000034 0 170 socket
0.01 0.000029 0 160 getsockname
0.01 0.000026 0 159 getsockopt
0.00 0.000000 0 7 sched_yield
0.00 0.000000 0 166 166 connect
0.00 0.000000 0 3 1 accept4
------ ----------- ----------- --------- --------- ----------------
100.00 0.435805 12958 653 total

在这个剖析结果中有很多有趣的东东,但本文中要特别指出的是read的错误数和futex调用的错误数。

一开始我没有深思futex的调用, 大部分情况它无非是一个唤醒调用(wake call)。既然这个程序会处理每秒几百个请求,它应该包含很多go routine。另一方面,它使用了channel,这也会导致很多block情况,所以有很多futex调用也很正常。 不过后来我发现这个数也包含来自其它的逻辑,后面再表。

Why you no read

有谁喜欢错误(error)?短短一分钟就有几百次的错误,太糟糕了, 这是我看到这个剖析结果后最初的印象。那么 read call又是什么东东?

1
2
3
复制代码read(36, "GET /xxx/v3?q=xx%20ch&d"..., 4096) = 520
...
read(36, 0xc422aa4291, 1) = -1 EAGAIN (Resource temporarily unavailable)

每次read调用同一个文件描述符,总是(可能)伴随着一个 EAGAIN error。我记得这个错误,当文件描述符还没有准备(ready)某个操作的时候就会返回这个错,上面的例子中操作是read。问题是为什么Go会这样做呢?

我猜想这可能是epoll_wait的一个bug, 它为每一个文件描述符提供了错误的ready事件?每一个文件描述符? 看起来read事件是错误事件的两倍,为什么是两倍?

老实说,我的epoll知识很了了,程序只是一个简单的处理事件的socket handler(类似)。没有多线程,没有同步,非常简单。

通过Google我找到了一篇极棒的文章分析评论epoll,由Marek所写,。

这篇文章重要的摘要就是:在多线程中使用epoll, 不必要的唤醒(wake up)通常是不可避免的,因为我们想通知每个等待事件的worker。

这也正好解释了我们的futex 唤醒数。还是让我们看一个简化版本来好好理解怎么在基于事件的socket处理程序中使用epoll吧:

  1. Bind socket listener 到 file descriptor, 我们称之为 s_fd
  2. 使用epoll_create创建 epoll file descriptor , 我们称之为 e_fd
  3. 通过epol_ctl bind s_fd 到 e_fd, 处理特殊的事件(通常EPOLLIN|EPOLLOUT)
  4. 创建一个无限循环 (event loop), 它会在每次循环中调用epoll_wait得到已经ready连接
  5. 处理ready的连接, 在多worker实现中会通知每一个worker

Using strace I found that golang using edge triggered epoll
使用strace我发现 golang使用 edge triggered epoll:

1
复制代码epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2490298448, u64=140490870550608}}) = 0

这意味着下面的过程应该是go socket的实现:

1、Kernel: 收到一个新连接.
2、Kernel: 通知等待的线程 threads A 和 B. 由于level-triggered 通知的”惊群”(“thundering herd”)行为,kernel必须唤醒这两个线程.
3、Thread A: 完成 epoll_wait().
4、Thread B: 完成 epoll_wait().
5、Thread A:
执行 accept(), 成功.
6、Thread B: 执行 accept(), 失败, EAGAIN错误.

现在我有八成把握就是这个case,不过还是让我们用一个简单的程序来分析。

1
2
3
4
5
6
7
8
9
复制代码package main
import "net/http"
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/test", handler)
http.ListenAndServe(":8080", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
}

一个简单的请求后的strace结果:

1
2
3
4
5
6
7
8
复制代码epoll_wait(4, [{EPOLLIN|EPOLLOUT, {u32=2186919600, u64=140542106779312}}], 128, -1) = 1
futex(0x7c1bd8, FUTEX_WAKE, 1) = 1
futex(0x7c1b10, FUTEX_WAKE, 1) = 1
read(5, "GET / HTTP/1.1\r\nHost: localhost:"..., 4096) = 348
futex(0xc420060110, FUTEX_WAKE, 1) = 1
write(5, "HTTP/1.1 200 OK\r\nDate: Sat, 03 J"..., 116) = 116
futex(0xc420060110, FUTEX_WAKE, 1) = 1
read(5, 0xc4200f6000, 4096) = -1 EAGAIN (Resource temporarily unavailable)

看到epoll_wait有两个futex调用,我认为是worker执行以及一次 error read。

如果GOMAXPROCS设置为1,在单worker情况下:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码epoll_wait(4,[{EPOLLIN, {u32=1969377136, u64=140245536493424}}], 128, -1) = 1
futex(0x7c1bd8, FUTEX_WAKE, 1) = 1
accept4(3, {sa_family=AF_INET6, sin6_port=htons(54400), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28], SOCK_CLOEXEC|SOCK_NONBLOCK) = 6
epoll_ctl(4, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1969376752, u64=140245536493040}}) = 0
getsockname(6, {sa_family=AF_INET6, sin6_port=htons(8080), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 0
setsockopt(6, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(6, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
setsockopt(6, SOL_TCP, TCP_KEEPINTVL, [180], 4) = 0
setsockopt(6, SOL_TCP, TCP_KEEPIDLE, [180], 4) = 0
accept4(3, 0xc42004db78, 0xc42004db6c, SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)
read(6, "GET /test?kjhkjhkjh HTTP/1.1\r\nHo"..., 4096) = 92
write(6, "HTTP/1.1 200 OK\r\nDate: Sat, 03 J"..., 139) = 139
read(6, "", 4096)

当使用1个worker,epoll_wait之后只有一次futex唤醒,并没有error read。然而我发现并不总是这样, 有时候我依然可以得到read error和两次futex 唤醒。

And then what to do?

在Marek的文章中他谈到Linux 4.5之后可以使用EPOLLEXCLUSIVE。我的Linux版本是4.8,为什么问题还是出现?或许Go并没有使用这个标志,我希望将来的版本可以使用这个标志。

从中我学到了很多知识,希望你也是。

[0] banu.com/blog/2/how-…
[1] idea.popcount.org/2017-02-20-…
[2]
gist.github.com/wejick/2cef…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

golang map源码详解

发表于 2017-11-29

本文将主要分析一下golang中map的实现原理,并对使用中的常见问题进行讨论。进行分析的golang版本为1.9。

golang中的map是用hashmap作为底层实现的,在github的源码中相关的代码有两处:runtime/hashmap.go定义了map的基本结构和方法,runtime/hashmap_fast.go提供了一些快速操作map的函数。

map基本数据结构

map的底层结构是hmap(即hashmap的缩写),核心元素是一个由若干个桶(bucket,结构为bmap)组成的数组,每个bucket可以存放若干元素(通常是8个),key通过哈希算法被归入不同的bucket中。当超过8个元素需要存入某个bucket时,hmap会使用extra中的overflow来拓展该bucket。下面是hmap的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码type hmap struct {
count     int // # 元素个数
flags     uint8
B         uint8  // 说明包含2^B个bucket
noverflow uint16 // 溢出的bucket的个数
hash0     uint32 // hash种子
 
buckets    unsafe.Pointer // buckets的数组指针
oldbuckets unsafe.Pointer // 结构扩容的时候用于复制的buckets数组
nevacuate  uintptr        // 搬迁进度(已经搬迁的buckets数量)
 
extra *mapextra
}

在extra中不仅有overflow,还有oldoverflow(用于扩容)和nextoverflow(prealloc的地址)。

bucket(bmap)的结构如下

1
2
3
4
5
6
7
8
9
10
11
复制代码type bmap struct {
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt values.
// NOTE: packing all the keys together and then all the values together makes the
// code a bit more complicated than alternating key/value/key/value/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}
  • tophash用于记录8个key哈希值的高8位,这样在寻找对应key的时候可以更快,不必每次都对key做全等判断。
  • 注意后面几行注释,hmap并非只有一个tophash,而是后面紧跟8组kv对和一个overflow的指针,这样才能使overflow成为一个链表的结构。但是这两个结构体并不是显示定义的,而是直接通过指针运算进行访问的。
  • kv的存储形式为”key0key1key2key3…key7val1val2val3…val7″,这样做的好处是:在key和value的长度不同的时候,节省padding空间。如上面的例子,在map[int64]int8中,4个相邻的int8可以存储在同一个内存单元中。如果使用kv交错存储的话,每个int8都会被padding占用单独的内存单元(为了提高寻址速度)。

hmap的结构差不多如图

map的访问

以mapaccess1为例,部分无关代码被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
复制代码func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
// do some race detect things
// do some memory sanitizer thins
 
if h == nil || h.count == 0 {
return unsafe.Pointer(&zeroVal[0])
}
if h.flags&hashWriting != 0 {  // 检测是否并发写,map不是gorountine安全的
throw("concurrent map read and map write")
}
alg := t.key.alg  // 哈希算法 alg -> algorithm
hash := alg.hash(key, uintptr(h.hash0))
m := bucketMask(h.B)
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
        // 如果老的bucket没有被移动完,那么去老的bucket中寻找 (增长部分下一节介绍)
if c := h.oldbuckets; c != nil {
if !h.sameSizeGrow() {
// There used to be half as many buckets; mask down one more power of two.
m >>= 1
}
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
if !evacuated(oldb) {
b = oldb
}
}
        // 寻找过程:不断比对tophash和key
top := tophash(hash)
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey {
k = *((*unsafe.Pointer)(k))
}
if alg.equal(key, k) {
v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
if t.indirectvalue {
v = *((*unsafe.Pointer)(v))
}
return v
}
}
}
        return unsafe.Pointer(&zeroVal[0])
}

map的增长

随着元素的增加,在一个bucket链中寻找特定的key会变得效率低下,所以在插入的元素个数/bucket个数达到某个阈值(当前设置为6.5,实验得来的值)时,map会进行扩容,代码中详见 hashGrow函数。首先创建bucket数组,长度为原长度的两倍

1
复制代码newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger)

,然后替换原有的bucket,原有的bucket被移动到oldbucket指针下。

扩容完成后,每个hash对应两个bucket(一个新的一个旧的)。oldbucket不会立即被转移到新的bucket下,而是当访问到该bucket时,会调用growWork方法进行迁移,growWork方法会将oldbucket下的元素rehash到新的bucket中。随着访问的进行,所有oldbucket会被逐渐移动到bucket中。

但是这里有个问题:如果需要进行扩容的时候,上一次扩容后的迁移还没结束,怎么办?在代码中我们可以看到很多”again”标记,会不断进行迁移,知道迁移完成后才会进行下一次扩容。

使用中常见问题

Q:删除掉map中的元素是否会释放内存?

A:不会,删除操作仅仅将对应的tophash[i]设置为empty,并非释放内存。若要释放内存只能等待指针无引用后被系统gc

Q:如何并发地使用map?

A:map不是goroutine安全的,所以在有多个gorountine对map进行写操作是会panic。多gorountine读写map是应加锁(RWMutex),或使用sync.Map(1.9新增,在下篇文章中会介绍这个东西,总之是不太推荐使用)。

Q:map的iterator是否安全?

A:map的delete并非真的delete,所以对迭代器是没有影响的,是安全的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

PHP 自动加载 深度总结

发表于 2017-11-29

PHP 版本:php-5.6
核心方法: spl_autoload_register

这里说的自动加载为官方提供的一系列 SPL 接口实现。

类加载方式

  • 手动加载
  • __autoload
  • spl_autoload_register

手动加载

包含:include include_once requice requice_one

include

以下文档也适用于 require。
被包含文件先按参数给出的路径寻找,如果没有给出目录(只有文件名)时则按照 include_path 指定的目录寻找。如果在 include_path 下没找到该文件则 include 最后才在调用脚本文件所在的目录和当前工作目录下寻找。如果最后仍未找到文件则 include 结构会发出一条警告;这一点和 require 不同,后者会发出一个致命错误。

如果定义了路径——不管是绝对路径(在 Windows 下以盘符或者 开头,在 Unix/Linux 下以 / 开头)还是当前目录的相对路径(以 . 或者 .. 开头)——include_path 都会被完全忽略。例如一个文件以 ../ 开头,则解析器会在当前目录的父目录下寻找该文件。

代码示例:
FILE: run.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码<?php
ini_set('display_errors', '1');

// 直接包含
$ret = include 'class.php';
echo sprintf("include ret-value:%d,ret-type:%s\n", $ret, gettype($ret));
Foo::getFoo();

// 包含不存在的文件
$ret1 = include './class1.php';
echo sprintf("include ret-value:%d,ret-type:%s\n", $ret1, gettype($ret1));

// 重复包含
$ret2 = include './class.php';
echo sprintf("include ret-value:%d,ret-type:%s\n", $ret, gettype($ret));
Foo::getFoo();

FILE: class.php

1
2
3
4
5
6
7
8
9
复制代码<?php

class Foo
{
static public function getFoo()
{
echo "I am foo!\n";
}
}

结果:

结论:

  1. include 成功返回值:1,失败返回值:false
  2. include 失败会有 warning,不会中断进程

include_once

include_once 行为和 include 语句类似,唯一区别是如果该文件中已经被包含过,则不会再次包含。

将 run.php 修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码<?php
ini_set('display_errors', '1');

// 直接包含
$ret = include 'class.php';
echo sprintf("include ret-value:%d,ret-type:%s\n", $ret, gettype($ret));
Foo::getFoo();

// 重复包含
$ret2 = include_once './class.php';
echo sprintf("include ret-value:%d,ret-type:%s\n", $ret, gettype($ret));
Foo::getFoo();

结果:

结论:

  1. include_once 重复包含时,会直接返回 1,并忽略此次包含操作,继续执行

require

require 和 include 几乎完全一样,但 require 在出错时产生 E_COMPILE_ERROR 级别的错误。(脚本将会中止运行)

将 run.php 修改如下:

1
2
3
4
5
6
7
8
9
10
11
复制代码<?php
ini_set('display_errors', '1');

// 直接包含
$ret = require 'class.php';
echo sprintf("include ret-value:%d,ret-type:%s\n", $ret, gettype($ret));
Foo::getFoo();

// 包含不存在的文件
$ret1 = require './class1.php';
echo sprintf("include ret-value:%d,ret-type:%s\n", $ret1, gettype($ret1));

结果:

结论:

  1. require 包含成功,同 include 一样,返回值:1
  2. require 包含失败,直接抛出 Fatal error,进程中止

require_once

require_once 语句和 require 语句完全相同,唯一区别是 PHP 会检查该文件是否已经被包含过,如果是则不会再次包含。

将 run.php 修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码ini_set('display_errors', '1');

// 直接包含
$ret = require_once 'class.php';
echo sprintf("include ret-value:%d,ret-type:%s\n", $ret, gettype($ret));
Foo::getFoo();

// 重复包含
$ret2 = require_once './class.php';
echo sprintf("include ret-value:%d,ret-type:%s\n", $ret, gettype($ret));
Foo::getFoo();

// 包含不存在的文件
$ret1 = require_once './class1.php';
echo sprintf("include ret-value:%d,ret-type:%s\n", $ret1, gettype($ret1));

结果:

结论:

  1. 成功,返回值:1
  2. 重复包含,返回 1,并忽略此次包含

总结

include include_once requice requice_one 成功时,都会返回 1,差别在于 包含失败、重复包含 的处理

__autoload

尝试加载未定义的类。此函数将会在 PHP 7.2.0 中弃用。

PHP 代码解释

使用示例:
FILE:foo.php

1
2
3
4
5
6
7
8
9
复制代码<?php

class Foo
{
static public function getFoo()
{
echo "I am foo!\n";
}
}

FILE:run.php

1
2
3
4
5
6
7
8
9
10
复制代码<?php
ini_set('display_errors', '1');

function __autoload($classname)
{
$filename = "./". lcfirst($classname) .".php";
include_once($filename);
}

Foo::getFoo();

结果:

1
2
复制代码➜  load git:(master) ✗ php run.php
I am foo!

结论:
遇到未包含的类,会触发 __autoload 进行加载,如果所有加载规则中没有此类,则 Fatal error。

Zend 代码解释

下面,我们来看一下 Zend 引擎是如何触发 __autoload 调用的。
利用 vld 来查看刚才执行过程中产生的 opcode,结果如下:

我们看到,PHP 运行到第 10 行时,所生成的 opcode 为:INIT_STATIC_METHOD_CALL,两个操作数都为常量(CONST)。
根据 opcode 的处理函数对应规则,我们利用 命名法 可以确定,
处理函数为:ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST_HANDLER
源码位置为:vim Zend/zend_vm_execute.h +3819
源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
复制代码static int ZEND_FASTCALL  ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *function_name;
zend_class_entry *ce;
call_slot *call = EX(call_slots) + opline->result.num;

SAVE_OPLINE();

if (IS_CONST == IS_CONST) {
/* no function found. try a static method in class */
if (CACHED_PTR(opline->op1.literal->cache_slot)) {
ce = CACHED_PTR(opline->op1.literal->cache_slot);
} else {
ce = zend_fetch_class_by_name(Z_STRVAL_P(opline->op1.zv), Z_STRLEN_P(opline->op1.zv), opline->op1.literal + 1, opline->extended_value TSRMLS_CC);
if (UNEXPECTED(EG(exception) != NULL)) {
HANDLE_EXCEPTION();
}
if (UNEXPECTED(ce == NULL)) {
zend_error_noreturn(E_ERROR, "Class '%s' not found", Z_STRVAL_P(opline->op1.zv));
}
CACHE_PTR(opline->op1.literal->cache_slot, ce);
}
call->called_scope = ce;
} else {
ce = EX_T(opline->op1.var).class_entry;

if (opline->extended_value == ZEND_FETCH_CLASS_PARENT || opline->extended_value == ZEND_FETCH_CLASS_SELF) {
call->called_scope = EG(called_scope);
} else {
call->called_scope = ce;
}
}

if (IS_CONST == IS_CONST &&
IS_CONST == IS_CONST &&
CACHED_PTR(opline->op2.literal->cache_slot)) {
call->fbc = CACHED_PTR(opline->op2.literal->cache_slot);
} else if (IS_CONST != IS_CONST &&
IS_CONST == IS_CONST &&
(call->fbc = CACHED_POLYMORPHIC_PTR(opline->op2.literal->cache_slot, ce))) {
/* do nothing */
} else if (IS_CONST != IS_UNUSED) {
char *function_name_strval = NULL;
int function_name_strlen = 0;


if (IS_CONST == IS_CONST) {
function_name_strval = Z_STRVAL_P(opline->op2.zv);
function_name_strlen = Z_STRLEN_P(opline->op2.zv);
} else {
function_name = opline->op2.zv;

if (UNEXPECTED(Z_TYPE_P(function_name) != IS_STRING)) {
if (UNEXPECTED(EG(exception) != NULL)) {
HANDLE_EXCEPTION();
}
zend_error_noreturn(E_ERROR, "Function name must be a string");
} else {
function_name_strval = Z_STRVAL_P(function_name);
function_name_strlen = Z_STRLEN_P(function_name);
}
}

if (function_name_strval) {
if (ce->get_static_method) {
call->fbc = ce->get_static_method(ce, function_name_strval, function_name_strlen TSRMLS_CC);
} else {
call->fbc = zend_std_get_static_method(ce, function_name_strval, function_name_strlen, ((IS_CONST == IS_CONST) ? (opline->op2.literal + 1) : NULL) TSRMLS_CC);
}
if (UNEXPECTED(call->fbc == NULL)) {
zend_error_noreturn(E_ERROR, "Call to undefined method %s::%s()", ce->name, function_name_strval);
}
if (IS_CONST == IS_CONST &&
EXPECTED(call->fbc->type <= ZEND_USER_FUNCTION) &&
EXPECTED((call->fbc->common.fn_flags & (ZEND_ACC_CALL_VIA_HANDLER|ZEND_ACC_NEVER_CACHE)) == 0)) {
if (IS_CONST == IS_CONST) {
CACHE_PTR(opline->op2.literal->cache_slot, call->fbc);
} else {
CACHE_POLYMORPHIC_PTR(opline->op2.literal->cache_slot, ce, call->fbc);
}
}
}
if (IS_CONST != IS_CONST) {

}
} else {
if (UNEXPECTED(ce->constructor == NULL)) {
zend_error_noreturn(E_ERROR, "Cannot call constructor");
}
if (EG(This) && Z_OBJCE_P(EG(This)) != ce->constructor->common.scope && (ce->constructor->common.fn_flags & ZEND_ACC_PRIVATE)) {
zend_error_noreturn(E_ERROR, "Cannot call private %s::__construct()", ce->name);
}
call->fbc = ce->constructor;
}

if (call->fbc->common.fn_flags & ZEND_ACC_STATIC) {
call->object = NULL;
} else {
if (EG(This) &&
Z_OBJ_HT_P(EG(This))->get_class_entry &&
!instanceof_function(Z_OBJCE_P(EG(This)), ce TSRMLS_CC)) {
/* We are calling method of the other (incompatible) class,
but passing $this. This is done for compatibility with php-4. */
if (call->fbc->common.fn_flags & ZEND_ACC_ALLOW_STATIC) {
zend_error(E_DEPRECATED, "Non-static method %s::%s() should not be called statically, assuming $this from incompatible context", call->fbc->common.scope->name, call->fbc->common.function_name);
} else {
/* An internal function assumes $this is present and won't check that. So PHP would crash by allowing the call. */
zend_error_noreturn(E_ERROR, "Non-static method %s::%s() cannot be called statically, assuming $this from incompatible context", call->fbc->common.scope->name, call->fbc->common.function_name);
}
}
if ((call->object = EG(This))) {
Z_ADDREF_P(call->object);
call->called_scope = Z_OBJCE_P(call->object);
}
}

call->num_additional_args = 0;
call->is_ctor_call = 0;
EX(call) = call;

CHECK_EXCEPTION();
ZEND_VM_NEXT_OPCODE();
}

通过以上源码,我们发现关键方法为 zend_fetch_class_by_name,跟进此方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码zend_class_entry *zend_fetch_class_by_name(const char *class_name, uint class_name_len, const zend_literal *key, int fetch_type TSRMLS_DC) 
{
zend_class_entry **pce;
int use_autoload = (fetch_type & ZEND_FETCH_CLASS_NO_AUTOLOAD) == 0;

if (zend_lookup_class_ex(class_name, class_name_len, key, use_autoload, &pce TSRMLS_CC) == FAILURE) {
if (use_autoload) {
if ((fetch_type & ZEND_FETCH_CLASS_SILENT) == 0 && !EG(exception)) {
if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_INTERFACE) {
zend_error(E_ERROR, "Interface '%s' not found", class_name);
} else if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TRAIT) {
zend_error(E_ERROR, "Trait '%s' not found", class_name);
} else {
zend_error(E_ERROR, "Class '%s' not found", class_name);
}
}
}
return NULL;
}
return *pce;
}

我们发现是通过 zend_lookup_class_ex 来获取类,继续跟进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
复制代码ZEND_API int zend_lookup_class_ex(const char *name, int name_length, const zend_literal *key, int use_autoload, zend_class_entry ***ce TSRMLS_DC) 
{
...

/* 注意:在 类的符号表 中没有找到示例中调用的类 foo */
if (zend_hash_quick_find(EG(class_table), lc_name, lc_length, hash, (void **) ce) == SUCCESS) {
if (!key) {
free_alloca(lc_free, use_heap);
}
return SUCCESS;
}

...

/*
* ZVAL_STRINGL 为 zval (即 PHP 类型的实现基础 zvalue_value)赋值宏,
* 此处实现了 把 ZEND_AUTOLOAD_FUNC_NAME 值 赋给 autoload_function
* #define ZEND_AUTOLOAD_FUNC_NAME "__autoload"
*/
ZVAL_STRINGL(&autoload_function, ZEND_AUTOLOAD_FUNC_NAME, sizeof(ZEND_AUTOLOAD_FUNC_NAME) - 1, 0);

...

fcall_info.size = sizeof(fcall_info);
fcall_info.function_table = EG(function_table);
fcall_info.function_name = &autoload_function;
fcall_info.symbol_table = NULL;
fcall_info.retval_ptr_ptr = &retval_ptr;
fcall_info.param_count = 1;
fcall_info.params = args;
fcall_info.object_ptr = NULL;
fcall_info.no_separation = 1;

fcall_cache.initialized = EG(autoload_func) ? 1 : 0;
fcall_cache.function_handler = EG(autoload_func); /* 留意此处 */
fcall_cache.calling_scope = NULL;
fcall_cache.called_scope = NULL;
fcall_cache.object_ptr = NULL;

...

retval = zend_call_function(&fcall_info, &fcall_cache TSRMLS_CC); /* 调用自动加载函数 */

...

EG(autoload_func) = fcall_cache.function_handler;

zval_ptr_dtor(&class_name_ptr);

zend_hash_quick_del(EG(in_autoload), lc_name, lc_length, hash);

...
}

我们发现是通过 zend_call_function 出发了自动加载函数,而且看到了加载方法的名字 __autoload (宏:ZEND_AUTOLOAD_FUNC_NAME)

zend_call_function 中会做一下检测并调用等,而且我们看到 zend_lookup_class_ex 的返回结果即为 zend_call_function 的返回结果。

接下来我们逐步退出函数调用栈:
假设 zend_call_function 调用失败,返回 FALSE,
则 zend_lookup_class_ex 返回 FALSE;
则 zend_fetch_class_by_name 返回 NULL;
则 ZEND_INIT_STATIC_METHOD_CALL_SPEC_CONST_CONST_HANDLER 抛出异常 Class ** not found,如下图所示:

结论

至此,我们通过 PHP 代码 Zend 源码 了解了 __autoload 的调用过程。
我们知道 __autoload 现在已并不推荐使用,
它的缺点也很明显,不支持多个自动加载函数。

spl_autoload_register

将函数注册到SPL __autoload函数队列中。如果该队列中的函数尚未激活,则激活它们。如果在你的程序中已经实现了__autoload()函数,它必须显式注册到__autoload()队列中。

PHP 代码解释

使用示例:
FILE:foo.php
(同上 __autoload)

FILE:foo2.class.php

1
2
3
4
5
6
7
8
9
复制代码<?php

class Foo2
{
static public function getFoo2()
{
echo "I am foo2!\n";
}
}

FILE:run.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码<?php
ini_set('display_errors', '1');

$my_autoload1 = function ($classname)
{
echo "entry my_autoload1 \n";
$filename = "./". lcfirst($classname) .".php";
include_once($filename);
};

$my_autoload2 = function ($classname)
{
echo "entry my_autoload2 \n";
$filename = "./". lcfirst($classname) .".class.php";
include_once($filename);
};

spl_autoload_register($my_autoload1);
spl_autoload_register($my_autoload2);

Foo::getFoo();
Foo2::getFoo2();

结果如下:

我们看到,调用 getFoo2 时,会先调用第一个注册的 autoload 方法,如果没找到对应的类,会产生 warning 并继续调用后边注册的 autoload 方法。
说明了 PHP 内核中为通过 spl_autoload_register 注册的 autoload 方法维护了一个队列,当前文件为包含调用类,便会触发此队列,并依次调用,直到队列结束
或者 找到对应类。

Zend 源码解释

首先,我们看一下 PHP 文件生成的 opcode

我们发现,其方法调用所生成的 opcode 跟 __autoload 一样,
但是我们之前调用了 spl\_autoload\_register, 那么,看一下spl_autoload_register的源码: **FILE:ext/spl/php_spl.c`*\

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
复制代码PHP_FUNCTION(spl_autoload_register)
{
char *func_name, *error = NULL;
int func_name_len;
char *lc_name = NULL;
zval *zcallable = NULL;
zend_bool do_throw = 1;
zend_bool prepend = 0;
zend_function *spl_func_ptr;
autoload_func_info alfi;
zval *obj_ptr;
zend_fcall_info_cache fcc;

if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS() TSRMLS_CC, "|zbb", &zcallable, &do_throw, &prepend) == FAILURE) {
return;
}

if (ZEND_NUM_ARGS()) {
if (Z_TYPE_P(zcallable) == IS_STRING) {
if (Z_STRLEN_P(zcallable) == sizeof("spl_autoload_call") - 1) {
if (!zend_binary_strcasecmp(Z_STRVAL_P(zcallable), sizeof("spl_autoload_call"), "spl_autoload_call", sizeof("spl_autoload_call"))) {
if (do_throw) {
zend_throw_exception_ex(spl_ce_LogicException, 0 TSRMLS_CC, "Function spl_autoload_call() cannot be registered");
}
RETURN_FALSE;
}
}
}

if (!zend_is_callable_ex(zcallable, NULL, IS_CALLABLE_STRICT, &func_name, &func_name_len, &fcc, &error TSRMLS_CC)) {
alfi.ce = fcc.calling_scope;
alfi.func_ptr = fcc.function_handler;
obj_ptr = fcc.object_ptr;
if (Z_TYPE_P(zcallable) == IS_ARRAY) {
if (!obj_ptr && alfi.func_ptr && !(alfi.func_ptr->common.fn_flags & ZEND_ACC_STATIC)) {
if (do_throw) {
zend_throw_exception_ex(spl_ce_LogicException, 0 TSRMLS_CC, "Passed array specifies a non static method but no object (%s)", error);
}
if (error) {
efree(error);
}
efree(func_name);
RETURN_FALSE;
}
else if (do_throw) {
zend_throw_exception_ex(spl_ce_LogicException, 0 TSRMLS_CC, "Passed array does not specify %s %smethod (%s)", alfi.func_ptr ? "a callable" : "an existing", !obj_ptr ? "static " : "", error);
}
if (error) {
efree(error);
}
efree(func_name);
RETURN_FALSE;
} else if (Z_TYPE_P(zcallable) == IS_STRING) {
if (do_throw) {
zend_throw_exception_ex(spl_ce_LogicException, 0 TSRMLS_CC, "Function '%s' not %s (%s)", func_name, alfi.func_ptr ? "callable" : "found", error);
}
if (error) {
efree(error);
}
efree(func_name);
RETURN_FALSE;
} else {
if (do_throw) {
zend_throw_exception_ex(spl_ce_LogicException, 0 TSRMLS_CC, "Illegal value passed (%s)", error);
}
if (error) {
efree(error);
}
efree(func_name);
RETURN_FALSE;
}
}
alfi.closure = NULL;
alfi.ce = fcc.calling_scope;
alfi.func_ptr = fcc.function_handler;
obj_ptr = fcc.object_ptr;
if (error) {
efree(error);
}

lc_name = safe_emalloc(func_name_len, 1, sizeof(long) + 1);
zend_str_tolower_copy(lc_name, func_name, func_name_len);
efree(func_name);

if (Z_TYPE_P(zcallable) == IS_OBJECT) {
alfi.closure = zcallable;
Z_ADDREF_P(zcallable);

lc_name = erealloc(lc_name, func_name_len + 2 + sizeof(zend_object_handle));
memcpy(lc_name + func_name_len, &Z_OBJ_HANDLE_P(zcallable),
sizeof(zend_object_handle));
func_name_len += sizeof(zend_object_handle);
lc_name[func_name_len] = '\0';
}

if (SPL_G(autoload_functions) && zend_hash_exists(SPL_G(autoload_functions), (char*)lc_name, func_name_len+1)) {
if (alfi.closure) {
Z_DELREF_P(zcallable);
}
goto skip;
}

if (obj_ptr && !(alfi.func_ptr->common.fn_flags & ZEND_ACC_STATIC)) {
/* add object id to the hash to ensure uniqueness, for more reference look at bug #40091 */
lc_name = erealloc(lc_name, func_name_len + 2 + sizeof(zend_object_handle));
memcpy(lc_name + func_name_len, &Z_OBJ_HANDLE_P(obj_ptr), sizeof(zend_object_handle));
func_name_len += sizeof(zend_object_handle);
lc_name[func_name_len] = '\0';
alfi.obj = obj_ptr;
Z_ADDREF_P(alfi.obj);
} else {
alfi.obj = NULL;
}

if (!SPL_G(autoload_functions)) {
ALLOC_HASHTABLE(SPL_G(autoload_functions));
zend_hash_init(SPL_G(autoload_functions), 1, NULL, (dtor_func_t) autoload_func_info_dtor, 0);
}

zend_hash_find(EG(function_table), "spl_autoload", sizeof("spl_autoload"), (void **) &spl_func_ptr);

if (EG(autoload_func) == spl_func_ptr) { /* registered already, so we insert that first */
autoload_func_info spl_alfi;

spl_alfi.func_ptr = spl_func_ptr;
spl_alfi.obj = NULL;
spl_alfi.ce = NULL;
spl_alfi.closure = NULL;
zend_hash_add(SPL_G(autoload_functions), "spl_autoload", sizeof("spl_autoload"), &spl_alfi, sizeof(autoload_func_info), NULL);
if (prepend && SPL_G(autoload_functions)->nNumOfElements > 1) {
/* Move the newly created element to the head of the hashtable */
HT_MOVE_TAIL_TO_HEAD(SPL_G(autoload_functions));
}
}

if (zend_hash_add(SPL_G(autoload_functions), lc_name, func_name_len+1, &alfi.func_ptr, sizeof(autoload_func_info), NULL) == FAILURE) {
if (obj_ptr && !(alfi.func_ptr->common.fn_flags & ZEND_ACC_STATIC)) {
Z_DELREF_P(alfi.obj);
}
if (alfi.closure) {
Z_DELREF_P(alfi.closure);
}
}
if (prepend && SPL_G(autoload_functions)->nNumOfElements > 1) {
/* Move the newly created element to the head of the hashtable */
HT_MOVE_TAIL_TO_HEAD(SPL_G(autoload_functions));
}
skip:
efree(lc_name);
}

if (SPL_G(autoload_functions)) {
zend_hash_find(EG(function_table), "spl_autoload_call", sizeof("spl_autoload_call"), (void **) &EG(autoload_func)); /* 注意此处 */
} else {
zend_hash_find(EG(function_table), "spl_autoload", sizeof("spl_autoload"), (void **) &EG(autoload_func));
}
RETURN_TRUE;
} /* }}} */

通过分析源码,我们发现 spl_autoload_register 会把注册的自动加载函数添加到 autoload_functions 中,最后将 autoload_functions 赋值给 EG(autoload_func) (上方源码倒数第一个 if 判断逻辑中)。
而有印象的同学会发现,EG(autoload_func) 在分析 __autoload 调用源码时出现过(可以划到之前的分析查看),它是执行环境全局结构体中的成员,出现调用大概源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码ZEND_API int zend_lookup_class_ex(const char *name, int name_length, const zend_literal *key, int use_autoload, zend_class_entry ***ce TSRMLS_DC)
{
...

fcall_info.size = sizeof(fcall_info);
fcall_info.function_table = EG(function_table);
fcall_info.function_name = &autoload_function;
fcall_info.symbol_table = NULL;
fcall_info.retval_ptr_ptr = &retval_ptr;
fcall_info.param_count = 1;
fcall_info.params = args;
fcall_info.object_ptr = NULL;
fcall_info.no_separation = 1;

fcall_cache.initialized = EG(autoload_func) ? 1 : 0;
fcall_cache.function_handler = EG(autoload_func); /* 注意这里 */
fcall_cache.calling_scope = NULL;
fcall_cache.called_scope = NULL;
fcall_cache.object_ptr = NULL;

zend_exception_save(TSRMLS_C);
retval = zend_call_function(&fcall_info, &fcall_cache TSRMLS_CC);
zend_exception_restore(TSRMLS_C);

...

return retval;
}

分析到这里,我们已经知道了,spl_autoload_register 注册的函数是如何在 PHP 代码调用时被触发的。
感兴趣的同学可以继续查看一下 zend_call_function 的源码,了解具体的调用方式。

结论

通过 spl_autoload_register 注册自动加载函数,会在 Zend 引擎中维护一个 autoload 队列,即可添加多个 autoload 函数,并在 PHP 调用当前文件未知的类时,触发 autoload_func 的调用。

同时,细心的同学也会从 spl_autoload_register 源码中发现,当注册时传入的方法不可调用时,spl_autoload 如果有实现,也会被注册到 autoload 队列中。

写在最后

我们介绍了 include* require*,
而且我们从 PHP 应用 和 zend 源码角度,分别分析了 __autoload spl_autoload_register 的实现和调用过程。
我们可以直到,加载函数推荐使用 spl_autoload_register 来实现。
我们的分析更多的是意在让自己对这些细节加深认识,并进一步深入了解
Zend 源码。

更多使用细节,请参考:

  • __autoload
  • spl_autoload_register
  • SPL 函数
  • 深入理解 PHP 内核

以上为个人分析,如有不适的地方,请多多指教!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…925926927…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%