Julia中没有class,也没有子类型的继承关系,所有具体类型都是最终的,并且只有抽象类型可以作为其超类型。Julia中的继承是继承行为,而不是继承结构。
声明某个变量的类型,也可以用来断言变量类型是否正确
(2+4)::Float64
>> ERROR: ...
(2+4)::Int64
6
类型声明常用的两个地方在函数中的参数类型和返回类型
function f9(x::Int64)::Float64
x/10
end
f9(10)
>>1.0
抽象类型不能被实例化,我们前面讲到的Int64/Float64等都是抽象类型的具体类型,抽象类型不能直接使用,类似于C++中的抽象类。
抽象类型的定义方法如下:
Julia abstract type «name» end abstract type «name» <: «supertype» end
第二行中的<:
表示新声明的抽象类型是后面类型的子类型。
如果没有给出父类型,则默认为Any。
抽象类型有:
abstract type Number end
abstract type Real <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer <: Real end
abstract type Signed <: Integer end
abstract type Unsigned <: Integer end
Integer <: Number
>>true
Integer <: AbstractFloat
>>false
原始类型是具体类型,其数据是由简单的位组成。即我们前来讲到的Float64/Int64/Uint64/Uint32等。是可以实例化为对象的。 声明原始类型的语法为:
primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end
而标准的原始类型都是在语言本身中定义的:
primitive type Float16 <: AbstractFloat 16 end
primitive type Float32 <: AbstractFloat 32 end
primitive type Float64 <: AbstractFloat 64 end
primitive type Bool <: Integer 8 end
primitive type Char <: AbstractChar 32 end
primitive type Int8 <: Signed 8 end
primitive type UInt8 <: Unsigned 8 end
原始类型的应用
function f10(x::Int64)::Int32
x + 10
end
a = f10(10)
我们前面说到抽象类型不能被实例化,但如果我们把上面的Int64换成抽象类型Real,发现也是可以正确被赋值的
function f11(x::Real)::Real
x + 10
end
b = f11(10)
这又是为什么呢?我们可以用typeof()
函数查看变量的类型
typeof(a)
>>Int32
typeof(b)
>>Int64
即在使用抽象类型时,Julia会针对每个调用它的参数的具体类型重新编译。 有两点需要说明:
这个我们在前面讲函数时其实已经提到过
function foo(a, b)
x::Int8 = a
y::Int8 = b
x + y
end
该函数中把Int64类型转成了Int8后再进行计算。也可以像C++中的强制类型转换的用法一样
Int8(10)
convert(Int8, 10)
但这种强转仅限于数字之间,而且不能越界
Int8(1000)
>>error
Int8("10")
>>error
convert(Int8, "10")
>>error
如果我们就是想把字符串转成数字的话,可以用parse
函数
parse(Int8, "10")
我们也可以为convert()
函数增加一个方法
Base.convert(::Type{Int8}, x::String) = parse(Int8, x)
convert(Int8, "10")
先看一个简单的例子
1 + 2.0
>>3.0
这个例子中,就是把Int64类型的1转换成了Float64,然后进行加法操作
再来看下面的例子
a = (1, 2.0, Int8(3))
>>(1, 2.0, 3)
b = promote(1, 2.0, Int8(3))
>>(1.0, 2.0, 3.0)
使用promote后,Tuple中的所有元素都提升到了Float64类型
+(1, 2.0)
>>3.0
a = (1, 2.0)
+(a...)
>>3.0
其实+(1,2.0)
就相当于+(promote(1, 2.0)...)
,可以运行@edit +(1+2.0)
查看Julia的实现方式。
即自定义类型,关键字是struct
,Julia中没有class关键字,都用struct代替
struct Foo
x1
x2::Int
x3::Float64
end
foo = Foo("Hello World!", 10, 11.9)
typeof(foo)
>>Foo
foo.x1
>>"Hello World!"
在foo的创建过程中,有两个默认的构造函数会被自动生成,一个可以接受任意参数,如上面的x1,另接受与字段类型完全匹配的参数,如上面的x2,x3。
struct是不可变类型,foo在创建后,内容不可更改。
foo.x2 = 2
>>type Foo is immutable
Stacktrace:
[1] setproperty!(::Foo, ::Symbol, ::Int64) at .\sysimg.jl:19
[2] top-level scope at In[15]:1
mutable struct Foo
x1
x2::Int
x3::Float64
end
foo2 = Foo("Hello World", 10, 12.1)
foo2.x2 = 20
可变的复合类型是建立在堆上的,并具有稳定的内存地址。
我们上面讨论的抽象类型、原始类型和复合类型,都有以下的共性:
typeof(Real)
>>DataType
DataType 可以是抽象的或具体的。它如果是具体的,就具有指定的大小、存储布局和字段名称(可选)。因此,原始类型是具有非零大小的 DataType,但没有字段名称。复合类型是具有字段名称或者为空(大小为零)的 DataType。
每一个具体的值在系统里都是某个 DataType 的实例。
类型共用体是一种特殊的抽象类型,具体用法:
IntOrString = Union{Int,AbstractString}
123::IntOrString
>>123
"abc":IntOrString
>>"abc"
12.4::IntOrString
>>TypeError: in typeassert, expected Union{Int64, AbstractString}, got Float64
f(x::IntOrString) = println(x)
f(12)
f("abc")
f(23.4)
f('a')
Julia类型系统的一个重要特色就是类型可以支持参数化,我们前面讲到的原始类型、抽象类型和复合类型都支持参数化。类似于C++中的template,但Julia是一种动态语言,在使用参数类型方面优势更加明显。
struct Pos{T}
x::T
y::T
end
a = Pos{Int64}(1,2)
b = Pos{Float64}(1.1,2.2)
c = Pos(3,4)
d = Pos(3.1,4.2)
typeof(c)
>>Pos{Int64}
typeof(d)
>>Pos{Float64}
每个实例都是Pos的子类型
Pos{Float64} <: Pos
>>true
Pos{Int64} <: Pos
>>true
但不同的T声明的具体类型之间不能互为子类型
Pos{Float64} <: Pos{Real}
>>false
还有一点需要注意,虽然有FLoat64<:Real
,但没有Pos{Float64}<:Pos{Real}
.
他们的关系如下
image
所以,下面的定义就不能使用
function norm(p::Pos{Real})
sqrt(p.x^2 + p.y^2)
end
p1 = Pos{Real}(1,2)
norm(p1)
>>2.23606797749979
p2 = Pos{Int64}(1,2)
norm(p2)
>>error
但我们可以写成这样
function nrom2(p::Pos{<:Real})
sqrt(p.x^2 + p.y^2)
end
norm(p2)
>>2.23606797749979
复合参数类型也支持多个参数类型
struct Pos1{T1,T2}
x1::T1
x2::T2
end
p1 = Pos1{Int64,Float64}(1,2.2)
p1.x1
>>1
p1.x2
>>2.2
由抽象类型而来,顾名思义,就是给抽象类型加了个参数
abstract type Pointy{T} end
与复合参数类型一样,每个实例都是Pointy的子类型
Pointy{Int64} <: Pointy
>>true
不同的T之间不能互为子类型
Pointy{Float64} <: Pointy{Real}
>>false
由原始类型而来,就是给原始类型加了个参数
primitive type Ptr{T} 64 end
Ptr{Float64} <: Ptr
>>true
元组的定义我们再《变量》一节中讲过了,元组的内容不可更改
t1 = (1, 2.8)
就相当于不可变参数类型的struct
struct Tuple2{T1,T2}
x1::T1
x2::T2
end
t2 = Tuple2{Int64,Float64}(1, 2.8)
那它们之间有什么区别么?
元组类型的最后一个参数可以是特殊类型Vararg
,表示后面可跟任意多个参数
Tup = Tuple{Float64, Vararg{Int64}}
isa((2.2,), Tup)
>>true
isa((2.2, 3), Tup)
>>true
ias((2.2, 3.1), Tup)
>>false
ias((2.2, 3, 4), Tup)
>>true
所谓的单态类型,就是Type{T}
,它是个抽象类型,且唯一的实例就是对象T。
isa(Int64, Type{Int64})
>>true
isa(Real, Type{Int64})
>>flase
isa(Type{Int64}, Type)
>>true
isa(Type{Real}, Type)
>>true
在抽象参数类型中,我们讲到,Pointy
是它所有实例Pointy{T})
等的超类型,但T的类型是不确定的,那这又是如何工作的呢?这就引入了UnionAll类型,Pointy是一种UnionAll类型,是这种类型某些参数的所有值的类型的迭代并集。
UnionAll类型通常使用关键字where
编写
struct Pos{T}
x::T
y::T
end
function f1(p::Pos{T} where T<:Real)
p
end
当有两个类型参数时
struct Pos2{T1,T2}
x::T1
y::T2
end
function f2(p::Pos2{T1,T2} where T1<:Int64 where T2<:Float64)
p
end
where还可以限定T的上下界
function f23(Pos{T} where Int64<:T<:Real)
p
end