这一部分介绍Julia的向量、元组、集合、字典等复合数据结构
以及函数的进一步介绍。

一维数组

Julia支持一维和多维的数组,
当一维数组的元素是数值时,
也可以理解成数学中的向量。

在程序中直接定义一个向量,
只要用方括号内写多个逗号分隔的数值,如

v1 = [2, 3, 5, 7, 11, 13, 17]
7-element Vector{Int64}:
  2
  3
  5
  7
 11
 13
 17
4-element Vector{Float64}:
 1.5
 3.0
 4.0
 9.12

其中v1是整数型的向量, v2是浮点型Float64的向量。
也可以定义元素为字符串的数组,
元素为不同类型的数组,
等等:

3-element Vector{String}:
 "苹果"
 "桔子"
 "香蕉"
v4 = [123, 3.14, "数学", [1, 2, 3]]
4-element Vector{Any}:
 123
   3.14
    "数学"
    [1, 2, 3]

length(x)求向量x的元素个数,如

可以用1:5定义一个范围,
在仅使用其中的元素值而不改写时作用与[1, 2, 3, 4, 5]类似。
1:2:9定义带有步长的范围,表示的值与[1, 3, 5, 7, 9]类似。
范围只需要存储必要的开始、结束、步长信息,
所以更节省空间,
但是不能对其元素进行修改。

1:5
## 1:5
1:2:7
## 1:2:7
5:-1:1
## 5:-1:1

范围不是向量,
而是一种“可遍历数据结构”。
collect()函数可以将范围转换成向量,如:

5-element Vector{Int64}:
 5
 4
 3
 2
 1

向量下标

x是向量,i是正整数,
x[i]表示向量的第i个元素。
第一个元素的下标为1,这种规定与R、FORTRAN语言相同,
但不同于Python、C、C++、JAVA语言。

v1 = [2, 3, 5, 7, 11, 13, 17]
7-element Vector{Int64}:
  2
  3
  5
  7
 11
 13
 17

end表示最后一个元素位置,如:

对元素赋值将在原地修改元素的值,如

v1[2] = 0
@show v1;
## v1 = [2, 0, 5, 7, 11, 13, 17]

这说明数组是“可变类型”(mutable),
即其中的成分可以原地修改。
字符串和元组则属于不可变类型(immutable)。

@show expr可以用比较简洁的带有提示的方式显示表达式和表达式的值。

用范围作为下标

下标可以是一个范围,如

v1 = [2, 3, 5, 7, 11, 13, 17]
v1[2:4]
3-element Vector{Int64}:
 3
 5
 7

在这种范围中,用end表示最后一个下标,如

4-element Vector{Int64}:
  7
 11
 13
 17
4-element Vector{Int64}:
 2
 3
 5
 7
4-element Vector{Int64}:
  2
  5
 11
 17
7-element Vector{Int64}:
 17
 13
 11
  7
  5
  3
  2

实际上,reverse(x)可以返回次序颠倒后的数组。

可以用仅有冒号作为下标,
这时表示包含所有元素的子集。
取出的多个元素可以修改,
可以用.=运算符赋值为同一个标量,如:

v1 = [2, 3, 5, 7, 11, 13, 17]
v1[:] .= 0; 
@show v1;
## v1 = [0, 0, 0, 0, 0, 0, 0]
v1 = [2, 3, 5, 7, 11, 13, 17]
v1[1:3] .= 0
@show v1;
## v1 = [0, 0, 0, 7, 11, 13, 17]

也可以分别赋值,如

v1 = [2, 3, 5, 7, 11, 13, 17]
v1[1:3] = [101, 303, 505]; 
@show v1;
## v1 = [101, 303, 505, 7, 11, 13, 17]

数组类型

当数组元素都是整数时,
显示其类型为“Array{Int64}”,
常用的还有“Array{Float64}”,
Array{String}”,
Array{Any}”等,
Any”是Julia语言类型系统的根类型,
相应的数组可以容纳任何Julia对象作为元素。

如果数组元素都是基本类型如Float64,
则不允许给元素赋值为其它类型,如:

3-element Vector{Float64}:
 1.2
 2.5
 3.6
vf[2] = "abc"
## MethodError: Cannot `convert` an object of type String to an object of type Float64
## .........

eltype()求元素类型,如:

向量初始化

zeros(n)可以生成元素类型为Float64、元素值为0、长度为n的向量,如

3-element Vector{Float64}:
 0.0
 0.0
 0.0

zeros(Int64, 3)可以生成指定类型的(这里是Int64)初始化向量。如

3-element Vector{Int64}:
 0
 0
 0

Vector{Float64}(undef, n)可以生成元素类型为Float64的长度为n的向量,
元素值未初始化,如

Vector{Float64}(undef, 3)
3-element Vector{Float64}:
 1.40319995e-315
 1.40320011e-315
 1.40320027e-315

类似可以生成其它元素类型的元素值未初始化向量,如

y1 = Vector{Int}(undef, 3);

用这样的办法为向量分配存储空间后可以随后再填入元素值。
可以用fill!()填入统一的值。
函数名以!结尾是一个习惯用法,
表示该函数会修改其第一自变量的值。

如:

3-element Vector{Int64}:
 100
 100
 100

也可以用fill(value, n)生成一个元素值都等于value的长度为n的一维数组。

可以用collect()将一个范围转换成可修改的向量。如:

5-element Vector{Int64}:
 1
 2
 3
 4
 5

变量与值

由于Julia的变量仅仅是向实际存储空间的引用(reference),
或称绑定(binding),
所以两个变量可以引用(绑定)到同一个向量的存储空间,
修改了其中一个变量的元素值,则另一个变量的元素也被修改了。

x1 = [1,2,3]
x2 = x1
x2[2] = 100
@show x1;
## x1 = [1, 100, 3]

用“===”或“”(\equiv+TAB)可以比较两个变量是否同一对象,
如:

允许两个变量指向同一个对象是有用的,
尤其在函数自变量传递时,
但是在一般程序中这种作法容易引起混淆。
向量(或者数组)作为函数自变量时,
调用函数时传递的是引用,
在函数内可以修改传递进来的向量的元素值。

如果需要制作数组的副本,
copy()函数。

x1 = [1,2,3]
x2 = copy(x1)
x2[2] = -100
@show x1;
## x1 = [1, 2, 3]
x2 === x1
## false

将仅有冒号的子集如x[:]放在等号左边可以修改所有元素,
如果将其放在等号右边并赋值给一个变量,
就可以制作副本,如:

x1 = [1,2,3]
x2 = x1[:]
x2[2] = -100
@show x1;
## x1 = [1, 2, 3]

Julia对象的这种引用或者绑定做法,
初学者比较容易用错。
例如,
下面的程序将一个数组嵌套在另一个数组中:

x0 = [3, 4]
x1 = [1,2, x0]
x1[3][1] = 333
@show x0;
## x0 = [333, 4]

因为x1中引用(绑定)了x0的值,
所以x1[3]x0共用同一存储,
修改了x1[3]就修改了x0
那么,
制作x1的副本能否解决问题?

x0 = [3, 4]
x1 = [1, 2, x0]
x2 = copy(x1)
x2[1] = 111
x2[3][1] = 333
@show x2;
## x2 = Any[111, 2, [333, 4]]
@show x1;
## x1 = Any[1, 2, [333, 4]]
@show x0;
## x0 = [333, 4]

虽然x1[1]没有被修改,但是x1[3][1]还是被修改了,
x0也被修改了。
这是因为copy()执行的是所谓“浅层复制”,
对于内嵌的对象仍为引用。
可以用deepcopy()
能解决大部分问题:

x0 = [3, 4]
x1 = [1,2, x0]
x2 = deepcopy(x1)
x2[1] = 111
x2[3][1] = 333
@show x2;
## x2 = Any[111, 2, [333, 4]]
@show x1;
## x1 = Any[1, 2, [3, 4]]
@show x0;
## x0 = [3, 4]

仅修改了x2,没有修改x1x0

向量的有关函数

为了判断元素x是否属于数组v,可以用表达式x in vx ∈ v判断,
结果为布尔值。

函数indexin(a, b)返回向量a的每个元素首次出现在b中的位置,
没有时返回nothing,如:

indexin([1,3,5,3], [1,2,3])
4-element Array{Union{Nothing, Int64},1}:
 1
 3
  nothing
 3

v是向量,x是一个元素,
push!(v, x)修改向量v
x添加到向量v的末尾。
pushfirst!(v, x)修改向量v
x添加到向量v的开头,原有的元素后移。
注意,
函数名以叹号结尾是一个习惯约定,
表示此函数会修改其第一个自变量。

insert!(v, k, xi)函数可以在向量v的指定下标k位置插入指定的一个元素,
原有的元素后移。

v3 = [2,3,5]
push!(v3, 7)
@show v3;
## v3 = [2, 3, 5, 7]
pushfirst!(v3, 1)
@show v3;
## v3 = [1, 2, 3, 5, 7]

v是向量,u也是一个向量,
append!(v, u)修改向量v
u的所有元素添加到向量v的末尾。
要注意append!push!的区别,
一个是添加一个向量的所有元素到末尾,
一个是添加一个元素到末尾。

v3 = [2,3,5]
append!(v3, [7,11])
@show v3;
## v3 = [2, 3, 5, 7, 11]

pop!(v)可以返回v的最后一个元素并从v中删除此元素。
popfirst!(v)类似。
splice!(v, k)函数可以返回指定下标位置的元素并从v中删除此元素,
deleteat!(v, k)函数可以v中删除指定下标位置的元素但不返回值。
empty!(x)可以情况数组的所有元素,
实际上,这个函数可以情况集合、字典等复合类型的元素。

注意,push!()等函数修改输入的向量的大小,
根据使用的环境,
这可能是很高效的做法,
但是数值计算程序中通常不修改数组大小,
而是预先分配好数组的大小。

如果确实无法预先确定数组大小,
又有运行效率的困扰,
可以用如sizehint!(x, 10000)这样的做法为数组预先提示一个大小,
这可以提高程序的效率。

replace!()函数可以用来在数组中替换元素,如:

x = [1, 2, 1, 4, 1]
replace!(x, 1 => 0)
@show x;
## x = [0, 2, 0, 4, 0]
x = [1, 2, 1, 4, 1]
replace!(x, 1 => 0, 4 => 3)
@show x;
## x = [0, 2, 0, 3, 0]

可以指定一个总替换次数的上限,如:

x = [1, 2, 1, 4, 1]
replace!(x, 1 => 0, 4 => 3, count = 2)
@show x;
## x = [0, 2, 0, 4, 1]

如果要合并两个一维数组并将结果生成一个新数组,
不修改原来的两个数组,
可以用vcat()函数,如:

v1 = [1,2]; v2 = [-2, -1]
v3 = vcat(v1, v2)
@show v3;
## v3 = [1, 2, -2, -1]
v3[1] = 111
@show v1;
## v1 = [1, 2]

filter!(f, x)指定一个示性函数f,将x中不满足条件的元素删除,
如:

x = [2, 3, 5, 7, 11, 13]
filter!(a -> a % 3 == 1, x)
show(x)
## [7, 13]

unique(v)返回去掉重复元素的结果,
unique!(v)则直接去掉v中的重复元素。

sort(v)返回向量v按升序排序的结果;
sort!(v)直接修改v,将其元素按升序排序。
如果要用降序排序,可以加选项rev=true
sortperm(v)返回将v的元素从小到大排序所需要的下标序列,
在多个等长向量按照其中一个的次序同时排序时此函数有用。

maximum(v)求最大值,
minimum(v)求最小值,
argmax(v)求最大值首次出现的下标,
argmin(v)求最大值首次出现的下标。
findmax(v)findmin(v)则返回最值和相应的首次出现的下标。

sum(v)求和,
prod(v)求乘积。

对于布尔型数组,
all(v)判断所有元素为真,
any(v)判断存在真值元素。

x == y可以比较两个等长数组的对应元素是否完全相同。

对于字符串x
可以用collect(x)将其转换为每个字符为一个元素的数组。
对于元组,
也可以用collect转换成数组。

广播

许多现代的数据分析语言,
如Python, Matlab, R等都存在循环的效率比编译代码低一两个数量级的问题,
在这些语言中,
如果将对向量和矩阵元素的操作向量化,
即以向量和矩阵整体来执行计算,
就可以利用语言内建的向量化计算获得与编译代码相近的执行效率。

Julia语言依靠其LLVM动态编译功能,
对向量和矩阵元素循环时不损失效率,
用显式循环处理向量、矩阵与向量化做法效率相近,
有时显式循环效率更高。
但是,向量化计算的程序代码更简洁。

Julia中的函数,
包括自定义函数,
如果可以对单个标量执行,
将函数名加后缀句点后,
就可以变成向量化版本,
对向量和矩阵执行。
这称为广播
运算也是如此,运算符前面加点后就可以将标量运算应用到元素之间的运算。

3-element Array{Float64,1}:
 1.0
 1.4142135623730951
 1.7320508075688772

这种向量化对于多个自变量的函数也成立。
通过编译优化,
可以达到专用的以向量作为输入的函数的效率,
如果同一个语句中有多个加点运算,
编译时能够将其尽可能地合并到同一循环中。

元组(Tuple)

概念和生成

与向量类似的一种数据类型称为元组(tuple)。

(1, 2, 3)
## (1, 2, 3)
(1, "John", 5.1)
## (1, "John", 5.1)

元组的元素不要求属于同一类型。

单个元素的元组要有逗号分隔符,如(1,)是单个元素的元组,
(1)不是元组。

元组表面上类似于一维数组,
但是元组属于不可修改(immutable)类型,
不能修改其中的元素。
其存储也与数组不同。

可以用tuple()函数生成元组。
可以用类似一维数组的方法对元组取子集,
x[1], x[2:3]等。
如:

x = ('a', 'b', 'c', 'd')
typeof(x)
## NTuple{4,Char}

这个类型的意思是由4个字符组成的元组。

访问片段

x[1]
## 'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
x[2:3]
## ('b', 'c')

不允许修改元组中的元素:

结果出错:

MethodError: no method matching setindex!(::NTuple{4,Char}, ::Char, ::Int64)
.........

比较

元组可以看作一个整体参加比较,
比较方法类似于字典序,如:

(1, 3, 5) < (1, 3, 6)
## true

赋值等号左边的元组

可以利用元组写法对变量同时赋值,

a, b = 13, 17
println("a=", a, " b=", b)
## a=13 b=17

这种赋值可以用来交换两个变量的值,如:

a, b = b, a
println("a=", a, " b=", b)
## a=17 b=13

元组赋值的右侧也可以是数组等其它序列类型,如

a, b = [19, 23]
println("a=", a, " b=", b)
## a=19 b=23

用元组在函数中返回多个值

自定义函数可以返回元组,
从而返回多个值,见下面的自定义函数章节。
内置的有些函数就利用了这种特性,
比如,divrem(x, y)返回除法的商和余数:

元组转换为一维数组

collect()将元组转换为一维数组,如:

3-element Array{Int64,1}:
 1
 3
 5

两个向量成对使用

xy是两个等长的一维数组,
zip(x, y)是一个迭代器(可以用在for循环中),
每次迭代返回两个数组的一对对应元素。
可以用collect()将迭代器转换成二元组的数组:

x = ["a", "b", "c"]
y = [3, 1, 2]
collect(zip(x, y))
3-element Vector{Tuple{String, Int64}}:
 ("a", 3)
 ("b", 1)
 ("c", 2)

有名元组

元组可以为元素命名,
这使得其在一定程度上类似于字典。
但是,字典是可变类型(可以修改其中的元素),
元组是不可变类型。

tn1 = (; name="John", age=32)
tn1[:name]
## "John"
tn1[1]
## "John"

定义时用了左括号后面加一个分号的格式。
这是推荐的写法,
当有多个元素时可以省略分号,
但有分号使得定义有名元组的意图更明显。

有名元组经常用来给函数的关键字参数赋值,
而这样的关键字参数的值又是类似关键字参数的,
即内容可有可无,可多可少。

要注意有名元组用变量名访问时用的是符号(Symbol),
即不写成字符串的变量名前面有冒号。
也可以用加点格式访问:

字典

生成字典

Julia提供了一种Dict数据类型,
是映射的集合,
每个元素是从一个“键”(key)到另一个“值”(value)的映射,
元素之间没有固定次序。如

d = Dict("name" => "Li Ming", "age" => 18)
Dict{String,Any} with 2 entries:
  "name" => "Li Ming"
  "age"  => 18

Dict()生成空的字典。

也可以用二元组的数组作为初值定义字典,如

d2orig = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
d2 = Dict(d2orig)
Dict{Char,Int64} with 4 entries:
  'a' => 1
  'c' => 3
  'd' => 4
  'b' => 2

当键和值分别保存在两个等长的数组中的时候,
可以用zip()函数将这两个数组合并为二元组的数组,
从而产生字典,如:

x = ['a', 'b', 'c', 'd']
y = [1,2,3,4]
d2 = Dict(zip(x, y))
Dict{Char, Int64} with 4 entries:
  'a' => 1
  'c' => 3
  'd' => 4
  'b' => 2

length()求长度,如:

字典的键可以用字符串、整数值、浮点数值、元组这样的不可变类型(immutable),
不能取数组这样的可变类型(mutable)。
最常用的是字符串。

上面生成字典的方法是自动判断键和值的数据类型,
为保险起见,
最好在生成字典时指定键和值的数据类型,
格式为Dict{S, T}(...)S为键的类型,T为值的类型。如:

Dict{String, Int64}("apple" => 1, "pear" => 2, "orange" => 3)
Dict{String,Int64} with 3 entries:
  "pear"   => 2
  "orange" => 3
  "apple"  => 1

对(Pair)

事实上,
"apple" => 1这样的写法也是Julia的一种数据类型,
称为“对”(Pair)。
如:

x = "apple" => 1
typeof(x)
## Pair{String, Int64}

first(x)取出对的第一项,
last(x)取出对的第二项。如:

(first(x), last(x))
## ("apple", 1)

可以用collect(dict)将字典转换成键、值二元组的一维数组,如:

4-element Vector{Pair{Char, Int64}}:
 'a' => 1
 'c' => 3
 'd' => 4
 'b' => 2

访问元素

访问单个元素如

d = Dict("name" => "Li Ming", "age" => 18)
d["age"]
## 18

这种功能类似于R语言中用元素名作为下标,
但R中还可以用序号访问元素,
而字典中的元素没有次序,不能用序号访问。

读取字典中单个键的对应值也可以用get(d, key, default)的格式,
其中default是元素不存在时的返回值。如:

可以用haskey(d, key)检查某个键值是否存在,如:

haskey(d, "gender")
## false

给不存在的键值赋值就可以增加一对映射,如

d["gender"] = "Male";
@show d;
## d = Dict{String, Any}("name" => "Li Ming", 
##     "gender" => "Male", "age" => 18)

delete!(d, key)可以删除指定的键值对。

get!(d, key, default)可以在指定键值不存在时用default值填入该键值,
已存在时就不做修改,
两种情况下都返回新填入或原有的键对应的值。

pop!(d, key)返回key对应的值并从字典中删除该键值对。

merge(dict1, dict2)合并两个字典,
有共同键时取后一个字典的值。

遍历字典

可以用keys()函数遍历各个键值,次序不确定:

d2 = Dict('a' => 1, 'b' => 2, 'c' => 3, 'd' => 4)
for k in keys(d2)
    println(k, " => ", d2[k])
end
a => 1
c => 3
d => 4
b => 2

除了可以用haskey(dict, key)判断某个键是否存在,
也可以用key in keys(dict)判断,如:

在字典中查找某个键是使用散列表(hash table)技术,
所以查找时间不会随元素个数增长而线性增长,
可以比较方便地存储需要快速查找的键值对。

字典存储并没有固定的存储次序。
为了在遍历时按键值的次序,
需要使用如下的效率较低的方法:

for k in sort(collect(keys(d2)))
    println(k, " => ", d2[k])
end
a => 1
b => 2
c => 3
d => 4

对字典排序遍历的另一方法是将字典转换成键值对的数组,
然后用sort排序,
再遍历,如:

d2p = collect(d2)
sort!(d2p, by=first)
for (k, v) in d2p
    println(k, " ==> ", v)
end
a ==> 1
b ==> 2
c ==> 3
d ==> 4

可以用values()遍历各个值,但也没有固定次序。比如

4-element Array{Int64,1}:
 1
 3
 4
 2

可以直接用二元组对字典遍历,如

for (k,v) in d2
    println(k, " => ", v)
end
a => 1
c => 3
d => 4
b => 2

可以将字典转换成键值对的列表,
如:

[(k, v) for (k, v) in d2]
4-element Vector{Tuple{Char, Int64}}:
 ('a', 1)
 ('c', 3)
 ('d', 4)
 ('b', 2)

用生成器生成字典

可以用Dict(x => f(x) for x in collection)的方法生成字典,
如:

Dict(x => x*x for x in [2,3,5,7])
Dict{Int64,Int64} with 4 entries:
  7 => 49
  2 => 4
  3 => 9
  5 => 25

字典应用:频数表

在基本的描述统计中,
经常需要对某个离散取值的变量计算其频数表,
即每个不同值出现的次数。
如果不利用字典类型,
可以先找到所有的不同值,
将每个值与一个序号对应,
然后建立一个一维数组计数,
每个数组元素与一个变量值对应。

利用字典,
我们不需要预先找到所有不同值,而是直接用字典计数,
每个键值是一个不同的变量值,
每个值是一个计数值。
对字典可以用get()函数提取某个键值对应的值,
并在键值不存在时返回指定的缺省值。
如:

sex = ["F", "M", "M", "F", "M"]
freqs = Dict()
for xi in sex
    freqs[xi] = get(freqs, xi, 0) + 1
end
freqs
Dict{Any, Any} with 2 entries:
  "M" => 3
  "F" => 2

将上述的频数计算功能编写成一个函数如下:

function freqd(x)
    y = Dict()
    for xi in x
        y[xi] = get(y, xi, 0) + 1
    end
    return y
end
freqd(sex)
Dict{Any, Any} with 2 entries:
  "M" => 3
  "F" => 2

StatsBase包的countmap函数实现了上述的freqd的功能。
如:

using StatsBase
StatsBase.countmap(sex)
Dict{String, Int64} with 2 entries:
  "M" => 3
  "F" => 2

可以看出StatsBase的版本更为合理,
其返回的字典的数据类型更加精确。

有时返回字典类型不方便使用,
可以返回取值和频数分别的列表:

function freq(x)
    y = StatsBase.countmap(x)
    return keys(y), values(y)
end
freq(sex)
## (["M", "F"], [3, 2])
d3 = freq("disillusionment")
d3
## (['n', 'd', 'i', 's', 'l', 'u', 'o', 'm', 'e', 't'], 
##  [2, 1, 3, 2, 2, 1, 1, 1, 1, 1])

因为字典的键必须是不可变类型,
所以freq()中数组x的元素必须是不可变类型。

集合类型

Julia中Set是集合类型。
集合是可变类型,
没有重复元素,
元素没有次序。

Set()生成一个集合,如:

Set{Int64} with 3 elements:
  2
  3
  1
Set{Int64} with 3 elements:
  2
  3
  1
Set(['a', 'b', 'c', 'b'])
Set{Char} with 3 elements:
  'a'
  'c'
  'b'
Set{Char} with 3 elements:
  'k'
  'e'
  'p'

Set()输入一个序列(字符串也是序列),
将序列的元素变成集合元素。
注意Set([1,2,3])正确而Set(1,2,3)错误。

因为字符串也是序列,
所以,
要生成只有一个字符串的集合,
也需要将其作为字符串的数组输入,如:

Set{String} with 1 element:
  "keep"

支持集合的常见运算:

  • union(A, B)A ∪ B:并集。输入为\cup<TAB>
  • intersect(A, B)A ∩ B: 交集。输入为\cap<TAB>
  • setdiff(A, B)A \ B:差集。
  • symdiff(A, B):对称差集,即 (A∖B)∪(B∖A)

  • issetequal(A, B): 集合相等。
  • issubset(A, B)A ⊆ B:子集,输入为\subseteq<TAB>
    (\nsubseteq+TAB)表示非子集。
  • (\supseteq<TAB>):超集。(\nsupseteq<TAB>)表示非超集。
  • 属于关系用in, (\in+TAB),(\ni+TAB),(\notin+TAB), (\nni+TAB)表示。

例如,
判断某个单词的字母都在另一个单词的字母中:

Set("cat") ⊆ Set("atomic")
## true

判断某个单词中有没有重复字母:

length(Set("keep")) < length("keep")
## true

push!(x, a)将元素a加入到集合x中。

对数组x, unique(x)返回由x的不同元素组成的数组。

自定义复合数据类型

类似于其它编程语言中的struct, class,
Julia可以用mutable struct或者struct定义自己的复合数据类型。
例如,
为了表示平面上的一个矩形,
我们需要一个左下角坐标和长度、高度,
就可以定义如下的数据类型:

mutable struct Rectangle
    xll::Real 
    yll::Real 
    width::Real
    height::Real 
end

这定义了一个新的数据类型Rectangle
命名的惯例是使用大写字母开头。
其中的xll, yll, width, height称为这个复合数据结构的“属性”。

生成这个类型的变量:

rect1 = Rectangle(0, 0, 2, 1)

这表示左下角坐标为

(0,0),宽度为2,高度为1的一个矩形。
变量名.属性名的格式访问其中的属性,如:

rect1 = Rectangle(0, 0, 2, 1)
rect1.width
## 2

可以针对这样的自定义类型定义相应的运算和函数,
比如,
平移运算的函数:

function move(rect::Rectangle, offset) 
    Rectangle(rect.xll + offset[1], 
        rect.yll + offset[2],
        rect.width, rect.height)
end

测试:

rect2 = move(rect1, (20, 10))
## Rectangle(20, 10, 2, 1)

struct定义的复合数据,
其中的属性不允许修改。
mutable struct定义的复合数据,
其中的属性是可以修改的。