Julia类型操作
弱类型机制
在Julia中,类型与具体的值是密切相关的。值是占据内存的对象实体,是计算操作的真正目标,所以必然对应一个确切的类型,而且是可实例化的具体类型。
在类型与值之间,还有另外一个概念:变量。变量在Julia中并不像其他语言那样有着很重要的地位,而只是指代程序中某个元素的名称,仅是在定义时才绑定某个具体的值,只是对值的一次引用,以便后续对该值进行各种操作。而且这种微弱的绑定关系随时可以改变。例如:
julia> a = 10
10
julia> a = 3.2
3.2
julia> a = "this is www.coolcou.com"
"this is www.coolcou.com"
其中,变量a先是用于指代整数10,再用于指代3.2这个浮点数值,最后又变成了字符串。
所以在Julia中,变量本身没有类型的概念,值(对象)才有。如果说一个变量是某类型,实际是指其引用的值属于该类型,这与C++或Java这些强类型语言是完全不同的。在这些强类型语言中,变量需要以声明的方式与某类型建立确切、唯一且恒定的关系,之后这个变量无法再变更类型,只能存取操作该类型的值。
在Julia这种弱类型机制中,变量无须先声明即可随时使用,而且不强制进行类型的限定。在语言内部,编译期的类型检查也不会太严格,一般直到运行时才确定操作目标对象的具体类型。这也是动态语言的基本特点。而且,变量的类型可以像上例那样随时变换(虽然这并不是Julia建议的做法)。
然而,过于松散的类型控制(例如允许数据类型随意变换或整型可与字符值相加),很容易导致开发的程序出现莫名其妙的错误。同时,如果类型延迟到运行期才确定,往往会带来很多效率损耗。所以Julia的编译器不会自动地对类型做隐式转换或提升,除非转换提升存在着明确的定义(语言内置的或用户定义的代码中);而且为了科学计算的高性能,Julia建议开发者在使用变量或值时,要尽可能地给出确切、详尽的类型描述,并能够维持类型的稳定性。
对于变量,如果希望其与值的绑定关系稳定,可以在定义时使用关键字const在定义时将其标识为常量。例如:
julia> const mye = 2.71828182845904523536;
julia> const mypi = 3.14159265358979323846;
其中,mye和mypi均被定义为浮点数常量。
原则上,常量只能在声明时赋值一次。但变量与值之间的赋值实际只是建立了两者的绑定关系,所以const在本质上影响的便是这种微弱的绑定关系。如果一定要对常量重新赋值,也是可以的,例如:
julia> mye = 3.21
WARNING: redefining constant mye
3.21
julia> mye
3.21
其中,强制性地修改了常量mye的值,但在返回新值前会报出“redefining”警告信息,不过仍会执行成功。但需要注意的是,如果试图将普通的同名变量再次定义为常量,会报异常(而不仅仅是个警告),即会执行失败:
类型断言
弱类型在对变量控制方面更为简单、轻松,但在计算操作中,仍需要明确的数据或值的内存结构,即表明了数据或值内部表达方式的类型定义。在开发中,必要时可通过操作符::
对值的类型进行“断言”,以判断其是否为某个类型。例如:
可见断言表达式会在类型相容(匹配)时返回原值,否则上报TypeError
异常。
这种断言操作对抽象类型同样有效:如果值的具体类型是被断言类型的子类型,断言也会成立。例如:
julia> 1::Number
1
julia> a::Real
1
当然,在断言之前我们也可以通过isa()
函数判断某个值是否为给定类型的实例,例如:
julia> a = 1;
julia> isa(1, Int32)
false
julia> isa(a, Int64)
true
这样,在预先知道值不是指定类型的对象时,可提前给予恰当的处置,不会像断言那样让程序报错中断。
同样,该函数也适用于抽象类型,例如:
此时若第一个参数(值)的具体类型是第二个参数(类型)的子类型,isa()
便会取true值,否则会返回false值。
DataType
在Julia中一切皆对象,类型本身也是可以操作的对象。如果我们将某类型作为参数,使用typeof()
查看其类型,会发现:
其中,DataType便是Julia中包括元类型、抽象类型及后文介绍的复合类型等所有类型的“类型”;而且也是Any的类型(换言之,Any是其实例之一)。事实上,DataType本身也是类型的一种,在类型系统中也是Any的子类型之一。
对于类型对象,前述的isa()
函数与断言操作符::
同样适用,例如:
julia> isa(Int, DataType)
true
julia> isa(Any, DataType)
true
julia> isa(DataType, DataType)
true
julia> Int64::DataType
Int64
julia> Real::DataType
Real
julia> DataType::DataType # DataType是DataType自身的类型
DataType
可以说,在Julia的类型系统中,任意的类型均是DataType的实例(对象)。这样的设计机制使得Julia的类型系统构成了所谓“完备的闭集”,在概念与操作上具备充分的统一性。
为此,作为可操作的对象,类型之间也可以使用“是否相等”或“完全相同”运算符,判断它们是否是同样的类型,例如:
julia> Real == Number
false
julia> Int32 === Int32
true
类型别称
类型本身也可以作为右操作数,赋值给某个变量,例如:
julia> MyInt = Integer
Integer
julia> MyInt === Integer
true
显然,其中定义的类型变量本身仍是DataType的实例,也是可用的类型,即:
julia> typeof(MyInt)
DataType
这种类型变量有时候会非常有用,例如,在长期迭代或规模稍大的程序中,为了实现前后的兼容性、适应第三方包或方便系统移植,往往需要对类型进行各种变化,这其中最简单的方式便是为类型另起一个名字。假设需要达到如下的效果:
# 32位系统
julia> UInt
UInt32
# 64位系统
julia> UInt
UInt64
即无论是怎样的系统,都希望UInt都是有效的类型,而且能自动适配系统的位数,可写如下的代码实现:
if Int === Int64 # Int是Julia预先定义的具体类型,会随系统而变化。
const UInt = UInt64
else
const UInt = UInt32
end
此后,UInt便会在不同的系统中自动取对应的具体元类型,且与源类型有着一致的操作,从而可轻松地实现代码移植。
对于一些结构复杂、表述较长的类型,可以通过这种方法声明一个类型别称,以方便使用。可以说,实现过程中涉及直接的类型操作时,这样的机制提供了很大的想象空间,能够开发出更为灵活自如的程序。
继承关系
类型之间除了能够进行“是否相等”或“完全相同”判断,也能够进行“小于等于”或“大于等于”这种比较操作。不过意义不再是数值的大小,而是在类型拓扑图中,两者是否处于同一条路径上,即两者是否存在父子继承关系。
在实践中,可以使用<:
作为操作符判断一个类型是否是另外一个的子类型,例如:
julia> Unsigned <: Integer
true
julia> Signed <: Real
true
julia> AbstractFloat <: Any
true
或者:
julia> Number <: Integer
false
julia> Real <: AbstractFloat
false
显然:
julia> DataType <: Any
true
这确实是一件很有意思的事情:如前所述,Any的类型是DataType,而DataType因为也是一种类型,所以也是Any的子类型。
另外,对于定义的类型别称,会自动获得源类型在类型系统中的位置,进而获得源类型的继承关系,例如:
julia> MyInt >: Int64
true
julia> MyInt <: Number
true
与操作符<:
相对,也可以使用操作符>:
判断一个类型是否是另外一个的父类型,例如:
julia> Unsigned >: Integer
false
julia> Number >: Integer
true
julia> Real >: Real
true
除了结果相反,用法上与<:
是一致的。
如果参与继承关系断言的是同一个类型,会发现:
julia> Integer <: Integer
true
julia> Integer >: Integer
true
julia> Any <: Any
true
也同样是成立的。所以在Julia的类型系统中,类型与其自身是满足继承关系断言的,即与自身有父子关系。
酷客网相关文章:
评论前必须登录!
注册