Julia类型操作

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”警告信息,不过仍会执行成功。但需要注意的是,如果试图将普通的同名变量再次定义为常量,会报异常(而不仅仅是个警告),即会执行失败:
Julia类型操作

类型断言

弱类型在对变量控制方面更为简单、轻松,但在计算操作中,仍需要明确的数据或值的内存结构,即表明了数据或值内部表达方式的类型定义。在开发中,必要时可通过操作符::对值的类型进行“断言”,以判断其是否为某个类型。例如:
Julia类型操作

可见断言表达式会在类型相容(匹配)时返回原值,否则上报TypeError异常。

这种断言操作对抽象类型同样有效:如果值的具体类型是被断言类型的子类型,断言也会成立。例如:

julia> 1::Number
1

julia> a::Real
1

当然,在断言之前我们也可以通过isa()函数判断某个值是否为给定类型的实例,例如:

julia> a = 1;

julia> isa(1, Int32)
false

julia> isa(a, Int64)
true

这样,在预先知道值不是指定类型的对象时,可提前给予恰当的处置,不会像断言那样让程序报错中断。

同样,该函数也适用于抽象类型,例如:
Julia类型操作

此时若第一个参数(值)的具体类型是第二个参数(类型)的子类型,isa()便会取true值,否则会返回false值。

DataType

在Julia中一切皆对象,类型本身也是可以操作的对象。如果我们将某类型作为参数,使用typeof()查看其类型,会发现:
Julia类型操作

其中,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的类型系统中,类型与其自身是满足继承关系断言的,即与自身有父子关系。

酷客网相关文章:

赞(0)

评论 抢沙发

评论前必须登录!