整数与浮点数

整数

Julia 支持固定长度的整数类型,包括有符号和无符号的版本,例如 Int8Int32UInt32 等。对于十进制整数字面量,在64位系统中默认使用 Int64 类型整数。可以使用 typeof() 查看字面量的类型(Python 对应的函数为 type()

1
typeof(1) # Int64

类型也是可以作为参数进行运算的,例如使用 typemin()typemax() 直接查看类型的最大最小值

1
2
3
typemin(Int32) # -2147483648
typemin(Int64) # -9223372036854775808
typemax(Int64) # 9223372036854775807

注:

  • Julia 支持使用 _ 作为数字的分隔符,可以提高可读性,例如10_000
  • 由于整数的位数是固定的,在运算中自然也存在溢出问题,与其它语言中的处理类似,不再赘述。
  • Python 的整数是无限精度的,Julia 也提供了 BigInt 类型以支持无限精度。

对于非十进制的整数字面量,Julia 采用了不同的处理:默认将其视作无类型整数,并自动选择合适的位数,例如

1
2
typeof(0x11)        # UInt8
typeof(0x1111111) # UInt32

除此之外,对于非十进制的字面量还有很多细节处理的差异,谨慎使用。

布尔值

提供布尔类型的 truefalse

在底层实现时,Julia 采用 8 位整数存储的 0 和 1 分别代表 truefalse。相应的类型转换为

1
2
Bool(1) # true
Bool(0) # false

需要注意的是,Julia 不会把非零整数全部转换为 true,下面的语句报错

1
Bool(2) # error

在类型系统中,Bool 属于 Integer 的子类型

1
2
julia> Bool <: Integer
true

浮点数

Julia 支持基本的浮点数类型,包括默认使用的双精度浮点数 Float64,单精度浮点数 Float32 和半精度浮点数 Float16

有一点奇怪的是,Julia 在科学记数法的表示中,使用 e 会对应 Float64 的字面量,使用 f 则会对于 Float32 的字面量。

1
2
typeof(1.1e-3) # Float64
typeof(1.1f-3) # Float32

可以使用类型名将整数转换为浮点数,例如

1
2
3
Float64(1) # 1.0

typeof(Float64(1)) # Float64

有意思的是,Julia 给不同的浮点数类型分别设置了 naninf

  • Float64:NaNInf
  • Float32:NaN32Inf32
  • Float16:NaN16Inf16

关于这些特殊值的运算都符合 IEEE 标准,因此不同语言之间并没有什么区别。

Julia 的 eps 和 MATLAB 类似,作为一个函数,可以传入一个浮点数或者浮点数类型:

  • 如果传入的是浮点数,那么返回的就是它附近的浮点数间隙;
  • 如果传入的是浮点数类型,返回的是 1.0 附近的浮点数间隙;
  • 缺省时相当于 eps(Float64)
1
2
3
4
5
6
eps(1000.0) # 1.1368683772161603e-13
eps(1.0) # 2.220446049250313e-16

eps(Float32) # 1.1920929f-7
eps(Float64) # 2.220446049250313e-16
eps() # 2.220446049250313e-16

Julia 提供了 zeroone 函数来创建指定类型的 0 和 1,可以传入类型或者一个数,在某些情况下可以避免不必要的类型转换。 例如

1
2
3
4
zero(1.0)   # 0.0   # Float64
zero(Int) # 0 # Int64

one(Int32) # 1 # Int32

在整数和浮点数混合运算时,Julia 会自动转换为浮点数进行处理,并没有 MATLAB 那样的奇葩语法。

1
1 + 0.2 # 1.2

复数与有理数

复数

Julia 支持复数,使用全局常量 im 表示虚数单位,例如

1
1 + 2im

有意思的是

1
2
typeof(1+im) # Complex{Int64}
typeof(im) # Complex{Bool}

这表明复数并不是简单的一个类型,实际类型与复数的实部虚部所属的类型有关。

在数学中,实数是复数的一个子类,但是在编程实现中很难如此实现,通常实数(包括浮点数和整数)是单独的一套类型体系,复数则是一个几乎与此无关的类型。

涉及到 im 的字面量需要特别注意运算优先级的差异,例如

1
2
3/4*im # 0.0 + 0.75im
3/4im # 0.0 - 0.75im

更建议的做法是使用 complex 函数来创建复数,例如

1
complex(1,2) # 1 + 2im

注意:Julia 严格区分字面量是否是复数类型,例如 -1-1+0im 是完全不同的

1
2
-1 == -1+0im # true
-1 === -1+0im # false

在使用 sqrt() 时的差异非常明显

1
2
sqrt(-1) # error
sqrt(-1+0im) # 0.0 + 1.0im

有理数

Julia 支持有理数类型,可以通过 // 运算符创建,例如

1
2
2//3
typeof(2//3) # Rational{Int64}

注:

  • Julia 会自动对分数进行化简,并且保证分母非负。
  • Julia 允许分母为0,例如 5//0,但是不允许分子分母同时为 0。
  • Julia 不支持涉及浮点数的 // 运算

字符与字符串

与 Python 不同,Julia 使用单引号 ' 表示字符,使用双引号 " 来表示字符串,两者是不同的类型,不可以混用。例如

1
2
typeof('a')  # Char
typeof("a") # String

字符的底层当然是基于 ASCII 存储的,因此也支持与数值的运算,例如

1
'a' + 1     # 'b'

Julia 支持多行字符串和 raw 标记的原始字符串(禁用 \ 转义,适合用于 Windows 路径),例如

1
2
3
4
5
6
text = """
这是
多行字符串
"""

rawstr = raw"C:\Users\Alice"

Julia 有如下两种方式拼接字符串,注意使用 * 而不是 + 进行拼接!

1
2
3
"Hello, " * "world!"         # "Hello, world!"

join(["a", "b", "c"], ", ") # "a, b, c"

使用 ^ 表示字符串的重复

1
"abc"^4 # "abcabcabcabc"

与 Python 的 f-string 类似,在字符串中可以使用 $ 插入变量或表达式,有歧义时可以加括号,例如

1
2
3
4
5
name = "Alice"
greeting = "Hello, $(name)!" # "Hello, Alice!"

age = 25
info = "Age next year: $(age + 1)" # "Age next year: 26"

Julia 的字符串支持基于字典序的大小比较。

Julia 甚至为版本号提供了一个专门的字符串字面量:版本号字面量,使用 v 作为前缀,例如 VERSION 是一个特殊的常数,代表 Julia 当前版本,可以用下面的语句进行特定版本的处理

1
2
3
if v"0.2" <= VERSION < v"0.3-"
# 针对 0.2 系列版本
end

有很多关于字符串的操作,包括子串的搜索和提取,正则匹配等,但是目前不太需要,因此略去。

运算符

基本运算符

基本运算符和其它语言都差不多,只有几个值得注意的:

  • a*b:乘法
  • a/b:除法
  • a\b:反向除法,a\b 等价于 b/a
  • a^b:幂运算
  • a%b:取余
  • a ÷ b:整除
  • ==:相等,这和 isequal 函数不太一样
  • !=: 不等
  • ===:严格比较二进制数据相等

注:

  • 涉及到 NaNInf 的等号和不等号判断可能违反直觉,例如 NaN != NaN,但是这也是遵循浮点数标准的。
  • 部分运算符对应的 Unicode(LaTeX) 字符也可以正常使用,例如不等号也支持 \leq
  • =====isequal 的语义存在细微区别,尤其在某些边缘例子中,一般来说,=== 最严格,isequal 最弱。

与 Python 一样,Julia 支持链式比较,例如

1
1 < 2 <= 2 < 3 == 3 > 2 >= 1 == 1 < 3 != 5 # true

Julia 支持 += 等复合赋值运算符,例如 a += b 等价于 a = a + b,这个过程中可能改变 a 的类型,例如

1
2
3
a=0; typeof(a) # Int64

a += 0.1; typeof(a) # Float64

和 Python 一样,这类运算符的优先级很低。

Numpy 的 += 等运算符是就地更新的,而 Julia 并不存在这样的区别,a += b 始终等价于 a = a + b

! 是逻辑非,&& 是逻辑与,|| 是逻辑或,并且 &&|| 都是短路运算,例如

1
2
3
true && false # false
true || false # true
!true # false

~&| 是按位运算符,例如

1
2
3
~0 # -1
123 & 234 # 106
1 | 2 | 4 # 7

点运算符

Julia 为每一个二元运算符提供了点运算符的变体,例如

1
[1, 2, 3] ^ 2 # error

但是加上 . 就可以正常执行

1
[1, 2, 3] .^ 2 # [1, 4, 9]

. 的含义为对逐个元素进行运算。

实际上,a .^ b 会被处理为 (^).(a,b),首先对两个输入进行广播操作,使得不同的尺寸相匹配,然后进行逐个元素运算。

除了二元运算符,函数调用也可以变成点运算版本,例如 f.(A)

在一个复杂的表达式中,. 运算会自动融合,例如

1
2 .* A.^2 .+ sin.(A)

这里涉及到的点运算会合并到一起,实际只会执行一次对 A 的循环遍历。

如果需要对一个复杂表达式的各个运算都改成点运算版本,也可以直接使用 @. 这个宏,例如

1
@. 2A^2 + sin(A))

在函数的复合调用中,点运算也会自动融合,例如

1
f.(g.(A))

复合赋值运算符也支持点运算的版本,例如

1
A .+= B

将点运算符用于数值字面量可能会导致歧义。例如 1.+x 既可以表示 1. + x,也可以表示 1 .+ x? 遇到这种存在歧义的情况时,Julia 会直接报错,可以用空格消除歧义。

1
2
3
4
5
x = [1,2,3]

1.+x # error

1 .+ x # ok

补充

类型转换

可以直接使用 T(x)x 转换为类型 T,例如

1
Int32(10)

对于基本数据类型,这通常是调用 convert() 函数实现的,例如

1
convert(Int32, 10)

类型提升

在不同类型的数据进行混合数值运算时,通常会将其提升为统一的数据类型,例如

1
1 + 0.3 # 1.3

可以用 promote() 函数探究类型提升的细节,例如

1
promote(1,0.3) # (1.0, 0.3)

对于自定义的类型,也支持给 Julia 提供对应的类型提升规则,例如

1
promote_rule(::Type{Float64}, ::Type{Float32}) = Float64

数值字面量系数

这是一个非常糟糕的,为了所谓的公式简洁优雅而提供的语法糖,实际上给代码可读性和语法解析都带来了额外的问题。

Julia 允许变量直接跟在一个数值字面量后,暗指乘法,例如

1
2
3
x = 3
2x^2 + 4x + 5 # 21
6.0x^3-x # 46.0

数值字面量系数的优先级跟一元运算符相同,比如取相反数,例如:

  • 2^3x 会被解析成 2^(3x)
  • 2x^3 会被解析成 2*(x^3)

括号表达式在某些情况下可以被用作变量的系数,暗指表达式与变量相乘,例如

1
2
3
x = 3
2(x+1) # 8
(x-1)x # 6

但是这玩意其实很鸡肋,因为下面的表达式都是语法错误:

1
2
3
x = 3
x(x-1) # error
(x-1)(x+1) # error

而且这里还可能和其它语法产生冲突,例如函数调用,十六进制的前缀0x,用于浮点数表示的fe,以及虚数单位im等。 总的来说,字面量系数这种奇葩语法糖还是了解即可,实践中不要使用。