Julia 笔记系列:

唠唠闲话

近期刷 LeetCode 题,在“先写 Python 后 Julia”的 coding 模式下体会了两门语言的差异。本篇介绍 Python 编程习惯在 Julia 中的实现,绝大多数情况下,Julia 的实现方式更灵活且有两个鲜明特性:类型派发函数式

Ps:LeetCode.jl 项目 提供部分 LeetCode 习题的 Julia 解答。对于有代码的问题,可以学习别人写的 Julia 代码风格和技巧,其他问题则可以自行补充和提交,两全其美。


跳转链接:类型转化列表, 元组字典元素添加删除查找包含最大最小值排序反转逻辑判断字符串生成器索引处理向量化IO 编程函数式面向对象,以及类的“继承”

数据和操作

数组部分参考了这篇教程

  1. Python 类型转化用 int, float, str,Julia 规则如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # 整数 -> 浮点数(与 Python 方法相同)
    Float32(1)
    Float64(2)

    # 浮点数 -> 整数
    # 类似 Python 的 int,但是浮点数与整数的差值必须小于精度值
    Int(1.0)
    # 下边报错
    # Int(1.001)
    # 四舍五入
    round(Int, 1.1)
    # 取上整
    ceil(Int, 1.1)
    # 取下整
    floor(Int, 1.1)

    # 整数,浮点数 -> 字符串
    string(1), string(1.)

    # 字符串 -> 整数,浮点数
    parse(Int, "12")
    parse(Float, "1e2")
  2. Python 的 list -> Julia 的 Vector

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 与 Python 规则相同,等价写法是 Int[1, 2, 3]
    l1 = [1, 2, 3]
    # 列表需要指定数据类型
    l2 = Float[1, 2, 3]
    # 空列表必须指明类型
    l3 = Int[]; l4 = Float64[]
    # 取复杂性更高的类型,等价于 mat = Float64[1, 2]
    l5 = [1, 2.]
    # 用 `类型(数据)` 的方式转化数据格式
    l6 = Vector{Float}([1,2,3])
    # collect 类似于 list 命令
    l7 = collect(1:5)
    # 列表拼接
    l8 = vcat(l1, l2)
    • Python 的 range(start, stop+1, step) -> Julia 的切片 start:step:stop,注意规则区别
    • collect 将可迭代对象转化为列表(Vector),类似于 Python 的 list 操作
    • Julia 的列表拼接要用 vcat 或有时可以用 append!,而 + 操作是列表的元素相加,与 Python 的 + 不同
  3. Python 的 tuple -> Julia 的 TupleNTuple

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 直接用圆括号定义
    t1 = (1, )
    # 用类型转化的方式定义
    t2 = Tuple([1,2,3])
    # 取前 3 个构成元组
    t3 = Tuple{Int,Int,Int}(1:10)
    # 上一命令的等价写法
    t4 = NTuple{3,Int}(1:10)
    # 元组拼接,类似于 Python 语法的 (*t1, t2)
    t5 = (t1..., t2...)

    在 Python 中,+ 运算可以拼接元组,但在 Julia 中不能直接这么操作;当然,也可以手写拼接函数

    1
    concatenate(t1::Tuple, t2::Tuple) = (t1, t2...)
  4. Python 的 dict -> Julia 的 Dict

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 空字典,需指定键和键值的类型
    dic = Dict{Char, Int}()
    # 等同于 Dict{Any, Any}() 不建议
    dic = Dict()
    # 创建字典并初始化
    dic = Dict{Int, Int}(1=>2, 2=>3)
    # 判断是否包含键值,不能用 1 in dic
    haskey(dic, 1)
    1 ∈ keys(dic) # 等价写法
    # 删除指定键,并返回键值
    pop!(dic, 1)
  5. 元素添加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    data = [1, 2, 3]
    # 追加一个元素
    push!(data, 3)
    # 追加多个
    push!(data, 4, 5)
    # 同 push!
    append!(data, 1, 2)
    # 用列表追加多个元素
    append!(data, [1,2,3])
    # 用混合方式,追加多个元素
    append!(data, 1:5, [1,2], 3)
    # 追加列表
    res = [[1, 2], [2, 3]]
    append!(res, [[1, 3]])
    # 注意 res 的元素类型为 Vector{Int},追加信息不能直接写 [1, 3]
    # 在列表前边追加
    insert!(data, 1, 2)
    • push!append! 的功能类似 Python 列表的 .append.extend,但也有区别
    • append! 追加的内容可以是元素,或者列表,迭代器等
    • append! 追加元素时,相当于 Python 的 .append
    • append! 追加列表一类时,将把列表展开再加入,相当于 Python 的 .extend
    • 此外 push! 追加内容只能是元素,追加列表时,将把列表作为一个元素加入
  6. 元素删除操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    data = collect(1:10)
    # 删除最后一个元素,返回被删除值
    pop!(data)
    # 删除第一个元素,返回被删除值
    popfirst!(data)
    # 删除指定位置的元素,返回列表本身
    deleteat!(data, 2)
    # 删除多个元素,返回列表本身
    deleteat!(collect(1:10), 1:4)
    # 删除类似名称
    names = ["foo", "bar", "foo", "rex", "zong"]
    deleteat!(names, findall(names, ==("foo")))
    # 清空内容
    empty!(names)
    # 保留指定元素,删除其他
    filter(==(1), 1:5)
    # 删除指定元素,原地修改
    filter!(!=("a"), ["a", "b", "c"])
    • Julia 的等号支持函数式编程,比如 ==(1) 相当于 i->(i==1),注意括号不可省略
    • pop! 不能删除列表的中间元素,而要用 deleteat!,且返回值是列表本身
    • Julia 1.7 中,还增加了 keepat! 函数
    1
    2
    keepat!(collect(1:10), 2:3) # 只保留指定索引部分
    keepat!(collect(1:10), 2) # 只保留第 2 个元素
  7. 定义函数,常见有四种方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 匿名函数,相当于 Python 的 lambda
    f1 = i -> i
    f2 = (i, j) -> i + j
    # 符合数学形式的函数定义
    f3(i, j) = i + j
    # 类似 Python 的 def
    function f4(i, j)
    i + j
    end
    # 支持创建函数的符号
    ==(1) # 相当于 i->(i==1)
    ≥(2) # 相当于 i->(i>=2)
    • 使用 function 定义函数时,末尾数值将默认作为 return 的结果,而不需要显式写 return
    • 此外,Julia 还支持 do 语法创建匿名函数,比如
    1
    2
    3
    open("xxx.txt", "r") do x
    ...
    end

    等价于先将 do 主体写成函数,再传入 open 的第一个参数

    1
    2
    3
    4
    function f(x)
    ...
    end
    open(f, "xxx.txt", "r")
  8. 查找,Python 的 .index -> Julia 的 findfirst 等函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    data = collect(1:5)
    # 第一参为函数
    # 返回计算为真的第一个位置
    findfirst(==(2), data)
    # 找不到时,返回 nothing
    findfirst(==(6), data)
    # 在字符串中匹配字符
    findfirst('a', "abca")
    # 从后往前匹配
    findlast('a', "abca")
    # 匹配字符串,返回匹配到的切片
    findfirst("ab", "abca")
    # 匹配所有结果,返回列表
    findall("ab", "abca")
    • 由于第一参支持输入函数,这些工具比 Python 的 .index 能处理更复杂的问题
    • findall 不支持直接匹配字符 findall('a', "abca"),而要用 findall(==('a'), "abca")
  9. Julia 提供了很多“包含”关系的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 判断元素是否在列表中
    1 in [1, 2, 3]
    1 ∈ [1, 2, 3] # \in + <tab>
    # 判断元素不在列表中
    1 ∉ [1, 2, 3] # \notin + <tab>
    # 判断子集
    [1, 2] ⊆ [1, 2, 3] # \subseteq + <tab>
    # 函数式
    filter(∈(1:2), [1, 2, 3])

    相关表格如下,其中最后两个符号没有定义函数

    函数 快捷键
    \subseteq + <tab>
    \subseteq + <tab> + \not + <tab>
    \supseteq + <tab>
    \supseteq + <tab> + \not + <tab>
    \in + <tab>
    \notin + <tab>
    \ni + <tab>
    \ni + <tab> + \not + <tab>
    \subset + <tab>
    \subset + <tab> + \not + <tab>
  10. 最大最小值

1
2
3
4
5
6
7
8
data = [1, 2, 3]
# 多个元素最大值
max(1, 2, 3)
# 最大值
maximum(data)
# 用 ... 将数据打开,相当于 Python 的 *
max(data...)
min;minimum; # 最小值用法同上

函数式用法:Python 的 max 返回取值最大的数值(定义域),而 Julia 的 maximum 返回最大函数值(值域)

1
2
3
4
## Python 情形
max([1,2,3], key=lambda i: -i) # 返回数据 1
## Julia 情形
maximum(-, [1,2,3]) # 返回 -1

通过论坛发现,Python 类似功能可以用 findmaxargmax 实现,不过需要 Julia 1.7 版本,较低版本的实现方式相对绕一点

1
2
3
4
5
# Julia 1.6 及以下版本
data = [1,2,3]
data[argmax((-).data)]
# Julia 1.7 及以上版本
argmax(-, data)
  1. 排序,反转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
   # 类似 Python 的 reversed,但返回数据而不是生成器
reverse("12")
# 原地修改,并返回本身,类似 Python 的 .reverse()
reverse!([1, 2])
# 排序,类似 Python 的 sorted,但返回数据而不是生成器
sort([1, 2])
# 原地修改的排序,并返回本身,类似 Python 的 .sort()
sort!([1, 2])
# 函数式
# 类似 Python 的 sorted([1, 2, 3], key = lambda i:-i)
sort([1,2,3];by=-)
```

11. 布尔判断:Python 中,判断条件可以用空字符串,空列表,`None` 代表 `False`,但 Julia 需显式指出<span id="bool"></span>
```py
isempty(Int[]) # 判断列表是否空
!isempty([1,2]) # 判断列表是否非空
isnothing(nothing) # 判断是否是 nothing
i === nothing # 等价写法,三等号类似 Python 的 is
isempty("") # 字符串判断同列表
  • Julia 中与或非为 &&, ||, ! 而不能用 and, or, not
  • 算符 ! 支持函数式,比如 !f 等价与 g(...) = !f(...)
    布尔值构成的列表类型为 BitVectorVector{Bool},结合索引可以有妙用,比如
1
2
3
4
5
b = collect(1:8)
a = iseven.(b)
b[a] # [2, 4, 6, 8] | 返回判断为真的数值
reverse!(@view(b[a])) # 将偶数部分取反,原地修改
print(b) # [1, 8, 3, 6, 5, 4, 7, 2] | 修改后的结果
  1. 常见算符,注意与 Python 的区别

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 下整除,输入 \div + tab 键,对应 Python 的 //
    2 ÷ 3
    # 异或 \xor,对应 Python 的 ^
    23
    # 与或非,异或 \xor, 同非 \nor, 与非 \nand
    &, |, ~, ⊻, ⊽, ⊼
    # 乘方,对应 Python 的 **
    2 ^ 3
    # 问号表达式,类似 expr1 if true else expr2
    true ? expr1 : expr2
    # 代表有理数 1/2
    1 // 2
  2. 字符串操作,注意双引号 "" 代表字符串,单引号 '' 代表字符,而在 Python 中二者没有区别

    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
    # 字符串拼接,对应 Python 的 +
    "abc" * "def"
    # 字符串复制,对应 Python 的 *
    "abc" ^ 3
    # Julia 的字符串允许换行号,而 Python 需要三个引号
    "
    多行字符串"
    # 左切,对应 Python 的 .lstrip
    lstrip("---abc---", '-')
    # 右切,默认去除空格
    rstrip("---abc---", '-')
    # 第二参允许用列表代表除去多个,但不能用字符串
    strip("-*-abc-*", ['-', '*'])
    # 使用 $ 在字符串中插入变量
    "1+1=$(1+1)"
    # 字符运算按 ASCII 表和 Unicode 进行
    'a' + 1 == 'b'
    # 字符支持切片操作
    collect('a':'z')
    # 判断是否包含字符
    'a' in "abc"
    # 判断是否包含于字符串,注意不能直接用 in
    occursin("ab", "abc")
    # 函数式用法,筛选 "abcd" 的子串
    filter(occursin("abcd"), ["abc", "def"])
    # 判断是否包含该字符串
    contains("abc", "ab")
    # 函数式用法,筛选包含 "ab" 的字符串
    filter(contains("ab"), ["abc", "def"])

    相关函数还有 split, join, replace 等就不一一演示了,其中 replace 支持正则匹配,比如

    1
    2
    # 用 `r""` 代表正则表达式,如果需要提取结果,替代结果使用 s""
    replace("Dog chase Cat", r"(.*) chase (.*)" => s"\g<2> is chased by \g<1>")

    关于 Python 和 Julia 的正则表达式区别,参考这篇正则表达式 | Python/Julia/Shell 语法详解

  3. 生成器和 for 循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 列表生成器,类似 Python 的语法
    [i for i in 1:9]
    # Julia for 循环中的 in 可以用 = 代替
    [i for i = 1:9 if i < 6]
    # 迭代器
    (i for i in 1:9)
    # 多元列表循环,注意循环变量要用括号包住
    for (i, j) in zip(1:3, 1:5)
    println(i,j) # 打印 3 次
    end
    # 双重 for 循环的简写,注意中间用逗号隔开
    for i in 1:3, j in 1:5
    println(i,j) # 打印 3 * 5 = 15 次
    end
    # for 循环临时创建变量为内部变量
    # 外部查看 i, j 将报错
    print(i, j)

    Julia 的 for 循环中,无论是生成器形式还是一般形式,变量 i,j 与外部不共享;而 Python 生成器中的 i 与外部不共享,for 循环的 i 与外部共享

  4. 索引处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    data = collect(1:10)
    # 获取切片而不创建新对象
    @view data[1:3]
    # 创建二维数组
    mat = fill(1, 3, 4)
    # 用 CartesianIndices 创建索引集
    inds = CartesianIndices(mat)
    # 创建索引
    I, J = CartesianIndex.([(1,1), (1,2)])
    # 判断索引是否越界
    I + J ∈ inds
    # 用 eachindex 遍历数组
    for i in eachindex(mat)
    mat[i]
    end
    • 和 Python 一样,Julia 的切片将创建新对象,但用 @view 可以读取原内存片段
    • @view 获取切片而不创建新对象,这一特性有许多妙用,比如实现快速排序算法时,传值不需要传递索引
    • CartesianIndices 获取索引信息,这种操作可读性更好,且利于一般化处理
    • eachindex 代替切片 for i in 1:length(data) 更直接
    • 值得注意的是,Julia 中的矩阵是列优先存储,也即按 mat[1, 1], ..., mat[n, 1] 的顺序。在遍历 Julia 数组时,因尽量按内存数据进行,以提升运行效率。
  5. Julia 的向量化

    1
    2
    3
    4
    5
    6
    7
    f(i) = i ^ 2
    # 用 . 代表向量化
    f.(1:4) # [1, 4, 9, 16]
    # 与上一命令等同
    map(f, 1:4)
    # 使用宏 @. 声明运算为向量化,效果等同于 f.(1:4) .+ 2
    @. f(1:4) + 2

    Julia 的列表做加减运算等同看成点集的运算

    1
    2
    [1, 2] + [2, 3] # [3, 5]
    [2, 3] - [1, 2] # [1, 1]

    使用向量化时,可以用 Ref 将某些数据看成整体,比如

    1
    2
    3
    nums = [[1,2], [2, 3], [4, 5]]
    # nums .- [1, 2] # 计算报错
    nums .- Ref([1, 2]) # 将 [1, 2] 看成标量
  6. 其他函数

    1
    2
    3
    4
    5
    6
    fill(2, 3, 3) # 生成常数矩阵
    zeros(2, 3) # 生成浮点零矩阵
    ones(Int, 4) # 生成整数向量
    rand(3) # 生成 3 个浮点数
    rand(Int, 5) # 生成 5 个整数,范围 -2^64 -> 2^64 -1
    rand(1:10, 3) # 从 1:10 中随机取 3 个数

IO 编程

内容参考官网教程

基础部分

  1. 读入和输出

    1
    2
    3
    name = readline()
    println(name) # 换行打印
    print(name) # 不换行打印

    Python 版的 print 可如下实现

    1
    2
    3
    4
    5
    6
    function pyprint(args...; SEP::String = " ", END::String="")
    for (i, s) in zip(args, fill(SEP, length(args)))
    print(i, s)
    end
    print(END)
    end

    一些场景中,println 可能会等待循环结束才输出,这时可以用 flush(stdout) 强制输出。

  2. 标准信息流

    1
    2
    3
    4
    5
    stdout # 标准输出信息流
    stderr # 标准错误信息流
    stdin # 标准输入信息流
    ## 三者都是抽象类型 IO 的子类
    @. typeof([stdout, stderr, stdin]) <: IO
  3. 文件读写操作,两种语法

    1
    2
    3
    4
    5
    6
    ## 基本语法
    open(f::Function, args...; kwargs...)
    ## 使用 do 语法
    open(args...; kwargs) do x
    ...
    end

    示例,do 语法定义函数,并放在第一参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ## 使用 do 语法
    open("myfile.txt", "w") do io
    write(io, "Hello world!")
    end;
    ## 等价表述
    function f(io::IO)
    write(io, "Hello world!")
    end
    open(f, "myfile.txt", "w")
  4. mode 常用参数

    模式 描述 关键字定义
    r read none
    w write, create, truncate write = true
    a write, create, append append = true
    r+ read, write read = true, write = true
    w+ read, write, create, truncate truncate = true, read = true
    a+ read, write, create, append append = true, read = true

处理数据流

  1. 除了 write 还有其他几个函数支持 IO 写入

    1
    2
    3
    4
    5
    open("tmp.txt", "w") do io
    print(io, "hello ") # 写入到文件
    show(io, "world!") # 写入到文件
    print("hello world") # 输出到信息流
    end

    深度截图_选择区域_20220228114105

  2. open 第一参的函数省略时,将返回 IO 变量

    1
    open(filename::AbstractString, [mode::AbstractString]; keywords...) -> IOStream

    先新建 IO 数据流,读写操作完毕再保存,比如

    1
    2
    3
    4
    5
    6
    7
    # 以写方式打开
    io = open("tmp.txt", write=true)
    write(io, "hello world") # 返回写入的字符数目
    ;cat tmp.txt # 此时修改未保存
    close(io) # 保存修改
    ;cat tmp.txt # 文件已改动
    rm("tmp.txt") # 删除文件

    深度截图_选择区域_20220228120534

  3. 如果不想创建临时文件,可以使用缓冲区处理 IO 数据流(目前较少用到)

    1
    2
    3
    4
    5
    # 创建缓冲区,默认可读可写,无上界,数据类型为 UInt8
    io = IOBuffer(UInt8[], read=true, write=true)
    write(io, "write into buffer ", b"zone") ## 写入内容
    io.size # 查看缓冲区长度
    String(take!(io)) # 将所有内容取出,并转为字符串形式

文件操作

  1. 几个实用变量
    1
    2
    3
    Base.PROGRAM_FILE # 文件的相对路径
    Base.source_path() # 文件的绝对路径
    Base.source_dir() # 查看路径名
    这些变量编写脚本时有很大用处,比如执行
    1
    cd(Base.source_dir()) # 切换到脚本所在目录
    这一操作可以确保在不同位置执行这一脚本,都能正常运行(只要脚本位置放置准确)

函数式编程

  1. 谈到函数式编程,就不得不提到 Mathematica,我们先用 Julia 实现 MMA 里的一个有趣设定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Base.@kwdef mutable struct MMA
    name::String = "f"
    show::String = "f"
    end
    Base.show(io::IO, f::MMA) = print(io, f.show)
    (f::MMA)(args...)::MMA = MMA(f.name, "$(f.name)($(join(args,", ")))")
    Base.:(+)(f::MMA, g::MMA) = MMA(f.name, "$(f.show) + $(g.show)")
    # 初始化函数 f 和 g
    f, g = MMA(), MMA("g", "g")

    此处定义的结构体 MMA 可以抽象地解释函数作用后,发生的什么事情,比如
    深度截图_选择区域_20220227221625

  2. reduce 类似 MMA 的 Fold

    1
    2
    3
    4
    5
    6
    7
    foldl(=>, 1:4) # 左结合律下,依次调用
    # f(f(f(1, 2), 3), 4)
    foldr(=>, 1:4) # 右结合率
    # f(1, f(2, f(3, 4)))
    reduce(=>, 1:4) # 类似 foldl,但不保证左结合率
    reduce(f, 1:3; init='i') # 带上初值
    # f(f(f(i, 1), 2), 3)

    深度截图_选择区域_20220227192618
    注意 reduce 在满足结合律下才能使用,否则结果可能错误
    深度截图_选择区域_20220227192749

  3. mapreduce(f,g,data) 等同于 reduce(g, f.data)

    1
    2
    3
    mapreduce(f, g, 1:3)
    reduce(g, f.(1:3))
    # g(g(f(1), f(2)), f(3))
  4. filter 过滤数据,类似 MMA 的 Select

    1
    filter(isodd, 1:10)
  5. do 语法,创建匿名函数,并作为调用函数的首参,且支持类型派发

    1
    2
    3
    4
    5
    6
    7
    8
    func(data) do x::Vector
    ...
    end
    # 等同于
    function f(x::Vector)
    ...
    end
    func(f, data)
  6. 其他函数。。。等待补充

面向对象

更新:据说 ObjectOrient.jl 模块提供了更好的面向对象编程体验,但是我还没用过,有兴趣的可以试试

Julia 不支持面向对象编程,但借助类型系统和派发,能实现面向对象能做的绝大多数内容,举个例子

  1. 用结构体定义点集

    1
    2
    3
    4
    5
    ## 定义结构体,并用 @kwdef 定义初值
    Base.@kwdef struct Point
    dims::Int = 1
    data::Tuple = (0, )
    end
  2. Python 初始化 __init__ -> Julia 多重派发,定义两种初始化

    1
    2
    3
    4
    # 输入 Point((1,2,3)) 时
    Point(data::Tuple) = Point(length(data), data)
    # 输入 Point(1,2,3) 时
    Point(args::Int...) = Point(length(args), args)
  3. Python 的显示 __repr__, __str__ -> Base.show

    1
    2
    3
    Base.show(io::IO, p::Point) = print(io, p.data)
    p = Point(1, 2, 3)
    string(p)

    深度截图_选择区域_20220227163936

  4. 加法重载 __add__ -> +

    1
    2
    3
    4
    5
    6
    7
    function Base.:(+)(p1::Point, p2::Point)::Point
    p1.dims != p2.dims && throw("dimension not matched!")
    Point(p1.data .+ p2.data) ## 借助向量化
    end
    p1 = Point(1,2,3)
    p2 = Point(0,-1,2)
    p1 + p2

    深度截图_选择区域_20220227161311

  5. 重载减号和负号 __sub__, __neg__ -> -

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ## 重载减号
    function Base.:(-)(p1::Point, p2::Point)
    p1.dims != p2.dims && throw("dimension not matched!")
    Point(p1.data .- p2.data)
    end
    ## 重载负号
    function Base.:(-)(p::Point)
    Point(0 .- p1.data)
    end
  6. Julia 的 +-/*&|!% 等符号都属于函数,重新定义即可,特殊符号支持通过 LaTeX 输入。符号的“运算位置”不能更改,比如 * 为二元运算符,无法定义为形如 *p 的调用方式,相关讨论见这里。此外 Julia 提供了相关的宏 @infix,但不适用与 1.x 版本。

类的继承

Julia 的具体类型不能被继承,这种设定带来的好处远大于其弊端。但一些情况下,为了增加代码的复用性,可以用宏来实现“继承”,参见 ReusePatterns.jl 以及相关讨论

为了实现“类的继承”,比较 “Julian” 的方式有两种,假设 B 要继承 A:

  1. B 将 A 当成属性,用宏 @forword 把与 A 相关的函数重新定义一遍 B 的版本,函数调用时只是把其中的属性 A 拿出来
  2. 定义“拟抽象”类型,介于抽象类型与具体类型之间,增加一些规则限定。

示例一,回退

  1. 定义结构体 Book

    1
    2
    3
    4
    5
    6
    7
    8
    9
    using ReusePatterns
    ##### Alice's code #####
    struct Book
    title::String
    author::String
    end
    Base.show(io::IO, b::Book) = println(io, "$(b.title) (by $(b.author))")
    Base.print(b::Book) = println("In a hole in the ground there lived a hobbit...")
    author(b::Book) = b.author
  2. 定义结构体 PaperBook,并继承 Book 的方法

    1
    2
    3
    4
    5
    6
    7
    #####  Bob's code  #####
    struct PaperBook
    b::Book
    number_of_pages::Int
    end
    @forward((PaperBook, :b), Book)
    pages(book::PaperBook) = book.number_of_pages
  3. 定义结构体 Edition 继承 PaperBook 的方法

    1
    2
    3
    4
    5
    6
    struct Edition
    b::PaperBook
    year::Int
    end
    @forward((Edition, :b), PaperBook)
    year(book::Edition) = book.year
  4. 初始化结构体 Edition ,并使用已有的方法

    1
    2
    3
    4
    5
    6
    7
    8
    #####  Charlie's code  #####
    book = Edition(PaperBook(Book("The Hobbit", "J.R.R. Tolkien"), 374), 2013)

    print(author(book), ", ", pages(book), " pages, Ed. ", year(book))
    # J.R.R. Tolkien, 374 pages, Ed. 2013

    print(book)
    # In a hole in the ground there lived a hobbit...
  5. 使用宏 @forward((PaperBook, :b), Book) 时,先识别与 .b 相关的函数,接着为函数派发 PaperBook 的版本,新函数调用时直接以 .b 作为参数。

  6. 使用缺陷:

    • 如果涉及的方法数量非常多,或者方法定义分布在多个模块中,应用可能会很麻烦
    • 这种组合不是递归进行的,假设 B 继承 A, C 继承 B ,则 C 在初始化时要把 A 和 B 都先初始化,当继承的对象很多时,这将会非常不方便
    • 每个组合层引入了少量开销,从而导致性能损失

示例二,拟抽象类型

更自然的方式是使用宏 @quasiabstract ,暂略。