julia学习笔记(五):类型

类型

  • Julia 类型系统是动态的,但通过允许指出某些变量具有特定类型,获得了静态类型系统的一些优点。
  • 在类型被省略时,Julia 的默认行为是允许变量为任何类型。
  • 添加类型注释主要有三个目的:利用 Julia 强大的多重派发机制、提高代码可读性以及捕获程序错误。

Julia 用类型系统的术语描述是动态(dynamic)、主格(nominative)和参数(parametric)的。泛型可以被参数化,并且类型之间的层次关系可以被显式地声明,而不是隐含地通过兼容的结构。

Julia 类型系统的一个特别显著的特征是具体类型相互之间不能是子类型:所有具体类型都是最终的类型,并且只有抽象类型可以作为其超类型。Julia中的继承是继承行为,而不是继承结构。

类型声明

变量/表达式 :: 类型

  • 左值为变量:变量类型声明
  • 左值为表达式:断言assert
  • 给编译器提供额外的类型信息,这可能帮助程序提升性能

抽象类型

抽象类型不能被实例化

定义

abstract type «name» end
abstract type «name» <: «supertype» end

e.g. abstract type Integer :< Real end

:<意味新抽象类型是后面的子类型。默认为Any

Note.

function myplus(x,y)
    x+y
end

上述的参数声明等价于 x::Any 和 y::Any。当函数被调用时,例如 myplus(2,5),派发器会选择与给定参数相匹配的名称为 myplus 的最具体方法。
假设没有找到比上述方法更具体的方法,Julia 接下来会在内部定义并编译一个名为 myplus 的方法,专门用于基于上面给出的泛型函数的两个 Int 参数,即它定义并编译:

function myplus(x::Int,y::Int)
    x+y
end

最后,调用这个具体的方法。

因此,抽象类型允许程序员编写泛型函数,之后可以通过许多具体类型的组合将其用作默认方法。即使程序员依赖参数为抽象类型的函数,性能也不会有任何损失,因为它会针对每个调用它的参数元组的具体类型重新编译。(但在函数参数是抽象类型的容器的情况下,可能存在性能问题)

原始类型

具体类型,可以实例化为对象的
primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end
Note. 目前支持的大小只能是 8 位的倍数。

e.g. primitive type Float64 <: AbstractFloat 64 end

类型转换

<类型>(变量 / 表达式)
convert(类型, 变量 / 表达式)

Int8(10)
convert(Int8, 10)

这种方法仅限数字之间,且不能越界。

字符串解析

parse(类型, 字符串)

类型提升

隐式

julia> 1 + 2.0
3.0

显式

promote(tuple)
将tuple所有元素提升到它们之间的最大范围。

复合类型

不可变复合类型

即自定义类型,用struct定义,struct是不可变类型,创建后无法更改。

  • 它可以更高效。某些 struct 可以被高效地打包到数组中,并且在某些情况下,编译器可以避免完全分配不可变对象。
  • 不可能违反由类型的构造函数提供的不变性。
  • 使用不可变对象的代码更容易推理。

Note. 不可变对象可以包含可变对象(比如数组)作为字段。那些被包含的对象将保持可变;只是不可变对象本身的字段不能更改为指向不同的对象。

struct Foo
	x1
	x2::Int
	x3::Float64
end

julia> foo = Foo("Hello World", 10, 11.9)
julia> typeof(foo)
Foo

julia> foo.x1
"Hello World!"

两个默认构造:一个可以接受任意参数(如x1),另一个接受与字段类型完全匹配的参数,(如x2, x3)。

fieldnames()可以找到字段名列表。

没有字段的不可变复合类型是单态类型;这种类型只能有一个实例:

julia> struct NoFields
       end

julia> NoFields() === NoFields()
true

可变复合类型

在定义的struct前面加mutable。建立在堆上,有稳定的内存地址。

已声明的类型

前面章节中讨论的三种类型(抽象、原始、复合)实际上都是密切相关的。它们共有相同的关键属性:

  • 它们都是显式声明的。
  • 它们都具有名称。
  • 它们都已经显式声明超类型。
  • 它们可以有参数。

由于这些共有属性,它们在内部表现为相同概念 DataType 的实例,其是任何这些类型的类型。

DataType 可以是抽象的或具体的。它如果是具体的,就具有指定的大小、存储布局和字段名称(可选)。因此,原始类型是具有非零大小的 DataType,但没有字段名称。复合类型是具有字段名称或者为空(大小为零)的 DataType。

每一个具体的值在系统里都是某个 DataType 的实例。

类型共用体

类型共用体是一种特殊的抽象类型,它包含作为对象的任何参数类型的所有实例,使用特殊Union关键字构造:

Julia> IntOrString = Union{Int,AbstractString}
Union{Int64, AbstractString}

julia> 1 :: IntOrString
1

julia> "Hello!" :: IntOrString
"Hello!"

julia> 1.0 :: IntOrString
ERROR: TypeError: in typeassert, expected Union{Int64, AbstractString}, got a value of type Float64

Union 类型的一种特别有用的情况是 Union{T, Nothing}。通过将函数参数或字段声明为 Union{T, Nothing},可以将其设置为类型为 T 的值,或者 nothing 来表示没有值。

参数类型

Julia 类型系统的一个重要和强大的特征是它是参数的:类型可以接受参数,因此类型声明实际上引入了一整套新类型——每一个参数值的可能组合引入一个新类型。

所有已声明的类型(DataType 类型)都可被参数化,在每种情况下都使用一样的语法。

参数复合类型

类型名称后引入,用大括号扩起来:

julia> struct Point{T}
           x::T
           y::T
       end

Point{Float64} 是一个具体类型,该类型等价于通过用 Float64 替换 Point 的定义中的 T 所定义的类型。因此,单独这一个声明实际上声明了无限个类型:Point{Float64},Point{AbstractString},Point{Int64},等等。这些类型中的每一个类型现在都是可用的具体类型。

Point 本身也是一个有效的类型对象,包括所有实例 Point{Float64}、Point{AbstractString} 等作为子类型。

Point 不同 T 值所声明的具体类型之间,不能互相作为子类型。i.e. 即使 Float64 <: Real 也没有 Point{Float64} <: Point{Real}。它们的内存表示也不同:

  • Point{Float64} 的实例可以紧凑而高效地表示为一对 64 位立即数;
  • Point{Real} 的实例必须能够保存任何一对 Real 的实例。由于 Real 实例的对象可以具有任意的大小和结构,Point{Real} 的实例实际上必须表示为一对指向单独分配的 Real 对象的指针。

一种正确的方法来定义一个接受类型的所有参数的方法,Point{T}其中T是一个子类型Real:

function norm(p::Point{<:Real})
    sqrt(p.x^2 + p.y^2)
end

(等效地,另一种定义方法 function norm(p::Point{T} where T<:Real) 或 function norm(p::Point{T}) where T<:Real。)

由于 Point{Float64} 类型等价于在 Point 声明时用 Float64 替换 T 得到的具体类型,它可以相应地作为构造函数使用:

julia> Point{Float64}(1.0, 2.0)
Point{Float64}(1.0, 2.0)

julia> typeof(ans)
Point{Float64}

在许多情况下,提供想要构造的 Point 对象的类型是多余的,因为构造函数调用参数的类型已经隐式地提供了类型信息。

julia> Point(1.0,2.0)
Point{Float64}(1.0, 2.0)

julia> typeof(ans)
Point{Float64}

julia> Point(1,2)
Point{Int64}(1, 2)

julia> typeof(ans)
Point{Int64}

在 Point 的例子中,当且仅当 Point 的两个参数类型相同时,T 的类型才确实是隐含的。如果不是这种情况,构造函数将失败并出现 MethodError:

julia> Point(1,2.5)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)

参数抽象类型

julia> abstract type Pointy{T} end

在此声明中,对于每个类型或整数值 T,Pointy{T} 都是不同的抽象类型。与参数复合类型一样,每个此类型的实例都是 Pointy 的子类型:

julia> Pointy{Int64} <: Pointy
true

julia> Pointy{1} <: Pointy
true

符号 Pointy{<:Real} 可用于表示协变类型的 Julia 类似物,而 Pointy{>:Int} 类似于逆变类型,但从技术上讲,它们都代表了类型的集合。

julia> Pointy{Float64} <: Pointy{<:Real}
true

julia> Pointy{Real} <: Pointy{>:Int}
true

正如之前的普通抽象类型用于在具体类型上创建实用的类型层次结构一样,参数抽象类型在参数复合类型上具有相同的用途。例如,我们可以将 Point{T} 声明为 Pointy{T} 的子类型:

julia> struct Point{T} <: Pointy{T}
           x::T
           y::T
       end

julia> Point{Float64} <: Pointy{Float64}
true

julia> Point{Real} <: Pointy{Real}
true

julia> Point{AbstractString} <: Pointy{AbstractString}
true

元组类型

元组类型类似于参数化的不可变类型,其中每个参数都是一个字段的类型。

struct Tuple2{A,B}
    a::A
    b::B
end

然而,有三个主要差异:

  • 元组类型可以具有任意数量的参数。
  • 元组类型的参数是协变的:Tuple{Int} 是 Tuple{Any} 的子类型。因此,Tuple{Any} 被认为是一种抽象类型,且元组类型只有在它们的参数都是具体类型时才是具体类型。
  • 元组没有字段名称; 字段只能通过索引访问。
julia> Tuple{Int,AbstractString} <: Tuple{Real,Any}
true

julia> Tuple{Int,AbstractString} <: Tuple{Real,Real}
false

julia> Tuple{Int,AbstractString} <: Tuple{Real,}
false

变参元组类型

元组类型的最后一个参数可以是特殊类型 Vararg,它表示任意数量的尾随参数:

julia> mytupletype = Tuple{AbstractString,Vararg{Int}}
Tuple{AbstractString,Vararg{Int64,N} where N}

julia> isa(("1",), mytupletype)
true

julia> isa(("1",1), mytupletype)
true

julia> isa(("1",1,2), mytupletype)
true

julia> isa(("1",1,2,3.0), mytupletype)
false

Vararg{T} 对应于零个或更多的类型为 T 的元素。变参元组类型被用来表示变参方法接受的参数。
类型 Vararg{T,N} 对应于正好 N 个类型为 T 的元素。NTuple{N,T} 是 Tuple{Vararg{T,N}} 的别名,即包含正好 N 个类型为 T 元素的元组类型。

具名元组类型

具名元组是 NamedTuple 类型的实例,该类型有两个参数:一个给出字段名称的符号元组,和一个给出字段类型的元组类型。

julia> typeof((a=1,b="hello"))
NamedTuple{(:a, :b),Tuple{Int64,String}}

参数原始类型

原始类型也可以参数化声明,例如,指针都能表示为原始类型

# 32-bit system:
primitive type Ptr{T} 32 end

# 64-bit system:
primitive type Ptr{T} 64 end

与典型的参数复合类型相比,此声明中略显奇怪的特点是类型参数 T 并未在类型本身的定义里使用——它实际上只是一个抽象的标记,定义了一整族具有相同结构的类型,类型间仅由它们的类型参数来区分。因此,Ptr{Float64} 和 Ptr{Int64} 是不同的类型,就算它们具有相同的表示。当然,所有特定的指针类型都是总类型 Ptr 的子类型:

julia> Ptr{Float64} <: Ptr
true

julia> Ptr{Int64} <: Ptr
true

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 f3(pos{T} where Int64<:T<:Real)
	p
end

类型操作

已经引入了一些对于使用或探索类型特别有用的函数,例如 <: 运算符,它表示其左操作数是否为其右操作数的子类型。

isa 函数测试对象是否具有给定类型并返回 true 或 false:

julia> isa(1, Int)
true

julia> isa(1, AbstractFloat)
false
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值