TensorFlow训练

Optimizer

1
2
3
4
5
6
7
8
9
10
11
class tf.train.GradientDescentOptimizer:

class tf.train.AdagradOptimizer:

class tf.train.MomentumOptimizer:

class tf.train.AdamOptimizer:

class tf.train.FtrlOptimizer:

class tf.train.RMSPropOptmizer:

TensorFlow执行

Session

Session:TF中OPs对象执行、Tensor对象计算的封装环境

  • Session管理图、OPs

    • 所有图必须Session中执行
    • 将图中OPs、其执行方法分发到CPU、GPU、TPU等设备上
    • 为当前变量值分配内存
  • Session只执行Data/Tensor Flow中OPs,忽略不相关节点

    • 即通往fetches的flow中的OPs构成子图才会被计算
  • 各Session中各值独立维护,不会相互影响

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
class Session:
def __init__(self,
target=str,
graph=None/tf.Graph,
config=None/tf.ConfigProto)

# 获取Session中载入图
self.graph

# 关闭会话,释放资源
def close(self):
pass

# 支持上下文语法
def __enter__(self):
pass
def __exit__(self):
pass

# 执行TensorFlow中节点,获取`fetches`中节点值
# 返回值若为Tensor
# python中:以`np.ndarray`返回
# C++/C中:以`tensorflow:Tensor`返回
# 可直接调用`.eval()`获得单个OP值
def run(self,
fetches=tf.OPs/[tf.OPs],
feed_dict=None/dict,
options=None,
run_metadata=None)

tf.InteractiveSession

tf.InteractiveSession:开启交互式会话

1
2
3
4
5
6
7
8
9
10
11
 # 开启交互式Session
sess = tf.InteractiveSession()
a = tf.constant(5.0)
b = tf.constant(6.0)
c = a * b
x = tf.Variable([1.0, 2.0])
# 无需显式在`sess.run`中执行
# 直接调用`OPs.eval/run()`方法得到结果
x.initializer.run()
print(c.eval())
sess.close()

Session执行

  • Fetch机制:sess.run()执行图时,传入需取回的结果, 取回操作输出内容

  • Feed机制:通过feed_dict参数,使用自定义tensor值替代图 中任意feeable OPs的输出

    • tf.placeholder()表示创建占位符,执行Graph时必须 使用tensor替代
    • feed_dict只是函数参数,只在调用它的方法内有效, 方法执行完毕则消失
  • 可以通过feed_dict feed所有feedable tensor,placeholder 只是指明必须给某些提供值

    1
    2
    3
    4
    a = tf.add(2, 5)
    b = tf.multiply(a, 3)
    with tf.Session() as sess:
    sess.run(b, feed_dict={a: 15}) # 45

config参数选项

  • log_device_placement:打印每个操作所用设备
  • allow_soft_placement:允许不在GPU上执行操作自动迁移到 CPU上执行
  • gpu_options
    • allow_growth:按需分配显存
    • per_process_gpu_memory_fraction:指定每个GPU进程 使用显存比例(无法对单个GPU分别设置)
  • 具体配置参见tf.ConfigProto

Graph

Graph:表示TF中计算任务,

  • operation/node:Graph中节点,包括:operatorvariableconstant

    • 获取一个、多个Tensor执行计算、产生、返回tensor
    • 不能无法对其值直接进行访问、比较、操作
    • 图中节点可以命名,TF会自动给未命名节点命名
  • tensor:Graph中边,n维数组

    • TF中所有对象都是Operators
    • tensor是OPs执行结果,在图中传递/流动
  • 图计算模型优势

    • 优化能力强、节省计算资源
      • 缓冲自动重用
      • 常量折叠,只计算取得目标值过程中必须计算的值
      • 方便并行化
      • 自动权衡计算、存储效率
    • 易于部署
      • 可被割成子图(即极大连通分量),便于自动区分
      • 子图可以分发到不同的设备上分布式执行,即模型并行
      • 许多通用ML模型是通过有向图教学、可视化的
  • 图计算模型劣势

    • 难以debug
      • 图定义之后执行才会报错
      • 无法通过pdb、打印状态debug
    • 语法繁复

构建图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Graph:
def __init__(self):
pass

# 将当前图作为默认图
# 支持上下文语法
def as_default(self):
pass

# 强制OPs依赖关系(图中未反映),即优先执行指定OPs
# 支持上下文语法
def as_default(self):
def control_dependencies(self,
?ops=[tf.OPs])
pass

# 以ProtoBuf格式展示Graph
def as_graph_def(self):
pass

# 判断图中节点是否可以被feed
def is_feedable(self,
?op=tf.OPs):
pass
  • 可以通过tf.Graph创建新图,但最好在是在一张图中使用多个 不相连的子图,而不是多张图

    • 充分性:Session执行图时忽略不必要的OPs
    • 必要性
      • 多张图需要多个会话,每张图执行时默认尝试使用所有 可能资源
      • 不能通过python/numpy在图间传递数据(分布式系统)
  • 初始化即包含默认图,OP构造器默认为其增加节点

    • 通过tf.get_default_graph()获取

图相关方法

  • 获取TF初始化的默认图

    1
    2
    def tf.get_default_graph():
    pass

命名空间

1
2
3
4
5
6
7
8
 # 均支持、利用上下文语法,将OPs定义于其下
def tf.name_scope(name(str)):
pass
def tf.variable_scope(
name(str),
reuse=tf.AUTO_REUSE
):
pass
  • tf.name_scope:将变量分组
    • 只是将变量打包,不影响变量的重用、可见性等
    • 方便管理、查看graph
  • tf.variable_scope
    • 对变量有控制能力
      • 可设置变量重用性
      • 变量可见性局限于该variable scope内,即不同 variable scope间可以有完全同名变量 (未被TF添加顺序后缀)
    • 会隐式创建name scope
    • 大部分情况是用于实现变量共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def fully_connected(x, output_dim, scope):
# 设置variable scope中变量自动重用
# 或者调用`scope.reuse_variables()`声明变量可重用
with tf.variable_scope(scope, reuse=tf.AUTO_REUSE) as scope:
# 在当前variable scope中获取、创建变量
w = tf.get_variable("weights", [x.shape[1]], output_dim,
initializer=tf.random_normal_initializer())
b = tf.get_variable("biases", [output_dim],
initializer=tf.constant_initizer(0.0))
return tf.matmul(x, w) + b

def two_hidden_layers(x):
h1 = fully_connected(x, 50, "h1")
h2 =-fully_connected(h1, 10, "h2")

with tf.variable_scope("two_layers") as scope:
logits1 = two_hidden_layers(x1)
logits2 = two_hidden_layers(x2)

Lazy Loading

Lazy Loading:推迟声明/初始化OP对象至载入图时 (不是指TF的延迟计算,是个人代码结构问题,虽然是TF延迟图计算 模型的结果)

  • 延迟加载容易导致向图中添加大量重复节点,影响图的载入、 传递

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    x = tf.Variable(10, name='x')
    y = tf.Variable(20, name='y')

    with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    writer = tf.summary.FileWriter('graphs/lazy_loading', sess.graph)
    for _ in range(10):
    # 延迟加载节点,每次执行都会添加`tf.add`OP
    sess.run(tf.add(x, y))
    print(tf.get_default_graph().as_graph_def())
    writer.close()
  • 解决方案

    • 总是把(图的搭建)OPs的定义、执行分开
    • python利用@property装饰器,使用单例模式函数 封装变量控制,保证仅首次调用函数时才创建OP

Eager Execution

1
2
3
4
import tensorflow as tf
import tensorflow.contrib.eager as tfe
# 启用TF eager execution
tfe.enable_eager_execution()
  • 优势

    • 支持python debug工具
    • 提供实时报错
    • 支持python数据结构
    • 支持pythonic的控制流

      1
      2
      3
      i = tf.constant(0)
      whlile i < 1000:
      i = tf.add(i, 1)
  • eager execution开启后

    • tensors行为类似np.ndarray
    • 大部分API和未开启同样工作,倾向于使用
      • tfe.Variable
      • tf.contrib.summary
      • tfe.Iterator
      • tfe.py_func
      • 面向对象的layers
      • 需要自行管理变量存储
  • eager execution和graph大部分兼容

    • checkpoint兼容
    • 代码可以同时用于python过程、构建图
    • 可使用@tfe.function将计算编译为图

示例

  • placeholder、sessions

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 普通TF
    x = tf.placholder(tf.float32, shape=[1, 1])
    m = tf.matmul(x, x)
    with tf.Session() as sess:
    m_out = sess.run(m, feed_dict={x: [[2.]]})

    # Eager Execution
    x = [[2.]]
    m = tf.matmul(x, x)
  • Lazy loading

    1
    2
    3
    4
    5
    x = tf.random_uniform([2, 2])
    for i in range(x.shape[0]):
    for j in range(x.shape[1]):
    # 不会添加多个节点
    print(x[i, j])

Device

设备标识

  • 设备标识:设备使用字符串进行标识

    • /cpu:0:所有CPU都以此作为名称
    • /gpu:0:第一个GPU,如果有
    • /gpu:1:第二个GPU
    1
    2
    3
    4
    5
    6
    # 为计算指定硬件资源
    with tf.device("/gpu:2"):
    a = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0], name="a")
    b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0], name="b")
    c = tf.multiply(a, b)
    # creates a graph
  • 环境变量:python中既可以修改os.environ,也可以直接设置 中设置环境变量

    • CUDA_VISIBLE_DEVICES:可被使用的GPU id

设备执行

设备执行:TF将图形定义转换成分布式执行的操作以充分利用计算 资源

  • TF默认自动检测硬件配置,尽可能使用找到首个GPU执行操作

  • TF会默认占用所有GPU及每个GPU的所有显存(可配置)

    • 但只有一个GPU参与计算,其他GPU默认不参与计算(显存 仍然被占用),需要明确将OPs指派给其执行
    • 指派GPU数量需通过设置环境变量实现
    • 控制显存占用需设置Session config参数
  • 注意:有些操作不能再GPU上完成,手动指派计算设备需要注意

tf.ConfigProto

  • Session参数配置

    1
    2
    3
    4
    5
     # `tf.ConfigProto`配置方法
    conf = tf.ConfigProto(log_device_placement=True)
    conf.gpu_options.allow_growth=True
    sess = tf.Session(config=conf)
    sess.close()

Spark Core

Spark特性

数据处理速度快

得益于Spark的内存处理技术、DAG执行引擎

内存计算

  • Spark尽量把数据(中间结果等)驻留在内存中,必要时才写入 磁盘,避免I/O操作,提高处理效率

  • 支持保存部分数据在内存中,剩下部分保存在磁盘中

  • 数据完全驻留于内存时,数据处理达到hadoop系统的 几十~上百倍,数据存在磁盘上时,处理速度能够达到hadoop的 10倍左右

DAG执行引擎

  • Spark执行任务前,根据任务之间依赖关系生成DAG图,优化数据 处理流程(减少任务数量)、减少I/O操作

  • 除了简单的map、reduce,Spark提供了超过80个数据处理的 Operator Primitives

  • 对于数据查询操作,Spark采用Lazy Evaluation方式执行, 帮助优化器对整个数据处力工作流进行优化

易用性/API支持

  • Spark使用Scala编写,经过编译后在JVM上运行

  • 支持各种编程语言,提供了简洁、一致的编程接口

    • Scala
    • Java
    • Python
    • Clojure
    • R

通用性

  • Spark是通用平台,支持以DAG(有向无环图)形式表达的复杂 数据处理流程,能够对数据进行复杂的处理操作,而不用将复杂 任务分解成一系列MapReduce作业

  • Spark生态圈DBAS(Berkely Data Analysis Stack)包含组件, 支持批处理、流数据处理、图数据处理、机器学习

兼容性

  • DataStorage

    • 一般使用HDFS、Amazon S3等分布式系统存储数据
    • 支持Hive、Hbase、Cassandra等数据源
    • 支持Flume、Kafka、Twitter等流式数据
  • Resource Management

    • 能以YARN、Mesos等分布式资源管理框架为资源管理器
    • 也可以使用自身资源的管理器以Standalone Mode独立运行
  • 使用支持

    • 可以使用shell程序,交互式的对数据进行查询
    • 支持流处理、批处理
  • 数据类型、计算表达能力

    • Spark可以管理各种类型的数据集:文本

Spark架构

核心组件

  • Spark StreamingSpark SQLSpark GraphXSpark MLLib为BDAS所包含的组件
  • Spark Streaming:提供对实时数据流高吞吐、高容错、可 扩展的流式处理系统

    • 采用Micro Batch数据处理方式,实现更细粒度资源分配, 实现动态负载均衡
    • 可以对多种数据源(Kafka、Flume、Twitter、ZeroMQ),进行 包括map、reduce、join等复杂操作
  • Spark SQL:结构化数据查询模块

    • 通过JDBC API暴露Spark数据集,让客户程序可以在其上 直接执行SQL查询
    • 可以连接传统的BI、可视化工具至数据集
    • 前身Shark即为Hive on Spark,后出于维护、优化、 性能考虑放弃
  • Spark GraphX:图数据的并行处理模块

    • 扩展RDD为Resilient Distributed Property Graph, 将属性赋予各个节点、边的有向图
    • 可利用此模块对图数据进行ExploratoryAnalysis、Iterative Graph Computation
  • Spark MLLib:可扩展的机器学习模块

    • 大数据平台使得在全量数据上进行学习成为可能
    • 实现包括以下算法
      • Classification
      • Regression
      • Clustering
      • Collaborative Filtering
      • Dimensionality Reduction

周围组件

  • BlinkDB:近似查询处理引擎

    • 可以在大规模数据集上,交互式执行SQL查询
    • 允许用户在查询精度、响应时间之间做出折中
      • 用户可以指定查询响应时间、查询结果精度要求之一
      • BlinkDB在Data Sample上执行查询,获得近似结果
      • 查询结果会给Error Bar标签,帮助决策
  • Tachyon:基于内存的分布式文件系统

    • 支持不同处理框架
      • 可在不同计算框架之间实现可靠的文件共享
      • 支持不同的上层查询处理框架,可以以极高的速度对集群 内存中的文件进行访问
    • 将workset文件缓存在内存中,避免对磁盘的存取,如果数据集 不断被扫描、处理,数据处理速度将极大提升

Spark实体

spark_entities

  • Spark Context:负责和CM的交互、协调应用

    • 所有的Spark应用作为独立进程运行,由各自的SC协调
    • SC向CM申请在集群中worker创建executor执行应用
  • Driver:执行应用主函数、创建Spark Context的节点

  • Worker:数据处理的节点

  • Cluster Manager:为每个driver中的应用分配资源

    • 以下3种资源管理器,在sceduling、security、monitoring 有所不同,根据需要选择不同的CM
      • Standalone
      • Mesos
      • YARN
    • CM对Spark是agnostic

Spark Context

spark.SparkContext

1
2
3
4
5
6
7
8
9
10
11
class SparkConf{
// 设置Spark应用名
def setAppName(appName: String)

// 设置集群地址:yarn master节点地址、"local[4]"本地standalone
def setMaster(master: String)
}
class SparkContext(?conf: SparkConf){
// 将driver中节点分块
def parallelize(?val: ?AnyRef, ?numPartition: Int)
}
  • SparkContext是Spark应用执行环境封装,任何应用都需要、 也只能拥有一个活动实例,有些shell可能默认已经实例化
1
2
3
4
5
import org.apache.spark.{SparkConf, SparkContext}

val conf = new SparkConf().setAppName("app name")
.setMaster("local[4]")
val sc = new SparkContext(conf)

Share Variable

共享变量:可以是一个值、也可以是一个数据集,Spark提供了两种 共享变量

Broadcast Variable

广播变量:缓存在各个节点上,而不随着计算任务调度的发送变量 拷贝,可以避免大量的数据移动

1
2
val broadcastVal = sc.breadcast(Array(1,2,3))
println(broadcastVal.value)

Accumulator

收集变量/累加器:用于实现计数器、计算总和

  • 集群上各个任务可以向变量执行增加操作,但是不能读取值, 只有Driver Program(客户程序)可以读取

  • 累加符合结合律,所以集群对收集变量的累加没有顺序要求, 能够高效应用于并行环境

  • Spark原生支持数值类型累加器,可以自定义类型累加器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建数值类型累加器
val accum = sc.accumulator(0, "accumulator")
sc.parallelize(Array(1,2,3,4)).foreach(x => accum += x)
println(accum.value)

// 自定义向量累加器工具
object VectorAccumulatorParam extends AccumulatorParam[Vector]{
def zero(initialValue: Vector): Vector = {
Vector.zeros(initialValue.size)
}
def addInPlace(v1: Vector, v2: Vector){
v1 += v2
}
}
// 创建向量累加器
val vecAccum = sc.accumulator(new Vector(1,2,3))(VectorAccumulator)

数据源

1
2
3
4
5
6
7
// 按行读取文本文件
def sc.textFile(?fileName: String, ?slices: Int): RDD[String]
// 读取包含多个小文件的目录
def sc.wholeTextFile(?directoryName: String): Map[String, RDD[String]]
// #todo
def sc.SequenceFiles(fileName: String)
def sc.hadoopRDD()
  • slices:切片数目,缺省为每个文件块创建切片,不能设置 小于文件块数目的切片值
  • Spark基于文件的方法,原生支持

    • 文件目录
    • 压缩文件:gz
    • 简单通配符

      |通配符|含义| |——-|——-| |[]:范围| |[^]|范围补集| |?|单个字符| |*|0、多个字符| |{}|整体或选择|

Spark Session

SparkSession:Spark2.0中新入口,封装有SparkConfSparkContextSQLContextHiveContext等接口

1
2
3
4
5
6
7
8
val warehouseLocation = "file:${system:user.dir}/spark-warehouse"
val spark = SparkSession
.builder()
.appName("App")
.config("spark.sql.warehouse.dir", warehouseLocation)
.enableHiveSupport()
.getOrCreate()
spark.conf.set("spark.executor.memory", "2g")

Resilient Distributed Dataset

RDD

RDD:容错的、immutable、分布式、确定可重复计算的数据集

  • RDD可分配在集群中的多个节点中以支持并行处理

    • 隶属于同一RDD数据,被划分为不同的Partition,以此为 单位分布到集群环境中各个节点上
  • RDD是无结构的数据表,可以存放任何数据类型

  • RDD immutable

    • 对RDD进行转换,不会修改原RDD,只是返回新RDD
    • 这也是基于Lineage容错机制的要求
  • 是Spark软件系统的核心概念

RDD容错机制

  • RDD采用基于Lineage的容错机制

    • 每个RDD记住确定性操作的lineage,即从其他RDD转换而来 的路径
    • 若所有RDD的转换操作是确定的,则最终转换数据一致, 无论机器发生的错误
    • 当某个RDD损坏时,Spark可以从上游RDD重新计算、创建 其数据
  • 容错语义

    • 输入源文件:Spark运行在HDFS、S3等容错文件系统上,从 任何容错数据而来的RDD都是容错的
    • receiver:可靠接收者告知可靠数据源,保证所有数据总是 会被恰好处理一次
    • 输出:输出操作可能会使得数据被重复写出,但文件会被 之后写入覆盖
  • 故障类型

    • worker节点故障:节点中内存数据丢失,其上接收者缓冲 数据丢失
    • driver节点故障:spark context丢失,所有执行算子丢失

RDD操作

1
import org.apache.spark.rdd.RDD

Transformation

转换:从已有数据集创建新数据集

  • 返回新RDD,原RDD保持不变

  • 转换操作Lazy

    • 仅记录转换操作作用的基础数据集
    • 仅当某个动作被Driver Program(客户操作)调用DAG 的动作操作时,动作操作的一系列proceeding转换操作才会 被启动
Transformation RDD DStream
map(func)
flatMap(func)
filter(func)
reduceByKey(func[, numTasks]) 包含(K, V)键值对,返回按键聚集键值对
groupByKey()
aggregateByKey()
pipe()
coalesce()
repartition(numPartitions)
union(other)
  • XXXByKey:RDD中应为(K, V)键值对

spark_rdd_transformation

  • 绿色、黑色:单、多RDD窄依赖转换
  • 紫色:KV shuffle转换
  • 黄色:重分区转换
  • 蓝色:特例转换

Action

动作:在数据集上进行计算后返回值到驱动程序

  • 施加于一个RDD,通过对RDD数据集的计算返回新的结果
    • 默认RDD会在每次执行动作时重新计算,但可以使用 cachepersist持久化RDD至内存、硬盘中,加速下次 查询速度
Action RDD DStream
count() 返回包含单元素RDD的DStream
collect() 将RDD数据聚集至本地
countByValue() 返回(T, long)键值对
countByKey()
first() 返回包含单元素RDDd的DStream
reduce(func) 返回包含单元素RDD的DStream
take(func)
foreach(func)
foreachPartition(func)
join(other[, numTasks]) 包含(K,V),与另一(K,W)连接
cogroup(other[, numTasks]) 包含(K,V)、输入(K,W),返回(K, Seq(V), Seq(W)
  • numTasks:默认使用Spark默认并发数目

DStream RDD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// RDD级`map`:`func`以RDD为参数,自定义转换操作
def transform(func)
// RDD级`foreach`
def foreachRDD(func)

// RDD级`reduce`
def updateStateByKey[S: ClassTag](
// `K`、`Seq[V]`:当前RDD中键`K`对应值`V`集合
// `Option[S]`:上个RDD结束后键`K`对应状态
updateFunc: (Iterator[(K, Seq[V], Option[S])]) => Iterator[(K, S)],
// 分区算法

partitioner: Partitioner,
// 是否在接下来Streaming执行过程中产生的RDD使用相同分区算法
remmemberPartition: Boolean,
// 键值对的初始状态
initRDD: RDD[(K,S)]
)
  • RDD分布在多个worker节点上,对不可序列化传递对象,需要在 每个worker节点独立创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    dstream.foreachRDD(rdd => {
    rdd.foreachPartition(partitionOfRecords => {
    // 为每个partition创建不可序列化网络连接
    // 为每个record创建成本过高
    val connection = createNewConnnection()
    // 进一步可以维护静态连接对象池
    // val connection = ConnectionPool.getConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    connection.close()
    })
    })

DStream Window Action

Window Action DStream
window(windowLength, slideInterval) 基于DStream产生的窗口化批数据产生DStream
countByWindow(windowLenght, slideInterval) 返回滑动窗口数
reduceByWindow(func, windowLength, slideInterval)
reduceByKeyAndWindow(func, windowLength, slidenInterval[, numTasks])
reduceByKeyAndWindow(func, invFunc, windowLength, slideInterval[, numTasks]) 须提供invFunc消除离开窗口RDD对reduce结果影响
countByValueAndWindow(windowLength, slideInterval[, numTasks])
  • windowLength:窗口长度
  • slideInterval:滑动间隔

  • 以上操作默认会持久化RDD至内存,无需手动调用persist等方法

spark_streaming_dstream_window_based_operation

Output

Output Operation RDD DStream
print 打印前10条元素 每个RDD打印前10条元素
saveAsObjectFile(prefix[, suffix]) 保存为序列化文件 命名为<prefix>-TIME_IN_M.<suffix>
saveAsTextFile(prefix[, suffix]) 保存为文本文件
saveAsHadoopFile(prefix[, suffix]) 保存为Hadoop文件

Persistence

Persistence RDD DStream
persist()
cache()

Directed Asycled Graph

Spark中DAG:可以看作由RDD、转换操作、动作操作构成,用于表达 复杂计算

  • 当需要执行某个操作时,将重新从上游RDD进行计算

  • 也可以对RDD进行缓存、持久化,以便再次存取,获得更高查询 速度

    • In-mem Storage as Deserialized Java Objects
    • In-mem Storage as Serialized Data
    • On-Disk Storage

DAG工作流示例

spark_dag_procedure

  • 从HDFS装载数据至两个RDD中
  • 对RDD(和中间生成的RDD)施加一系列转换操作
  • 最后动作操作施加于最后的RDD生成最终结果、存盘

宽依赖、窄依赖

spark_dag_wide_narrow_dependencies

  • 窄依赖:每个父RDD最多被一个子RDD分区使用

    • 即单个父RDD分区经过转换操作生成子RDD分区
    • 窄依赖可以在一台机器上处理,无需Data Shuffling, 在网络上进行数据传输
  • 宽依赖:多个子RDD分区,依赖同一个父RDD分区

    • 涉及宽依赖操作
      • groupByKey
      • reduceByKey
      • sortByKey
    • 宽依赖一般涉及Data Shuffling

DAG Scheduler

DAG SchedulerStage-Oriented的DAG执行调度器

spark_dag_job_stage

  • 使用Job、Stage概念进行作业调度

    • 作业:一个提交到DAG Scheduler的工作项目,表达成DAG, 以一个RDD结束
    • 阶段:一组并行任务,每个任务对应RDD一个分区,是作业 的一部分、数据处理基本单元,负责计算部分结果,
  • DAG Scheduler检查依赖类型

    • 把一系列窄依赖RDD组织成一个阶段
      • 所以说阶段中并行的每个任务对应RDD一个分区
    • 宽依赖需要跨越连续阶段
      • 因为宽依赖子RDD分区依赖多个父RDD分区,涉及 Data Shuffling,数据处理不能在单独节点并行执行
      • 或者说阶段就是根据宽依赖进行划分
  • DAG Scheduler对整个DAG进行分析

    • 为作业产生一系列阶段、其间依赖关系
    • 确定需要持久化的RDD、阶段的输出
    • 找到作业运行最小代价的执行调度方案、根据Cache Status 确定的运行每个task的优选位置,把信息提交给 Task Sheduler执行
  • 容错处理

    • DAG Scheduler负责对shuffle output file丢失情况进行 处理,把已经执行过的阶段重新提交,以便重建丢失的数据
    • stage内其他失败情况由Task Scheduler本身进行处理, 将尝试执行任务一定次数、直到取消整个阶段

DataFrame

  • DataFrame:类似关系型数据库中表,数据被组织到具名列中

    • 相较于RDD是对分布式、结构化数据集的高层次抽象,提供 特定领域的专用API进行处理
    • 静态类型安全:相较于SQL查询语句,在编译时即可发现 语法错误
  • Dataset:有明确类型数据、或无明确数据类型集合,相应API 也分为两类

    • 相较于DataFrame,也可组织半结构化数据,同样提供方便 易用的结构化API
    • 静态类型、运行时类型安全:相较于DataFrame,集合元素 有明确类型,在编译时即可发现分析错误
  • Spark2.0中二者API统一
  • DataFrame可视为无明确数据类型Dataset[Row]别名,每行是 无类型JVM对象

创建方式

  • .toDF

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    val sqlContext = new org.apache.spark.sql.SQLContext(sc)
    import sqlContext.implicits._

    // `.toDF` + `Seq`创建
    val df = Seq(
    (1, "F", java.sql.Date.valueOf("2019-08-02")),
    (2, "G", java.sql.Date.valueOf("2019-08-01"))
    ).toDF("id", "level", "date")
    // 不指定列名,则默认为`_1`、`_2`等


    // `.toDF` + `case Class`创建
    case class Person(name: String, age: Int)
    val people = sc.textFile("people.txt")
    .map(_.split(","))
    .map(p => Person(p(0),p(1).trim.toInt))
    .toDF()
  • .createDataFrame

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import org.apache.spark.sql.types._
    val schema = StrucType(List(
    StructField("id", IntegerType, nullable=False),
    StructField("level", StringType, nullable=False),
    StructField("date", DateType, nullable=False)
    ))
    val rdd = sc.parallelize(Seq(
    (1, "F", java.sql.Date.valueOf("2019-08-02")),
    (2, "G", java.sql.Date.valueOf("2019-08-01"))
    ))
    val df = sqlContext.createDataFrame(rdd, schema)
  • 读取文件创建

    1
    2
    3
    4
    5
    6
    7
    val df = sqlContext.read.parquet("hdfs:/peopole.parq")
    val df = spark.read.json("people.json")
    // 读取csv仅2.0版本`SparkSession`后可
    val df = spark.read.format("com.databricks.spark.csv")
    .option("header", "true")
    .option("mode", "DROPMALFORMED")
    .load("people.csv")

三种数据集对比

  • 空间、时间效率:DataFrame >= Dataset > RDD

    • DataFrame、Dataset基于SparkSQL引擎构建,使用Catalyst 生成优化后的逻辑、物理查询计划;无类型DataFrame相较 有类型Dataset运行更快
    • Spark作为编译器可以理解Dataset类型JVM对象,会使用 编码器将其映射为Tungsten内存内部表示
  • RDD适合场景

    • 对数据集进行最基本的转换、处理、控制
    • 希望以函数式编程而不是以特定领域表达处理数据
    • 数据为非结构化,如:流媒体、字符流
  • DataFrame、Dataset使用场景

    • 需要语义丰富、高级抽象、通用平台、特定领域API
    • 需要对半结构化数据进行高级处理,如:filter、SQL查询
    • 需要编译时/运行时类型安全、Catalyst优化、内存优化

模型评估

评估方向

模型误差

  • 给定损失函数时,基于损失函数的误差显然评估学习方法的标准
  • 回归预测模型:模型误差主要使用 MSE
  • 分类预测模型:模型误差主要是分类错误率 ERR=1-ACC
  • 模型训练时采用损失函数不一定是评估时使用的

Training Error

训练误差:模型在训练集上的误差,损失函数 $L(Y, F(X))$ 均值

  • $\hat f$:学习到的模型
  • $N$:训练样本容量
  • 训练时采用的损失函数和评估时一致时,训练误差等于经验风险
  • 训练误差对盘对给定问题是否容易学习是有意义的,但是本质上不重要
    • 模型训练本身就以最小化训练误差为标准,如:最小化 MSE、最大化预测准确率,一般偏低,不能作为模型预测误差的估计
    • 训练误差随模型复杂度增加单调下降(不考虑模型中随机因素)

Test Error

测试误差:模型在测试集上的误差,损失函数 $L(Y, f(X))$ 均值

  • $\hat f$:学习到的模型
  • $N$:测试样本容量
  • 测试误差反映了学习方法对未知测试数据集的预测能力,是模型 generalization ability 的度量,可以作为模型误差估计

  • 测试误差随模型复杂度增加呈U型

    • 偏差降低程度大于方差增加程度,测试误差降低
    • 偏差降低程度小于方差增加程度,测试误差增大
  • 训练误差小但测试误差大表明模型过拟合,使测试误差最小的模型为理想模型

模型复杂度

  • approximation error:近似误差,模型偏差,代表模型对训练集的拟合程度
  • estimation error:估计误差,模型方差,代表模型对训练集波动的稳健性
  • 模型复杂度越高

    • 低偏差:对训练集的拟合充分
    • 高方差:模型紧跟特定数据点,受其影响较大,预测结果不稳定
    • 远离真实关系,模型在来自同系统中其他尚未观测的数据集上预测误差大
  • 而训练集、测试集往往不完全相同

    • 复杂度较高的模型(过拟合)在测试集上往往由于其高方差效果不好,而建立模型最终目的是用于预测未知数据
    • 所以要兼顾偏差和方差,通过不同建模策略,找到恰当模型,其复杂度不太大且误差在可接受的水平
    • 使得模型更贴近真实关系,泛化能力较好
  • 简单模型:低方差高偏差
  • 复杂模型:低偏差高方差

  • 模型复杂度衡量参data_science/loss

Over-Fitting

过拟合:学习时选择的所包含的模型复杂度大(参数过多),导致模型对已知数据预测很好,对未知数据预测效果很差

  • 若在假设空间中存在“真模型”,则选择的模型应该逼近真模型(参数个数相近)
  • 一味追求对训练集的预测能力,复杂度往往会比“真模型”更高

解决方法

  • 减少预测变量数量
    • 最优子集回归:选择合适评价函数(带罚)选择最优模型
    • 验证集挑选模型:将训练集使用 抽样技术 分出部分作为 validation set,使用额外验证集挑选使得损失最小的模型
    • 正则化(罚、结构化风险最小策略)
      • 岭回归:平方损失,$L_2$ 范数
      • LASSO:绝对值损失,$L_1$ 范数
      • Elastic Net
  • 减弱变量特化程度:仅适合迭代求参数的方法
    • EarlyStop:提前终止模型训练
    • Dropout:每次训练部分神经元

模型信息来源

  • 训练数据包含信息
  • 模型形成过程中提供的先验信息
    • 模型:采用特定内在结构(如深度学习不同网络结构)、条件假设、其他约束条件(正则项)
    • 数据:调整、变换、扩展训练数据,让其展现更多、更有用的信息

评价指标

  • Classification 分类问题:输出变量$Y$为有限个离散变量

    • 混淆矩阵
      • F-Measure
      • TPRFPR
    • ROC
    • AUC
  • Tagging 标注问题:输入 $X^{(1)}, X^{(2)}, \cdots, X^{(n)}$、输出 $Y^{(1)}, Y^{(2)}, \cdots, Y^{(n)}$ 均为变量序列

    • 类似分类问题
  • Regression 回归问题

    • Squared Error
      • MSE
      • $R^2$、$R^2_{Adj}$
      • AIC
      • BIC
    • Absolute Error
      • MAE
      • MAPE
      • SMAPE
  • 经验损失、结构损失总是能用作评价模型,但是意义不明确

Scala 基本实体

Expression

表达式:可计算的语句

  • value:常量,引用常量不会再次计算
    • 不能被重新赋值
    • 类型可以被推断、也可以显式声明类型
    • 可以被声明为lazy,只有被真正使用时才被赋值
1
2
3
val constant = 1
lazy val lazy_constant = 1
var variable = 1
  • variable:变量,除可重新赋值外类似常量

Unified Types

  • Scala中所有值都有类型,包括数值、函数

类型层次结构

scala_unified_types_diagram

  • Any:顶级类型,所有类型超类

    • 定义一些通用方法
      • equals
      • hashCode
      • toString
  • AnyVal:值类型

    • 有9个预定义非空值类型
      • Double
      • Float
      • Long
      • Int
      • Short
      • Byte
      • Char
      • Boolean
      • Unit:唯一单例值()
    • 值类型可以按以下方向转换(非父子类之间) scala_value_types_casting_diagram
  • AnyRef:引用类型

    • 所有非值类型都被定义为引用类型
    • 用户自定义类型都是其子类型
    • 若Scala被应用于Java运行环境中,AnyRef相当于 java.lang.object
  • Nothing:底部类型,所有类型的子类型

    • 没有值是Nothing类型
    • 可以视为
      • 不对值进行定义的表达式的类型
      • 不能正常返回的方法
    • 用途
      • 给出非正常终止的信号:抛出异常、程序退出、死循环
  • NULL:所有引用类型的子类型

    • 唯一单例值null
    • 用途
      • 使得Scala满足和其他JVM语言的互操作性,几乎不应该 在Scala代码中使用

基本数据类型

数据类型 取值范围 说明
Byte $[-2^7, 2^7-1]$
Short $[-2^{15}, 2^{15}-1]$
Int $[-2^{31}, 2^{31}-1]$
Long $[-2^{63}, 2^{63}]$
Char $[0, 2^{16}-1]$
String 连续字符串 按值比较
Float 32bits浮点
Double 64bits浮点
Boolean truefalse
Unit () 不带任何意义的值类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
val x = 0x2F		// 16进制
val x = 41 // 10进制
val x = 041 // 8进制

val f = 3.14F
val f = 3.14f // `Float`浮点
val d = 3.14
val d = 0.314e1 // `Double`

val c = 'A'
val c = '\u0022'
val c = '\"' // `Char类型`

val str = "\"hello, world\"" // 字符串
val str = """"hello, world"""" // 忽略转义

var x = true
  • Unit()在概念上与Tuple0()相同(均唯一单例值) (但Tuple0类型不存在???)

Tuples

元组:不同类型值的聚集,可将不同类型的值放在同一个变量中保存

  • 元组包含一系列类:Tuple2Tuple3直到Tuple22

    • 创建包含n个元素的元组时,就是从中实例化相应类,用 组成元素的类型进行参数化
  • 特点

    • 比较:按值(内容)比较
    • 输出:输出括号包括的内容
  • 访问元素

    • <tuple>._<n>:访问元组中第n个元素(从1开始)
    • 元组支持模式匹配[解构]
  • 用途

    • 需要从函数中返回多个值
1
2
3
4
5
6
7
8
val t3 = ("hello", 1, 3.14)
val (str, i, d) = t3
// 必须括号包裹,否则三个变量被赋同值
println(t3._1)

val t2 = (1, 3)
val t22 = 1 -> 3
// `->`同样可以用于创建元组
  • 基于内容的比较衍生出模式匹配提取
  • 如果元素具有更多含义选择case class,否则选择元组

Symbol

符号类型:起标识作用,在模式匹配、内容判断中常用

  • 特点
    • 比较:按内容比较
    • 输出:原样输出
1
2
val s: Symbol = 'start
if (s == 'start) println("start......")

运算符号

  • 任何具有单个参数的方法都可以用作中缀运算符,而运算符 (狭义)就是普通方法

    • 可以使用点号调用
    • 可以像普通方法一样定义运算符
      1
      2
      3
      4
      case class Vec(val x: Double, val y: Double){
      def +(that: Vec) =
      new Vec(this.x + that.x, this.y + that.y)
      }
  • 表达式中运算符优先级:根据运算符第一个字符评估优先级

    • 其他未列出字符
    • */%
    • +-
    • :
    • +!
    • <>
    • &
    • ^
    • |
    • 所有字母:[a-zA-Z]

数值运算

  • 四则运算

    • +
    • -
    • *
    • /
    • %
  • 按位

    • &|^~
    • >>/<<:有符号移位
    • >>>/<<<:无符号移位
  • 比较运算

  • ><<=>=
  • ==/equals:基于内容比较
  • eq:基于引用比较

  • 逻辑运算

    • ||&&

字符串运算

  • Scala中String实际就是Java中java.lang.String类型
    • 可调用Java中String所有方法
    • 并定义有将String转换为StingOps类型的隐式转换函数 ,可用某某些额外方法
  • indexOf(<idx>)
  • toUppercasetoLowercase
  • reverse
  • drop(<idx>)
  • slice<<start>,<end>>
  • .r:字符串转换为正则表达式对象

类型推断

  • 编译器通常可以推断出

    • 表达式类型:工作原理类似推断方法返回值类型
    • 非递归方法的返回值类型
    • 多态方法、泛型类中泛型参数类型
  • 编译器不推断方法的形参类型

    • 但某些情况下,编译器可以推断作为参数传递的匿名函数 形参类型
  • 不应该依赖类型推断场合

    • 公开可访问API成员应该具有显式类型声明以增加可读性
    • 避免类型推断推断类型太具体

      1
      2
      var obj = null
      // 被推断为为`Null`类型仅有单例值,不能再分配其他值

类型别名

类型别名:具体类型别名

1
2
// 泛型参数必须指定具体类型
type JHashMap = java.util.HashMap[String, String]

Code Blocks

代码块:使用{}包围的表达式组合

  • 代码块中最后表达式的结果作为整个代码块的结果
    • Scala建议使用纯函数,函数不应该有副作用
    • 所以其中基本应该没有语句概念,所有代码块均可视为 表达式,用于赋值语句
  • {}:允许换行的(),Scala中{}基本同()

控制表达式

if语句

1
2
3
4
5
6
7
if(<eva_expr>){
// code-block if true
}else if(<eva_expr>){
// code-block
}else{
// code-block if false
}
  • 返回值:对应函数块返回值

while语句

1
2
3
while(<eva_expr>){
// code-block if true
}
  • Scala中while作用被弱化、不推荐使用
  • 返回值:始终为Unit类型()

for语句

1
2
3
4
5
6
7
for{
<item1> <- <iter1>
<item2> <- <iter2>
if <filter_exp>
if <filter_exp>
}{
}
  • 以上在同语句中多个迭代表达式等价于嵌套for
  • 返回值
    • 默认返回Unit类型()
    • 配合yield返回值迭代器(元素会被消耗)
  • 注意:迭代器中元素会被消耗,大部分情况不应该直接在嵌套 for语句中使用

match模式匹配

  • 模式匹配的候选模式

    • 常量
    • 构造函数:解构对象
      • 需伴生对象实现有unapply方法,如:case class
    • 序列
      • 需要类伴生对象实现有unapplySeq方法,如: Seq[+A]类、及其子类
    • 元组
    • 类型:适合需要对不同类型对象需要调用不同方法
      • 一般使用类型首字母作为case标识符name
      • 对密封类,无需匹配其他任意情况的case
      • 不匹配可以隐式转换的类型
    • 变量绑定
  • 候选模式可以增加pattern guards以更灵活的控制程序

  • 模式匹配可以视为解构已有值,将解构结果map至给定名称

    • 可以用于普通赋值语句中用于解构模式
    • 显然也适合于for语句中模式匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<target> match {
// 常量模式 + 守卫语句
case x if x % 2 == 0 =>
// 构造函数模式
case Dog(a, b) =>
// 序列模式
case Array(_, second) =>
// 元组模式
case (second, _*) =>
// 类型模式
case str: String =>
// 变量绑定
case all@Dog(name, _) =>
}

// 普通赋值语句中模式匹配
val all@Array(head, _*) = Array(1,3,3)
  • 可在普通类伴生对象中实现unapplyunapplySeq实现模式 匹配,此单例对象称为extractor objects

unapply

unapply方法:接受实例对象、返回创建其所用的参数

  • 构造模式依靠类伴生对象中unapply实现
  • unapply方法返回值应该符合
    • 若用于判断真假,可以返回Boolean类型值
    • 若用于提取单个T类型的值,可以返回Option[T]
    • 若需要提取多个类型的值,可以将其放在可选元组 Option[(T1, T2, ..., Tn)]
  • case class默认实现有此方法
1
2
3
4
5
6
7
8
class Dog(val name: String, val age: Int){
// 伴生对象中定义`unapply`方法解构对象
object Dog{
def unapply(dog: Dog): Option[(String, Int)]{
if (dog!=null) Some(dog.name, dog.age)
else None
}
}

unapplySeq

unapplySeq方法:接受实例对象、返回创建其所用的序列

  • 序列模式依靠类伴生对象中unapplySeq实现
  • 方法应返回Option[Seq[T]]
  • Seq[A+]默认实现此方法
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
// `scala.Array`伴生对象定义
object Array extends FallbackArrayBuilding{
def apply[T: ClassTag](xs: T*): Array[T] = {
val array = new Array[T](xs.length)
var i = 0
for(x <- xs.iterator) {
array(i) = x
i += 1
}
array
}
def apply[x: Boolean, xs: Boolean*]: Array[Boolean] = {
val array = new Array[Boolean](xs.length + 1)
array(0) = x
var i = 1
for(x <- xs.iterator){
array(i) = x
i += 1
}
array
}
/* 省略其它`apply`方法 */
def unapplySeq[T](x: Array[T]): Option[IndexedSeq[T]] =
if(x == null) None
else Some(x.toIndexedSeq)
}

正则表达式模式匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import scala.util.matching.{Regex, MatchIterator}
// `.r`方法将字符串转换为正则表达式
val dateP = """(\d\d\d\d)-(\d\d)-(\d\d)""".r
val dateP = new Regex("""(\d\d\d\d)-(\d\d)-(\d\d)""")

// 利用`Regex`的`unapplySeq`实现模式匹配
// 不过此应该是定义在`Regex`类而不是伴生对象中
for(date <- dateP.findAllIn("2015-12-31 2016-01-01")){
date match {
case dateRegex(year, month, day) =>
case _ =>
}
}
// `for`语句中模式匹配
for(dateP(year, month, day) <- dateP.findAllIn("2015-12-31 2016-01-01")){
}
  • scala.util.matching具体参见cs_java/scala/stdlib

Functions

函数:带有参数的表达式

1
2
3
4
5
// 带一个参数的匿名函数,一般用作高阶函数参数
Array(1,2,3).map((x: Int) => x + 1)
Array(1,2,3).map(_+1) // 简化写法
// 具名函数
val fx: Float => Int = (x: Float) => x.toInt
  • 函数结构

    • 可以带有多个参数、不带参数
    • 无法显式声明返回值类型
  • 函数可以类似普通常量使用lazy修饰,当函数被使用时才会被 创建

  • 作高阶函数参数时可简化

    • 参数类型可推断、可被省略
    • 仅有一个参数,可省略参数周围括号
    • 仅有一个参数,可使用占位符_替代参数声明整体
  • Java:函数被编译为内部类,使用时被创建为对象、赋值给相应 变量
  • Scala中函数是“一等公民”,允许定义高阶函数、方法
    • 可以传入对象方法作为高阶函数的参数,Scala编译器会将 方法强制转换为函数
    • 使用高阶函数利于减少冗余代码

函数类型

函数类型:Scala中有Funtion<N>[T1, T2, ..., TN+1]共22种函数 类型,最后泛型参数为返回值类型

1
2
3
4
5
// 实例化`Function2[T1, T2, T3]`创建函数
val sum = new Function2[Int, Int, Int] {
def apply(x: Int, y: Int): Int = x + y
}
val sum = (x: Int, y: Int) => x + y

偏函数

偏函数:只处理参数定义域中子集,子集之外参数抛出异常

1
2
3
4
5
6
7
8
9
10
11
// scala中定义
trait PartialFunction[-A, +B] extends (A => B){
// 判断元素在偏函数处理范围内
def isDefinedAt(?ele: A)
// 组合多个偏函数
def orElse(?pf: PartialFunction[A, B])
// 方法的连续调用
def addThen(?pf: PartialFunction[A, B])
// 匹配则调用、否则调用回调函数
def applyOrElse(?ele: A, ?callback: Function1[B, Any])
}
  • 偏函数实现了Function1特质
  • 用途
    • 适合作为map函数参数,利用模式匹配简化代码
1
2
3
4
5
val receive: PartialFunction[Any, Unit] = {
case x: Int => println("Int type")
case x: String => println("String type")
case _ => println("other type")
}

Methods

方法:表现、行为类似函数,但有关键差别

  • def定义方法,包括方法名、参数列表、返回类型、方法体

  • 方法可以接受多个参数列表、没有参数列表

    1
    2
    3
    def addThenMutltiply(x: Int, y: Int)(multiplier: Int): Int = (x+y) * multiplier

    def name: String = System.getProperty("user.name")
  • Scala中可以嵌套定义方法

  • Java中全在类内,确实都是方法

Currying

柯里化:使用较少的参数列表调用多参数列表方法时会产生新函数, 该函数接受剩余参数列表作为其参数

  • 多参数列表/参数分段有更复杂的调用语法,适用场景
    • 给定部分参数列表
      • 可以尽可能利用类型推断,简化代码
      • 创建新函数,复用代码
    • 指定参数列表中部分参数为implicit
1
2
3
4
5
6
7
8
val number = List(1,2,3,4,5,6,7,8,9)
numbers.foldLeft(0)(_ + _)
// 柯里化生成新函数
val numberFunc = numbers.foldLeft(List[Int]())_
val square = numberFunc((xs, x) => xs:+ x*x)
val cube = numberFunc((xs, x) => xs:+ x*x*x)

def execute(arg: Int)(implicit ec: ExecutionContext)

隐式转换

隐式转换:编译器发现类型不匹配时,在作用域范围内查找能够转换 类型的隐式转换

  • 类型S到类型T的隐式转换由S => T类型函数的隐式值、 或可以转换为所需值的隐式方法定义

    • 隐式转换只与源类型、目标类型有关
    • 源类型到目标类型的隐式转换只会进行一次
    • 若作用域中有多个隐式转换,编译器将报错
  • 适用场合:以下情况将搜索隐式转换

    • 隐式转换函数、类:表达式e的类型S不符合期望类型 T
    • 隐式参数列表、值:表达式e类型S没有声明被访问的 成员m
  • 隐式转换可能会导致陷阱,编译器会在编译隐式转换定义时发出 警告,可以如下关闭警告

    • import scala.language.implicitConversions到隐式 转换上下文范围内
    • 启用编译器选项-language: implicitConversions
1
2
3
4
5
6
7
8
import scala.language.implicitCoversions

// `scala.Predef`中定义有
// 隐式转换函数
implicit def int2Integer(x: Int) =
java.lang.Integer.valueOf(x)
// 隐式参数列表
@inline def implicitly[T](implicit e:T) = e

隐式转换函数、类

1
2
3
4
5
6
7
8
9
10
11
// 隐式转换函数
implicit def float2int(x: Float) = x.toInt

// 隐式类
implicit class Dog(val name: String){
def bark = println(s"$name is barking")
}
"Tom".bark

// 隐式类最终被翻译为
implicit def string2Dog(name: String): Dog = new Dog(name)
  • 隐式类通过隐式转换函数实现
    • 其主构造函数参数有且只有一个
    • 代码更简洁、晦涩,类和方法绑定

隐式值、参数列表

  • 若参数列表中参数没正常传递,Scala将尝试自动传递 正确类型的隐式值

  • 查找隐式参数的位置

    • 调用包含隐式参数列表的方法时,首先查找可以直接访问、 无前缀的隐式定义、隐式参数
    • 在伴生对象中查找与隐式候选类型相关的、有隐式标记的 成员
  • 说明

    • 隐式值不能是顶级值
    • implicit能且只能作用于最后参数列表
    • 方法才能包含隐式参数列表,函数不可
    • 包含隐式参数列表方法不可偏函数化
  • 例1

    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
    abstract class Monoid[A]{
    def add(x: A, y: A): A
    def unit: A
    }
    object ImplicitTest{
    // `implicit`声明该匿名类对象为隐式值
    implicit val stringMonoid: Monoid[String] = new Monoid[String]{
    def add(x: String, y: String): Strinng = x concat y
    def unit: String = ""
    }
    implicit val intMonoid: Monoid[Int] = new Monoid[Int] {
    def add(x: Int, y: Int): Int = x + y
    def unit: Int = 0
    }

    // 定义隐式参数列表
    def sum[A](xs: List[A])(implicit m: Monoid[A]): A =
    if (xs.isEmpty) m.unit
    else m.add(xs.head, sum(xs.tail))

    def main(args: Array[String]): Unit = {
    println(sum(List(1,2,3)))
    println(sum(List("a", "b", "c")))
    }
    }
  • 例2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    trait Multiplicable[T] {
    def multiply(x: T): T
    }
    // 定义隐式单例对象
    implicit object MultiplicableInt extends Multiplicable[Int]{
    def multiply(x: Int) = x*x
    }
    implicit object MultiplicableString extends Mulitplicable[String]{
    def multiply(x: String) = x*2
    }
    // `T: Multiplicable`限定作用域存在相应隐式对象、或隐式值
    def multiply[T: Multiplicable](x: T) = {
    // 调用`implicitly`返回隐式对象、或隐式值
    val ev = implicitly[Multiplcable[T]]
    ev.multiply(x)
    }
    println(multiply(5))
    println(multiply(5))

传名参数

传名参数:仅在被使用时触发实际参数的求值运算

1
2
def calculate(input: => Int) = input * 37
// 在参数类型前加上`=>`将普通(传值)参数变为传名参数
  • 传名参数
    • 若在函数体中未被使用,则不会对其求值
    • 若参数是计算密集、长时运算的代码块,延迟计算能力可以 帮助提高性能
  • 传值参数:仅被计算一次

传名参数实现while循环

1
2
3
4
5
6
7
8
9
10
11
def whileLoop(condition: => Boolean)(body: => Unit): Unit =
if(condition){
body
whileLoop(condition)(body)
}

var i = 2
whileLoop(i > 0){
println(i)
i -= 1
}

默认参数

默认参数:调用时可以忽略具有默认值的参数

  • 调用时忽略前置参数时,其他参数必须带名传递
  • 跳过前置可选参数传参必须带名传参
  • Java中可以通过剔除可选参数的重载方法实现同样效果
  • Java代码中调用时,Scala中默认参数必须、不能使用命名参数

命名参数调用

  • 调用方法时,实际参数可以通过其对应形参名标记
    • 命名参数顺序可以打乱
    • 未命名参数需要按照方法签名中形参顺序放在前面

Exception

异常处理:try{throw...}catch{case...}抛出捕获异常

1
2
3
4
5
6
7
8
9
10
def main(args: Array[String]){
try{
val fp = new FileReader("Input")
}catch{
case ex: FileNotFoundException => println("File Missing")
case ex: IOException => println("IO Exception")
}finally{
println("finally")
}
}

Scala 自定义实体

Class

  • Scala是纯面向对象语言,所有变量都是对象,所有操作都是 方法调用
  • class定义类,包括类名、主构造参数,其中成员可能包括

    • 普通类可用new创建类实例

成员说明

  • 常/变量:声明时必须初始化,否则需定义为抽象类, 相应成员也被称为抽象成员常/变量
    • 可以使用占位符_初始化
      • AnyVal子类:0
      • AnyRef子类:null
  • 抽象类型:type声明
  • 方法、函数
  • 内部类、单例对象:内部类、单例对象绑定至外部对象
    • 内部类是路径依赖类型
    • 不同类实例中的单例对象不同
  • Java:仍然会为匿名类生成相应字节码文件
  • Java:public类成员常/变量在编译后自动生成getter、 setter(仅变量)方法
    • 即使是公有成员也会被编译为private
    • 对其访问实际上是通过setter、getter方法(可显式调用)

内部类型投影

类型投影:Outter#Inner类型InnerOutter类型作为前缀, Outter不同实例中Inner均为此类型子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Outter{
class Inner
def test(i: Outter#Inner) = i
}

val o1 = new Outter
val o2 = new Outter

// 二者为不同类型
val i1 = new o1.Inner
val i2 = new o2.Inner

// 类型投影父类作为参数类型
o1.test(i2)
o2.test(i1)

单例类型

单例类型:所有对象都对应的类型,任意对象对应单例类型不同

1
2
3
4
5
6
import scla.reflect.runtime.universe.typeOf
class Dog
val d1 = new Dog
val d2 = new Dog
typeOf[d1.type] == typeOf[d2.type] // `false`
val d3: d1.type = d1 // 两者单例类型也不同
链式调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Pet{
var name: String = _
var age: Float = _
// 返回类型单例类型,保证类型不改变
def setName(name: String): this.type = {
this.name = name
this
}
def setAge(age: Float): this.age = {
this.age = age
this
}
}
class Dog extends Pet{
var weight: Float = _
def setWeight(weight: Float): this.type = {
this.weight = weight
this
}
}

// 若没有设置返回类型为单例类型`this.type`,`setAge`之后
// 将返回`Pet`类型,没有`setWeight`方法,报错
val d = new Dog().setAge(2).setWeight(2.3).setName("Tom")

Constructor

1
2
3
4
5
6
7
8
// 带默认参数的、私有主构造方法
class Person private(var name: String, var age: Int=10){
// 辅助构造方法
def this(age: Int) = {
// 必须调用已定义构造方法
this("", age)
}
}
  • 主构造方法:类签名中参数列表、类体

    • 其参数(类签名中)被自动注册为类成员变、常量

      • 其可变性同样由varval声明
      • 其访问控制类似类似类体中成员访问控制
      • 但缺省继承父类可见性,否则为private[this] val
      • 参数中私有成员变、常量没有被使用,则不会被生成
      • case class主构造方法中缺省为public val
    • 其函数体为整个类体,创建对象时被执行

    • 可类似普通方法

      • 提供默认值来拥有可选参数
      • 将主构造方法声明为私有
  • 辅助构造方法:this方法

    • 辅助构造方法必须以先前已定义的其他辅助构造方法、或 主构造方法的调用开始
  • 主构造方法体现于其中参数被自动注册为类成员

类继承

1
2
3
4
// `extends`指定类继承
class Student(name: String, age: Int, var studentNo: Int)
extends Person(name: String, age: Int){
}
  • 必须给出父类的构造方法(可以是辅助构造方法)

    • 类似强制性C++中初始化列表
  • 构造函数按以下规则、顺序执行

    • 父类、特质构造函数
    • 混入特质:多个特质按从左至右
    • 当前类构造函数
  • 可用override重写从父类继承的成员方法、常/变量

    • 重写父类抽象成员(未初始化)可省略override
    • java中方法都可以视为C++中虚函数,具有多态特性

提前定义、懒加载

  • 由于构造方法的执行顺序问题,定义于trait中的抽象成员 常/变量可能会在初始化化之前被使用
  • 可以考虑使用提前定义、懒加载避免
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.PrintWriter
trait FileLogger{
// 抽象变量
val fileName: String
// 抽象变量被使用
val fileOutput = new PrintWriter(fileName: String)
// `lazy`懒加载,延迟变量创建
lazy val fileOutput = new PrintWriter(fileName: String)
}
class Person
class Student extends Person with FileLogger{
val fileName = "file.log"
}
val s = new {
// 提前定义
override val fileName = "file.log"
} with Student

访问控制

  • 缺省:Scala没有public关键字,缺省即为public
  • protected[<X>]:在X范围内的类、子类中可见
  • private[<X>]:在X范围内类可见
  • X可为包、类、单例对象等作用域

    • 缺省为包含当前实体的上层包、类、单例对象,即类似Java 中对应关键字
  • 特别的private[this]表示对象私有

    • 只有对象本身能够访问该成员
    • 类、伴生对象中也不能访问

      1
      2
      3
      4
      5
      6
      7
      8
      class Person{
      private[this] age = 12
      def visit() = {
      val p = new Person
      // 报错,`age`不是`Person`成员
      println(p.age)
      }
      }

特殊类

abstract class

抽象类:不能被实例化的类

  • 抽象类中可存在抽象成员常/变量、方法,需在子类中被具体化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义抽象类
abstract class Person(var name: String, var age: Int){
// 抽象成员常量
val gender: String
// 抽象方法
def print: Unit
}

// 抽象类、泛型、抽象类型常用于匿名类
val p = new Person("Tom", 8){
override gender = "male"
// 覆盖抽象成员可以省略`override`
def print: Unit = println("hello")
}

case class

样例类

  • 和普通类相比具有以下默认实现

    • apply:工厂方法负责对象创建,无需new实例化样例类
    • unapply:解构方法负责对象解构,方便模式匹配
    • equals:案例类按值比较(而不是按引用比较)
    • toString
    • hashCode
    • copy:创建案例类实例的浅拷贝,可以指定部分构造参数 修改部分值
  • 类主构造函数参数缺省为val,即参数缺省不可变公开

  • 用途

    • 方便模式匹配
  • 样例类一般实例化为不可变对象,不推荐实例化为可变
1
2
3
4
case class Message(sender: String, recipient: String, body: String)
val message4 = Message("A", "B", "message")
// 指定部分构造参数的拷贝
val message5 = message.copy(sender=message4.recipient, recipient="C")

Value Class

Value Class:通过继承AnyVal以避免运行时对象分配机制

1
class Wrapper(val underlying: Int) extends AnyVal
  • 特点

    • 参数:仅有被用作运行时底层表示的公有val参数
    • 成员:可包含def自定义方法,但不能定义额外valvar、内嵌traitclassobject
    • 继承:只能继承universal trait,自身不能再被继承
    • 编译期类型为自定义类型,运行时类型为val参数类型
    • 调用universal trait中方法会带来对象分配开销
  • 用途

    • 和隐式类联合获得免分配扩展方法

      1
      2
      3
      4
      // 标准库中`RichInt`定义
      implicit class RichInt(val self Int) extends AnyVal{
      def toHexString: String = java.lang.Integer.toHexString(self)
      }
    • 不增加运行时开销同时保证数据类型安全

      1
      2
      3
      class Meter(val value: Double) extends AnyVal{
      def +(m: Meter): Meter = new Meter(value + m.value)
      }
  • JVM不支持value class,Scala有时需实例化value class
    • value class作为其他类型使用
    • value class被赋值给数组
    • 执行运行时类型测试,如:模式匹配

Object

单例对象:有且只有一个实例的特殊类,其中方法可以直接访问, 无需实例化

  • 单例对象由自身定义,可以视为其自身类的单例

    • 对象定义在顶层(没有包含在其他类中时),单例对象只有 一个实例
      • 全局唯一
      • 具有稳定路径,可以被import语句导入
    • 对象定义在类、方法中时,单例对象表现类似惰性变量
      • 不同实例中单例对象不同,依赖于包装其的实例
      • 单例对象和普通类成员同样是路径相关的
  • 单例对象是延迟创建的,在第一次使用时被创建

    • object定义
    • 可以通过引用其名称访问对象
  • 单例对象被编译为单例模式、静态类成员实现

Companion Object/Class

  • 伴生对象:和某个类共享名称的单例对象
  • 伴生类:和某个单例对象共享名称的类
  • 伴生类和伴生对象之间可以互相访问私有成员
    • 伴生类、伴生对象必须定义在同一个源文件中
  • 用途:在伴生对象中定义在伴生类中不依赖于实例化对象而存在 的成员变量、方法
    • 工厂方法
    • 公共变量
  • Java中static成员对应于伴生对象的普通成员
  • 静态转发:在Java中调用伴生对象时,其中成员被定义为伴生类 中static成员(当没有定义伴生类时)

apply

apply方法:像构造器接受参数、创建实例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import scala.util.Random

object CustomerID{
def apply(name: String) = s"$name--${Random.nextLong}"
def unapply(customerID: String): Option[String] = {
val stringArray:[String] = customer.ID.split("--")
if (stringArray.tail.nonEmpty) Some(StringArray.head)
else None
}
}

val customer1ID = CustomerId("Tom")
customer1ID match {
case CustomerID(name) => println(name)
case _ => println("could not extract a CustomerID")
}
val CustomerID(name) = customer1ID
// 变量定义中可以使用模式引入变量名
// 此处即使用提取器初始化变量,使用`unapply`方法生成值
val name = CustomerID.unapply(customer2ID).get
  • unapplyunapplySeq参见cs_java/scala/entity_components

应用程序对象

应用程序对象:实现有main(args: Array[String])方法的对象

  • main(args: Array[String])方法必须定义在单例对象中

  • 实际中可以通过extends App方式更简洁定义应用程序对象 ,trait App中同样是使用main函数作为程序入口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    trait App{
    def main(args: Array[String]) = {
    this._args = args
    for(proc <- initCode) proc()
    if (util.Propertie.propIsSet("scala.time")){
    val total = currentTime - executionStart
    Console.println("[total " + total + "ms")
    }
    }
    }
  • 包中可以包含多个应用程序对象,但可指定主类以确定包程序 入口

case object

样例对象:基本同case class

  • 对于不需要定义成员的域的case class子类,可以定义为 case object以提升程序速度
    • case object可以直接使用,case class需先创建对象
    • case object只生成一个字节码文件,case class生成 两个
    • case object中不会自动生成applyunapply方法

用途示例

创建功能性方法

1
2
3
4
5
6
7
8
9
10
11
package logging
object Logger{
def info(message: String): Unit = println(s"IFNO: $message")
}

// `import`语句要求被导入标识具有“稳定路径”
// 顶级单例对象全局唯一,具有稳定路径
import logging.Logger.info
class Test{
info("created projects")
}

Trait

特质:包含某些字段、方法的类型

  • 特点

    • 特质的成员方法、常/变量可以是具体、或抽象
    • 特质不能被实例化,因而也没有参数
    • 特质可以extends自类
  • 用途:在类之间共享程序接口、字段,尤其是作为泛型类型和 抽象方法

    • extend继承特质
    • with混入可以组合多个特质
    • override覆盖/实现特质默认实现
  • 子类型:需要特质的地方都可以使用特质的子类型替换

  • Java:trait被编译为interface,包含具体方法、常/变量 还会生成相应抽象类

Mixin

混入:特质被用于组合类

  • 类只能有一个父类,但是可以有多个混入

    • 混入和父类可能有相同父类
    • 通过Mixin trait组合类实现多继承
  • 多继承导致歧义时,使用最优深度优先确定相应方法

复合类型

复合类型:多个类型的交集,指明对象类型是某几个类型的子类型

1
<traitA> with <traitB> with ... {refinement}

sealed trait/sealed class

密封特质/密封类:子类确定的特质、类

1
2
3
4
sealed trait ST

case class CC1(id: String) extends ST
case object CC2 extends ST
  • 密封类、特质只能在当前文件被继承
  • 适合模式匹配中使用,编译器知晓所有模式,可提示缺少模式

自类型

自类型:声明特质必须混入其他特质,尽管特质没有直接扩展 其他特质

  • 特质可以直接使用已声明自类型的特质中成员

细分大类特质

1
2
3
4
5
6
7
8
9
10
11
12
13
trait User{
def username: String
}
trait Tweeter{
// 声明自类型,表明需要混入`User`特质
this: User =>
def tweet(tweetText: String) =
println(s"$username: $tweetText")
}
class VerifiedTweeter(val username_: String)
extends Tweeter with User{
def username = s"real $username_"
}

定义this别名

1
2
3
4
5
6
7
8
9
10
class OuterClass{
// 定义`this`别名,下面二者等价
outer =>
// outer: OuterClass =>
val v1 = "here"
class Innerclass {
// 方便在内部类中使用
println(outer.v1)
}
}

Universal Trait

Universal Trait:继承自Any、只有def成员、不作任何 初始化的trait

  • 继承自universal trait的value class同时继承该trait方法, 但是调用其会带来对象分配开销

泛型

  • 泛型类、泛型trait

    • 泛型类使用方括号[]接受类型参数
  • 泛型方法:按类型、值进行参数化,语法和泛型类类似

    • 类型参数在方括号[]中、值参数在圆括号()
    • 调用方法时,不总是需要显式提供类型参数,编译器 通常可以根据上下文、值参数类型推断
  • 泛型类型的父子类型关系不可传导

    • 可通过类型参数注释机制控制
    • 或使用类型通配符(存在类型)“构建统一父类”

存在类型/类型通配符

存在类型:<type>[T, U,...] forSome {type T; type U;...} ,可以视为所有<type>[]类型的父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 可以使用通配符`_`简化语法
// def printAll(x: Array[T] forSome {type T})
def printAll(x: Array[_]) = {
for (i <- x) {
print(i + " ")
}
println()
}

val a = Map(1 -> 2, 3 -> 3)
match a {
// 类型通配符语法可以用于模式匹配,但存在类型不可
case m: Map[_, _] => println(m)
}
  • 省略给方法添加泛型参数

类型边界约束

  • T <: A/T >: B:类型T应该是A的子类/B的父类

    • 描述is a关系
  • T <% STS的子类型、或能经过隐式转换为S子类型

    • 描述can be seen as关系
  • T : E:作用域中存在类型E[T]的隐式值

型变

型变:复杂类型的子类型关系与其组件类型的子类型关系的相关性

  • 型变允许在复杂类型中建立直观连接,利用重用类抽象
1
2
3
4
5
abstract class Animal{
def name: String
}
case class Cat(name: String) extends Animal
case class Cat(name: String) extends Animal

Covariant

协变:+A使得泛型类型参数A成为协变

  • 类型List[+A]A协变意味着:若AB的子类型,则 List[A]List[B]的子类型

  • 使得可以使用泛型创建有效、直观的子类型关系

1
2
3
4
5
6
7
8
9
10
11
12
def ConvarienceTest extends App{
def printAnimalNames(animals: List[Animal]): Unit = {
animals.foreach{ animal =>
println(animal.name)
}
}

val cats: List[Cat] = List(Cat("Winskers"), Cat("Tom"))
val dogs: List[Dog] = List(Dog("Fido"), Dog("Rex"))
printAnimalNames(cats)
printAnimalNames(dogs)
}

Contravariant

逆变:-A使得泛型类型参数A成为逆变

  • 同协变相反:若AB的子类型,则Writer[B]Writer[A]的子类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
abstract class Printer[-A] {
def print(value: A): Unit
}
class AnimalPrinter extends Printer[Animal] {
def print(animal: Animal): Unit =
println("The animal's name is: " + animal.name)
}
class CatPrinter extends Printer[Cat]{
def print(cat: Cat): Unit =
println("The cat's name is: " + cat.name)
}

val myCat: Cat = Cat("Boots")
def printMyCat(printer: Printer[Cat]): Unit = {
printer.print(myCat)
}

val catPrinter: Printer[Cat] = new CatPrinter
val animalPrinter: Printer[Animal] = new AnimalPrinter

printMyCat(catPrinter)
printMyCat(animalPrinter)
  • 协变泛型

    • 子类是父类的特化、父类是子类的抽象,子类实例总可以 替代父类实例
    • 协变泛型作为成员
      • 适合位于表示实体的类型中,方便泛化、组织成员
      • 作为[部分]输出[成员]
    • 注意:可变的协变泛型变量不安全
  • 逆变泛型

    • 子类方法是父类方法特化、父类方法是子类方法抽象,子类 方法总可以替代父类方法
    • 逆变泛型提供行为、特征
      • 适合位于表示行为的类型中,方便统一行为
      • 仅作为输入
  • 协变泛型、逆变泛型互补

    • 对包含协变泛型的某类型的某方法,总可以将该方法扩展 为包含相应逆变泛型的类
1
2
trait Function[-T, +R]
// 具有一个参数的函数,`T`参数类型、`R`返回类型

Invariant

不变:默认情况下,Scala中泛型类是不变的

  • 可变的协变泛型变量不安全
1
2
3
4
5
6
7
8
9
10
11
12
13
class Container[A](value: A){
private var _value: A = value
def getValue: A = _value
def setValue(value: A): Unit = {
_value = value
}
}

// 若`A`协变
val catContainer: Container[cat] = new Container(Cat("Felix"))
val animalContainer: Container[Animal] = catCont
animalContainer.setValue(Dog("Spot"))
val cat: Cat = catContainer.getValue

Type Erasure

类型擦除:编译后删除所有泛型的类型信息

  • 导致运行时无法区分泛型的具体扩展,均视为相同类型
  • 类型擦除无法避免,但可以通过一些利用反射的类型标签解决

    • ClassTag
    • TypeTag
    • WeakTypeTag
    • 参见cs_java/scala/stdlib
  • Java:Java初始不支持泛型,JVM不接触泛型
    • 为保持向后兼容,泛型中类型参数被替换为Object、或 类型上限
    • JVM执行时,不知道泛型类参数化的实际类
  • Scala、Java编译器执行过程都执行类型擦除

抽象类型

抽象类型:由具体实现决定实际类型

  • 特质、抽象类均可包括抽象类型

    1
    2
    3
    4
    trait Buffer{
    type T
    val element: T
    }
  • 抽象类型可以添加类型边界

    1
    2
    3
    4
    5
    abstract class SeqBuffer extends Buffer{
    type U
    type T <: Seq[U]
    def length = element.length
    }
  • 含有抽象类型的特质、类经常和匿名类的初始化同时使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    abstract class IntSeqBuffer extends SeqBuffer{
    type U = Int
    }
    def newIntSeqBuf(elem1: Int, elem2: Int): IntSeqBuffer =
    new IntSeqBuffer{
    type T = List[U]
    val element = List(elem1, elem2)
    }
    // 所有泛型参数给定后,得到可以实例化的匿名类
  • 抽象类型、类的泛型参数大部分可以相互转换,但以下情况无法 使用泛型参数替代抽象类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    abstract class Buffer[+T]{
    val element: T
    }
    abstract class SeqBuffer[U, +T <: Seq[U]] extends Buffer[T]{
    def length = element.length
    }
    def newIntSeqBuf(e1: Int, e2: Int): SeqBuffer[Int, Seq[Int]] =
    new SeqBuffer[Int, List[Int]]{
    val element = List(e1, e2)
    }

包和导入

package

  • Scala使用包创建命名空间,允许创建模块化程序
  • 包命名方式

    • 惯例是将包命名为与包含Scala文件目录名相同,但Scala 未对文件布局作任何限制
    • 包名称应该全部为小写
  • 包声明方式:同一个包可以定义在多个文件中

    • 括号嵌套:使用大括号包括包内容
      • 允许包嵌套,可以定义多个包内容
      • 提供范围、封装的更好控制
    • 文件顶部标记:在Scala文件头部声明一个、多个包名称
      • 各包名按出现顺序逐层嵌套
      • 只能定义一个包的内容
  • 包作用域:相较于Java更加前后一致

    • 和其他作用域一样支持嵌套
    • 可以直接访问上层作用域中名称
      • 行为类似Python作用域,优先级高于全局空间中导入
      • 即使名称在不同文件中
  • 包冲突方案:Java中包名总是绝对的,Scala中包名是相对的, 而包代码可能分散在多个文件中,导致冲突

    • 绝对包名定位包__root__.<full_package_path>
      • 导入时:import __root__.<package>
      • 使用时:val v = new __root__.<class>
    • 串联式包语句隐藏包:包名为路径分段串,其中非结尾 包被隐藏
  • Scala文件顶部一定要package声明所属包,否则好像不会默认 导入scala.等包,出现非常奇怪的错误,如:没有该方法

import

  • import语句用于导入其他包中成员,相同包中成员不需要 import语句

    1
    2
    3
    4
    5
    6
    import users._
    import users.{User, UserPreferences}
    // 重命名
    import users.{UserPreferences => UPrefs}
    // 导入除`HashMap`以外其他类型
    import java.utils.{HashMap => _, _}
  • 若存在命名冲突、且需要从项目根目录导入,可以使用 __root__表示从根目录开始

  • scalajava.langobject Predef默认导入
  • 相较于Java,Scala允许在任何地方使用导入语句

包对象

包对象:作为在整个包中方便共享内容、使用的容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// in file gardening/fruits/Fruit.scala
package gardening.fruits

case class Fruit(name: String, color: String)
object Apple extends Fruit("Appple", "green")
object Plum extends Fruit("Plum", "blue")
object Banana extends Fruit("Banana", "yellow")

// in file gardening/fruits/package.scala
package gardening
package object fruits{
val planted = List(Apple, Plum, Banana)
def showFruit(fruit: Fruit): Unit = {
println(s"${fruit.name}s are ${fruit.color}")
}
}
  • 包对象中任何定义都认为时包自身的成员,其中可以定义 包作用域级任何内容

    • 类型别名
    • 隐式转换
  • 包对象和其他对象类似,每个包都允许有一个包对象

    • 可以继承Scala的类、特质
    • 注意:包对象中不能进行方法重载
  • 按照惯例,包对象代码通常放在文件package.scala

Annotations

注解:关联元信息、定义

  • 注解作用于其后的首个定义、声明
  • 定义、声明之前可以有多个注解,注解顺序不重要

确保编码正确性注解

  • 此类注解条件不满足时,会导致编译失败

@tailrec

@tailrec:确保方法尾递归

1
2
3
4
5
6
7
8
9
import scala.annotations.tailrec

def factorial(x: Int): Int = {
@tailrec
def factorialHelper(x: Int, accumulator: Int): Int ={
if (x == 1) accumulator
else factorialHelper(x - 1, accumulator * x)
}
}

影响代码生成注解

  • 此类注解会影响生成字节码

@inline

@inline:内联,在调用点插入被调用方法

  • 生成字节码更长,但可能运行更快
  • 不能确保方法内联,当且仅当满足某些生成代码大小的 启发式算法时,才会出发编译器执行此操作

@BeanProperty

@BeanProperty:为成员常/变量同时生成Java风格getter、setter 方法get<Var>()set<Var>()

  • 对成员变/常量,类编译后会自动生成Scala风格getter、 setter方法:<var>()<var>_$eq()

Java注解

  • Java注解有用户自定义元数据形式
    • 注解依赖于指定的name-value对来初始化其元素

@interface

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
// 定义跟踪某个类的来源的注解
@interface Source{
public String URL();
public String mail();
}

// Scala中注解应用类似构造函数调用
// 必须使用命名参数实例化Java注解
@Source(URL="http://coders.com",
mail="support@coders.com")
public class MyClass

// 若注解中只有单个[无默认值]元素、名称为`value`
// 则可以用类似构造函数的语法在Java、Scala中应用
@interface SourceURL{
public String value();
public String mail() defualt "";
}
@SourceURL("http://coders.com/")
public class MyClass
// 若需要给`mail`显式提供值,Java必须全部使用命名参数
@Source(URL="http://coders.com",
mail="support@coders.com")
public class MyClass
// Scala可以继续利用`value`特性
@SourceURL("http://coders.com/"
mail = "support@coders.com")
public class MyClass

线性最长问题

最长公共子串

求两个字符串s1、s2(长度分别为m、n)最长公共子串长度

矩阵比较

  • 将两个字符串分别以行、列组成矩阵M

  • 对矩阵中每个元素$M[i, j]$,若对应行、列字符相同

    • 元素置为1,否则置0 longest_common_substr

    • 置元素$M[i,j] = M[i-1, j-1] + 1$,否则置0 longest_common_substr

  • 则矩阵中最长的非0斜序列对应子串即为最长公共子串

算法特点

  • 时间效率$\in \Theta(mn)$
  • 输入增强

最长公共子序列

求两个序列X、Y的最长公共子序列

  • 子序列:去掉给定序列中部分元素,子序列中元素在原始序列中 不必相邻
  • 最长公共子序列可能有很多

动态规划

  • 先使用动态规划确认最长子序列长度,构造动态规划表

    • $C[i,j]$:序列X前i个元素子序列、序列Y前j个元素子序列 最大子序列长度
  • 根据动态规划表找出最长公共子序列

    longest_common_subseq_dynamic_table

    • 从动态规划表中首个格子开始,沿着某条格子路径达到 表中最后一个元素
    • 路径中值改变格子对应序列中元素即为最长公共子序列中 元素
    • 不同格子路径可能找到不同的最长公共子序列

算法特点

  • 时间效率

    • 动态规划部分$\in \Theta(|X||Y|)$
    • 生成公共子序列部分$\in Theta(|X|+|Y|)$
  • 动态规划

最长升/降序序列

寻找长度为N的序列L中最长单调自增子序列

最长公共子序列法

  • 将原序列升序排序后得到$L^{ * }$
  • 原问题转换为求$L, L^{ * }$最长公共子序列

算法特点

  • 时间效率:$\in \Theta(|L|^2)$

动态规划法

  • 使用动态规划法求出以$L[i]$结尾的最长升序子序列长度, 得到动态规划表

    • $C[i]$:以$L[i]$结尾的最长升序子序列长度
  • 则动态规划表中值最大者即为最长升序序列长度

算法特点

  • 时间效率$\in O(|L|^2)$

动态规划2

使用线性表记录当前能够找到的“最长上升子序列”

  • 若当前元素大于列表最后(大)元素:显然push进线性表

    • 则当前线性表长度就是当前子串中最长上升子序列
  • 若当前元素不大于列表中最后元素

    • 考虑其后还有元素,可能存在包含其的更长上升序列
    • 使用其替换线性表中首个大于其的元素
      • 隐式得到以当前元素为结尾的最长上升子序列:其及 其之前元素
      • 更新包含其的上升子序列的要求:之后元素大于其
    • 不影响已有最长上升子序列长度,且若之后出现更长上升 子序列,则线性表被逐渐替换

算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lengthOfLIS(nums[0..n-1]):
// 动态规划求解最上升子序列
// 输入:序列nums
// 输出:最长上升子序列长度
if n == 0:
return 0
LIS = InitVector()
for num in nums:
if num > LIS.last()
LIS.push(num)
else:
for idx=0 to LIS.len():
if num <= LIS[idx]:
break
LIS[idx] = num
// 更新上升子序列中首个大于当前元素的元素
return LIS.len()

动态规划+二分

最长回文子串

中心扩展法

  • 遍历串,以当前元素为中心向两边扩展寻找以回文串

  • 为能找到偶数长度回文串,可以在串各元素间填充空位

    longest_subparlidrome_padding

    • 填充后元素位置$i = 2 i + 1$、填充符位置$2 i$
    • 两端也要填充:否则可能出现#为中心回文和端点回文 等长,但返回#中心回文
    • 填充后得到最长回文串肯定是原最长回文串扩展
      • #中心:原串不存在偶数长度回文串更长,则显然
      • #中心:显然

算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
LongestSubParlidrome(nums[0..n-1]):
// 中心扩展法求解最长回文子串
// 输入:串nums[0..n-1]
// 输出:最长回文串
nnums = padding(nums)
nn = len(nnums)
max_shift, center = 0, -1
for i=0 to nn:
shift = 1
while i >= shift and i + shift < nn:
if nnums[i-shift] != nnums[i+shift]:
break
shift += 1

// 越界、不匹配,均为-1得到正确、有效`shift`
shift -= 1

if shift > max_shift:
max_shift, center = shift, i

left = (center - max_shift + 1) // 2
right = (center + max_shift) // 2
return nums[left : right]

特点

  • 算法效率
    • 时间复杂度$\in O(n^2)$
    • 空间复杂度$\in O(1)$

动态规划

Manacher算法

  • 考虑已经得到的以$i$为中心、半径为$d$回文子串对称性

    • 则$[i-d, i+d+1)$范围内中回文对称
    • 即若$i-j, j<d$为半径为$dd$的回文串中心,则$2i - j$ 同样为半径为$dd$的回文串中心 ($[i-d, i+d-1)$范围内)
  • 所以可以利用之前回文串信息减少之后回文串检测

  • Manacher算法同样有中心扩展算法问题,要填充检测偶数长串

算法

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
LongestSubParlidrome(nums[0..n-1]):
// Manacher算法利用之前回文串信息求解最长回文子串
// 输入:串nums[0..n-1]
// 输出:最长回文串
nnums = padding(nums)
nn = len(nnums)
shift_d = [0] * nn
max_shift, center = 0, 0
for i=0 to nn:

// 利用之前信息初始化
shift = shift_d[i]

while shift <= i and shift < nn - i:
if nnums[i+shift] != nnums[i - shift]:
break
shift += 1
shift -= 1

// 更新可利用信息
for j=1 to shift:
shift_d[i+j] = max(
shift_d[i+j],
min(shift_d[i-j], i+j-shift))

if shift > max_shift:
max_shift, center = shift, i

left = (center - max_shift + 1) // 2
right = (center + max_shift) // 2
return nums[left: right]

特点

  • 算法效率
    • 时间复杂度$\in \Theta(n)$
    • 空间复杂度$\in \Theta(n)$

标准库

Scala Package

Any

1
2
3
4
5
6
class Any{
// 判断对象是否是否为类型`T`实例
def isInstanceOf[T]
// 将对象强转为类型`T`实例
def asInstanceOf[T]
}
1
2
3
4
1.asInstanceOf[String]				// 报错,未定义隐式转换函数
1.isInstanceOf[String] // `false`
List(1),isInstanceOf[List[String]] // `true`,泛型类型擦除
List(1).asInstanceOf[List[String]] // 成功,泛型类型擦除

Option

1
2
3
4
5
6
7
8
class Option[T]{
// `Some`实例:返回被被包裹值
// `None`实例:返回参数(默认值)
def getOrElse(default?: T)
}

class Some[T](s?: T) extends Option[T]
object None extends Option[_] ???
  • 推荐使用Option类型表示可选值,明示该值可能为None

  • Option类型可以被迭代

    • Some(s):唯一迭代s
    • None:空
    1
    2
    val a = Some("hello")
    a.foreach(x => println(x.length))

Predef

1
2
3
4
object Predef extends LowPriorityImplicits{
// 返回运行过程中类型,具体实现由编译器填补
def classOf[T]: Class[T] = null
}

List

collection

mutable

Map

1
2
3
4
val a=Map((3,4), 5->6)
// 两种创建`Map`、二元组形式等价
a.map{case (a, b) => println(a, b)}
// `{case...}`为偏函数(或`Function1`)

immutable

reflect

runtime

universe

  • universe:提供一套完整的反射操作,可以反思性的检查 类型关系,如:成员资格、子类型
1
2
// 返回类型`T`“类型值”,可以用于比较
typeOf[T]
TypeTag
  • TypeTag:提供编译时具体类型的信息

    • 能获取准确的类型信息,包括更高的泛型类型
    • 但无法获取运行时值的类型信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import scala.reflect.runtime.universe.{TypeTag, TypeRef, typeTag}

    // 声明隐式参数列表
    def recognize[T](x: T)(implicit tag: TypeTag[T]): String =
    tag.tpe match {
    case TypeRef(utype, usymbol, args) =>
    List(utype, usymbol, args).mkString("\n")
    }

    val list: List[Int] = List(1,2)
    val ret = recognize(list)

    // 显式实例化`TypeTag`
    val tag = typeTag[List[String]]
  • WeakTypeTag:提供编译时包括抽象类型的信息

    • WeakTypeTag可以视为TypeTag的超集
    • 若有类型标签可用于抽象类型,WeakTypeTag将使用该标记
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import scala.reflect.runtime.universe.{WeakTypeTag, TypeRef, weakTypeRef}

    // 声明隐式参数列表
    def recognize[T](x: T)(implicit tag: WeakTypeTag[T]): String =
    tag.tpe match {
    case TypeRef(utype, usymbol, args) =>
    List(utype, usymbol, args).mkString("\n")
    }
    abstract class SAClass[T]{
    // 抽象类型
    val list: List[T]
    val result = Recognizer.recognize(list)
    println(result)
    }
    new SAClass[Int] { val list = List(2,3)}

    // 显式实例化`WeakTypeTag`
    val tag = weakTypeTag[List[String]]
  • 当需要TypeTag[T]WeakTypeTag[T]类型的隐式值tag时, 编译器会自动创建,也可以显式实例化
  • 以上类型探测通过反射实现,编译器根据传递实参推断泛型 参数T,由此确定特定类型标签隐式值

ClassTag

ClassTag:提供关于值的运行时信息

  • 不能在更高层次上区分,如:无法区分List[Int]List[String]
  • 是经典的老式类,为每个类型捆绑了单独实现,是标准的 类型模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import scala.reflect.{ClassTag, classTag}

// def extract[T: ClassTag](list: List[Any]) =
def extract[T](list: List[Any])(implicit ct: ClassTag[T]) =
list.flatMap{
case element: T => Some(element)
// 以上被翻译为如下,告诉编译器`T`的类型信息
// case element @ ct(_: T) =>
// 否则泛型`T`被删除,编译不会中断,但是无法正确工作
case _ => None
}

val list: List[Any] = List(1, "string1", List(), "string2")
val rets = extract[String](list)

// 显式实例化`ClassTag[String]`
val ct = classTag[String]
  • 当需要ClassTag[T]类型的隐式值ct时,编译器会自动创建

    • 也可以使用classTag显式实例化
  • ClassTag[T]类型值ct存在时,编译器将自动

    • 包装(_:T)类型模式为ct(_:T)
    • 将模式匹配中未经检查的类型测试模式转换为已检查类型

util

matching

Regex

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
import scala.util.matching.{Regex, Match}

class Regex(?pattern: String, ?group: String*){
def findAllIn(source: CharSequence): MatchIterator
def findAllMatchIn(source: CharSequence): Iterator[Match]
def findFirstIn(source: CharSequence): Option[String]
def findFirstMatchIn(source: CharSequence): Option[Match]
def replaceAllIn(target: CharSequence, replacer: (Match) => String): String
def replaceAllIn(target: CharSequence, replacement: String): String
def replaceFirstIn(target: CharSequence, replacement: String): String
// 此`unapplySeq`就应该是定义在类中
def unapplySeq(target: Any): Option[List[String]] = target match{
case s: CharSequence => {
val m = pattern matcher s
if (runMatcher(m)) Some((1 to m.groupCount).toList map m.group)
else None
}
// 等价于重载
case m: Match => unapplySeq(m.matched)
case _ => None
}
}

class Match{
def group(?key: Int): String
def group(?key: String): String
}
1
2
3
4
5
6
7
8
9
10
val keyValPattern: Regex = "([0-9a-zA-Z-#() ]+): ([0-9a-zA-Z-#() ]+)".r
// `String.r`可使任意字符串变成正则表达式
// 使用括号同时匹配多组正则表达式

val input: String =
"""backgroud-color: #A03300l
|background-image: url(img/header100.png);""".stripMargin

for(patternMatch(attr, value) <- keyValPattern.findAllMatchIn(input))
println(s"key: ${attr}, value: ${value}")

Shell编程基础

变量

定义、使用、删除

  • 定义变量

    • 定义时不加$符号
    • 变量名和等号之间不能有空格
    • 变量名只能由英文字母、数字、下划线,且不以数字开头, 不能用bash中的关键字
    • 已经定义变量可以重新定义
  • 使用变量

    • 即可使用, 未定义变量直接使用不报错,返回空值
    • {}可选,但是{}能够明确定义变量名范围,帮助阅读、 解释器执行
  • 匿名变量

    • 可以认为所以语句(命令)的执行结果都存储在一个匿名 变量中,可以在其之前加上$获取其结果
  • readonly:设置变量为只读变量,更改(重定义)变量报错

  • unset:删除变量,不能删除只读变量

局部变量

局部变量:在脚本、命令中定义

  • 仅在当前shell实例中有效

    • shell变量默认为global,作用域从被定义处开始到脚本 末尾(即使在函数内部定义)
    • 显式local声明作用域局限于函数内部
  • 其他shell启动的程序不能访问局部变量

环境变量

环境变量:就是shell中的普通变量,只是这些变量往往被进程读取 用于自身初始化、设置

  • 狭义:export/declare -x声明的变量,只有这样的变量 才能默认被子进程继承
  • 广义;shell中所有的变量(包括局部变量)

环境变量设置

在shell中设置环境变量有两种方式

  • export/declare -x:设置全局(环境)变量

    • 任何在该shell设置环境变量后,启动的(子)进程都会 继承该变量
    • 对于常用、许多进程需要的环境变量应该这样设置
  • <ENV_NAME>=... cmd:设置临时环境变量

    • <ENV_NAME>=...不能被子进程继承,所以必须在其后立刻 接命令
    • 只对当前语句有效,也不能覆盖同名变量

用途

  • 环境变量是某些程序正常运行的必要条件
  • 所有程序都能访问环境变量,可用作进程通信
    • 在程序中设置环境变量,供其他进程访问变量
    • 父进程为子进程设置环境变量,供其访问

Shell变量

Shell变量:shell程序设置的特殊变量,包括环境变量、局部变量, 保证shell的正常运行

  • $0:shell执行脚本名

    • 交互式bash(即普通终端中)该参数为-bash
      • source执行脚本时,该参数可能非预期
    • 以下shell变量均不考虑$0,如
      • 函数中$0也是脚本名
      • shift无法跳过该参数
  • $1,..., $9:向脚本传递的位置参数

  • $@:脚本、函数参数列表
  • $*:脚本、函数参数字符串
  • $#:脚本、函数参数数量
  • $?:上条命令执行结果
  • $$$$:脚本进程号
  • $!:执行脚本最后一条命令

字符串

拼接

  • shell中字符默认为字符串,自动拼接

通配符

  • *:匹配任意长度字符串

    • 不包括./,必须显式匹配
  • ?:匹配一个字符

  • []:匹配出现在[]中的字符

    1
    2
    ls /[eh][to][cm]*
    # 匹配`/etc`、`/home`等
  • {}

    • 枚举全部

      1
      2
      3
      4
      5
      6
      $ mkdir {user1, user2}-{home, bin}
      # 笛卡尔积(字符串连接)
      # 等价于建立`user1-home, user1-bin, user2-home, user2-bin`
      $ echo {1..10}
      # 生成序列
      # 见`for`中`RangeIterator`部分

子串

1
2
$ file=/dir1/dir2/dir3/file.txt.bak
$ null=""

切片

  • ${<var_name>:n[:m]}:从第n开始m个字符
    • nm非负值
      • n:从0开始的下标起始
      • m切片长度缺省表示到末尾
    • nm使用0-负值表示
      • n:从0开始的负向下标起始
      • m:从0开始的负向下标终止
命令 解释 结果
${file:0:5} 提取首个字符开始的5个字符 /dir1
${file:5:5} 提取第5个字符开始的5个字符 /dir2
${file:5} 提取第5个字符开始至末尾 /dir2...
${file:0-5} 反向计算下标 t.bak
${file:0-5:0-1} 反向计算下标 t.ba
${file:5:0-2} 提取第5个字符开始至-2下标处 /dir2.../t.b

子串匹配

1
2
3
4
${<var_name>%<pattern>}
${<var_name>%%<pattern>}
${<var_name>#<pattern>}
${<var_name>##<pattern>}
  • #:去掉字符串左边最短pattern
  • ##:去掉字符串左边最长pattern
  • %:去掉字符串右边最短pattern
  • %%:去掉字符串右边最长pattern
  • 注意:var_name前不要变量标志$
  • 以上4种模式返回新值,不改变原变量值
  • 仅在pattern中使用通配符时,最短、最长匹配才有区别
命令 解释 结果
${file#*/} 去除首个/及其左边 dir2/dir3/file.txt.bak
${file##*/} 仅保留最后/右边 file.txt.bak
${file#*.} 去除首个.及其左边 txt.bak
${file##*.} 仅保留最后.右边 bak
${file%/*} 去除最后/及其右边 /dir1/dir2/dir3
${file%%*/} 去除首个/及其右边 空值
${file%*.} 去除最后.及其右边 /dir1/dir2/dir3/file.txt
${file%%*.} 去除首个.及其右边 /dir1/dir2/dir3/file.txt

替换

  • /from/to:替换首个fromto
  • //from/to:替换全部fromto
命令 解释 结果
${file/dir/path} 替换首个dirpath /path1/dir2/dir3/file.txt.bak
${file/dir/path} 替换全部dirpath /path1/path2/path3/file.txt.bak

默认值替换

  • -:变量未设置返回
  • +:变量设置返回
  • =:变量未设置返回、设置变量
  • ?:变量未设置输出至stderr
  • ::以上命令条件包括空值(空值视为未设置)

shell_variable_assignment

命令 解释 示例 结果
${井<var_name>} 获取字符串长度 ${井file} 27
${<var_name>-<default>} 变量未设置返回默认值 ${invalid-file.txt.bak} file.txt.bak
${<var_name>:-<default>} 变量未设置、空值返回默认值 ${null-file.txt.bak} file.txt.bak
${<var_name>+<default>} 变量设置返回默认值 ${file-file.txt.bak} fil.txt.bak
${<var_name>:+<default>} 变量非空返回默认值 ${file-file.txt.bak} file.txt.bak
${<var_name>=<default>} 变量未设置,返回默认值、并设置变量为默认值 ${invalid=file.txt.bak} file.txt.bak
${<var_name>:=<default>} 变量未设置、空值返回默认值、并设置变量为默认值 ${null=file.txt.bak} file.txt.bak
{$<var_name>?<default>} 变量未设置输出默认值至stderr {invalid?file.txt.bak} file.txt.bak输出至stderr
{$<var_name>:?<default>} 变量未设置、空值输出默认值至stderr {$null:?file.txt.bak} file.txt.bak输出至stderr

字符串比较

  • =, !=, -z, -n:字符串相等、不相等、为空、非空

  • 使用=比较变量是否为某字符串时,其中在两侧添加字符 保证=不为空值,否则若$test为空值,表达式报错

    1
    2
    3
    if [[ "$text"x = "text"x ]]; then
    command
    fi
  • 比较变量时要在两侧加上双引号"",否则可能报错、结果不 符合预期

数组

1
$ A=(a b c def)

取值

  • []:选择数组元素
    • [n]:返回数组第n个元素
    • [*]/[@]:返回数组全部元素
命令 解释 结果
${A[@]} 返回全部元素 a b c def
${A[*]} 同上 同上
${A[0]} 返回数组第一个元素 a
${井A[@]} 返回数组元素总数 4
${井A[*]} 同上 同上
${井A[3]} 返回数组第4个元素长度 3
A[3]=xyz 设置数组第4个元素值

数值

  • (())/[]:在其中执行整数运算
  • let:执行数值运算

规则

  • 返回的数据结果相当于存储在一个匿名变量中,使用的话需要 在之前加上$

  • 1
    2
    3
    $ a=5; b=7; c=2;
    $ echo $((a + b * $c))
    # 19
  • $((N#xx))/$[$#xx]:将其他进制数据转换为10进制

    1
    2
    $ echo $((2#110))
    # 6
  • 自增、自减运算可以在(())/[]中直接执行

    1
    2
    $ a=5; ((a++)); echo $a;
    # 6

特殊运算符

  • ~:位与
  • |:位或
  • ^:位异或
  • >>:位右移
  • <<:位左移
  • **:数值平方
  • +=, ++, -=, --, *=, \=:自变化运算符

逻辑运算

  • -eq, -ne, -ge, -le, -lt, -gt:比较
  • ==, !=, >=, <=, <, >:数值比较

文件

  • -e:目标存在
  • -f:文件为一般文件(非目录、设备)
  • -s:文件大小非0
  • -d:目标为目录
  • -b:文件为块设备(软盘、光驱)
  • -c:文件为流设备(键盘、modem、声卡)
  • -p:文件为管道
  • -h/L:目标为符号链接
  • -S:目标为Socket
  • -t:文件描述符被关联到终端
    • 一般用于检测脚本的stdin是否来自终端[ -t 0 ],或 输出是否输出至终端[ -t 1 ]
  • -r:目标是否可读权限
  • -w:目标是否可写权限
  • -x:目标是否可执行权限
  • -g:目标是否设置有set-group-id
  • -u:目标是否设置有set-user-id
  • -k:目标是否设置sticky bit
  • -O:用户是否是文件所有者
  • -G:用户是否属于文件所有组
  • -N:文件从上次读取到现在为止是否被修改
  • f1 -nt f2:文件f1f2
  • f1 -ot f2:文件f1f2
  • f1 -ef f2:文件f1f2指向相似文件实体(相同的硬链接)