前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Julia(类型系统)

Julia(类型系统)

作者头像
云深无际
发布2021-04-14 13:35:39
5.4K0
发布2021-04-14 13:35:39
举报
文章被收录于专栏:云深之无迹云深之无迹

传统上,类型系统分为两个截然不同的阵营:静态类型系统和动态类型系统,在静态类型系统中,每个程序表达式必须在执行程序之前具有可计算的类型;在动态类型系统中,直到运行时对类型的任何了解,直到实际值该程序可以操纵。面向对象通过允许编写代码而无需在编译时知道精确的值类型,从而在静态类型的语言中提供了一定的灵活性。编写可以在不同类型上运行的代码的能力称为多态性。经典动态类型语言中的所有代码都是多态的:只有通过显式检查类型或对象在运行时无法支持操作时,才可以限制任何值的类型。

朱莉娅的类型系统是动态的,但是通过表明某些值属于特定类型,可以获得静态类型系统的某些优点。这对于生成有效的代码有很大的帮助,但更重要的是,它允许对函数参数类型的方法分派与该语言进行深度集成。在方法中详细探讨了方法分配,但它扎根于此处介绍的类型系统。

省略类型时,Julia的默认行为是允许值是任何类型。因此,无需显式使用类型就可以编写许多有用的Julia程序。但是,当需要其他表现力时,可以很容易地将显式类型注释逐步引入到以前的“无类型”代码中。这样做通常会提高这些系统的性能和健壮性,并且可能有点违反直觉,通常会大大简化它们。

用类型系统的术语描述Julia ,它是:动态的,主格的和参数化的。可以对泛型类型进行参数化,并且显式声明类型之间的层次关系,而不是由兼容结构隐含。朱莉娅类型系统的一个特别与众不同的特征是,具体类型不能互为子类型:所有具体类型都是最终类型,并且只能具有抽象类型作为其超类型。虽然这乍看起来似乎过分地限制了它,但它带来了许多有益的结果,但缺点却很少。事实证明,能够继承行为比能够继承结构更为重要,并且继承两者都会给传统的面向对象语言带来很大的困难。朱莉娅类型系统的其他高级方面应在前面提到:

  • 对象值和非对象值之间没有划分:Julia中的所有值都是真正的对象,其类型属于单个完全连接的类型图,其所有节点均属于类型。
  • “编译时类型”没有有意义的概念:值的唯一类型是程序运行时的实际类型。在面向对象的语言中,这被称为“运行时类型”,其中静态编译与多态性的结合使这种区别很明显。
  • 只有值而不是变量才具有类型-变量只是绑定到值的名称。
  • 抽象类型和具体类型均可通过其他类型进行参数化。它们也可以通过符号,通过其isbits()返回true 的任何类型的值(本质上是像数字和布尔值之类的东西,如C类型或没有指针指向其他对象的结构存储)的参数化,也可以由其元组参数化。当不需要引用或限制类型参数时,可以将其省略。

朱莉娅的字体系统被设计为功能强大且富有表现力,但清晰,直观且不引人注目。许多Julia程序员可能永远都不会觉得需要编写显式使用类型的代码。但是,使用已声明的类型,某些类型的编程将变得更加清晰,简单,快速且健壮。

类型声明

::操作可用于连接类型注释表达式和变量的程序。这样做有两个主要原因:

  1. 作为断言,可以帮助您确认程序是否按预期运行,
  2. 为编译器提供额外的类型信息,然后在某些情况下可以提高性能

::运算符附加到计算值的表达式后,将其读作“是...的实例”。它可以在任何地方断言左表达式的值是右类型的实例。当右侧的类型为具体类型时,左侧的值必须具有该类型作为其实现-请记住,所有具体类型都是最终类型,因此任何实现都不是其他任何类型的子类型。当类型是抽象类型时,就可以由作为抽象类型的子类型的具体类型实现该值。如果类型断言不为真,则抛出异常,否则,返回左侧的值:

代码语言:javascript
复制
julia> (1+2)::AbstractFloat
ERROR: TypeError: typeassert: expected AbstractFloat, got Int64

julia> (1+2)::Int
3

这允许将类型断言附加到任何表达式上。

当附加到赋值左侧的变量或作为local声明的一部分时,::运算符的含义有些不同:它声明变量始终具有指定的类型,例如静态类型的类型声明。语言,例如C。分配给变量的每个值都将使用转换为声明的类型convert()

代码语言:javascript
复制
julia> function foo()
           x::Int8 = 100
           x
       end
foo (generic function with 1 method)

julia> foo()
100

julia> typeof(ans)
Int8

此功能对于避免在变量的分配之一意外更改其类型时可能发生的性能“陷阱”很有用。

此“声明”行为仅在特定情况下发生:

代码语言:javascript
复制
local x::Int8  # in a local declaration
x::Int8 = 10   # as the left-hand side of an assignment

并适用于整个当前范围,甚至在声明之前。当前,类型声明不能在全局范围内使用,例如在REPL中,因为Julia还没有常量类型的全局变量。

声明也可以附加到函数定义中:

代码语言:javascript
复制
function sinc(x)::Float64
    if x == 0
        return 1
    end
    return sin(pi*x)/(pi*x)
end

从此函数返回的行为就像是对具有声明类型的变量的赋值一样:该值始终转换为Float64

抽象类型

抽象类型无法实例化,只能用作类型图中的节点,从而描述了相关具体类型的集合:作为其后代的具体类型。我们从抽象类型开始,尽管它们没有实例化,因为它们是类型系统的骨干:它们形成概念层次结构,这使Julia的类型系统不仅仅是对象实现的集合。

回想一下,在整数和浮点数,我们推出了多种类型的具体数值的:Int8UInt8Int16UInt16Int32UInt32Int64UInt64Int128UInt128Float16Float32,和Float64。虽然他们有不同的表示大小,Int8Int16Int32Int64Int128所有的共同点在于它们是整数类型签名。同样UInt8UInt16UInt32UInt64并且UInt128都是无符号整数类型,同时Float16Float32Float64与浮点类型不同,而不是整数。例如,仅当一段代码的参数是某种整数,而不真正取决于哪种特定类型的整数时,一段代码才有意义。例如,最大的公分母算法适用于所有类型的整数,但不适用于浮点数。抽象类型允许构造类型的层次结构,从而提供适合具体类型的上下文。例如,这使您可以轻松地编程为任何整数类型,而无需将算法限制为特定的整数类型。

抽象类型使用abstract type关键字声明。声明抽象类型的常规语法为:

代码语言:javascript
复制
abstract type «name» end
abstract type «name» <: «supertype» end

abstract type关键字引入了一个新的抽象类型,其名字由下式给出«name»。该名称后面可以有一个可选的名称,<:并且该名称已经存在,表示新声明的抽象类型是此“父”类型的子类型。

如果未指定任何超类型,则默认超类型为Any–预定义的抽象类型,该对象的所有对象均为实例,所有类型均为其子类型。在类型理论中,Any通常将其称为“顶部”,因为它位于类型图的顶点。Julia在类型图的最低点处还有一个预定义的抽象“底部”类型,写为Union{}。恰恰相反Any:没有对象是的实例,Union{}所有类型都是的超类型Union{}

让我们考虑一下构成Julia的数字层次结构的一些抽象类型:

代码语言:javascript
复制
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

Number类型的直接子类Any,并且Real是它的孩子。反过来,Real有两个孩子(有更多孩子,但这里只显示了两个;稍后我们将介绍其他孩子):IntegerAbstractFloat,将世界分为整数表示和实数表示。实数的表示形式当然包括浮点类型,但也包括其他类型,例如有理数。因此,AbstractFloat是的适当子类型Real,仅包含实数的浮点表示。整数进一步细分为SignedUnsigned

<:一般的手段经营者“是的子类型”,并在声明中这样使用,声明右手型是新声明的类型的直接超类型。它也可以在表达式中用作子类型运算符,true当其左操作数是其右操作数的子类型时返回:

代码语言:javascript
复制
julia> Integer <: Number
true

julia> Integer <: AbstractFloat
false

抽象类型的重要用途是为具体类型提供默认实现。举一个简单的例子,考虑:

代码语言:javascript
复制
function myplus(x,y)
    x+y
end

首先要注意的是,上述参数声明与x::Any和等效y::Any。调用此函数时,如as myplus(2,5),调度程序将选择myplus与给定参数匹配的最具体的方法。(有关更多调度的更多信息,请参见方法。)

假设没有找到比上述方法更具体的方法,那么Julia会根据上面给出的泛型函数在内部定义并编译一个myplus专门针对两个Int参数的方法,即隐式定义和编译:

代码语言:javascript
复制
function myplus(x::Int,y::Int)
    x+y
end

最后,它调用此特定方法。

因此,抽象类型允许程序员编写泛型函数,这些泛型函数以后可以由许多具体类型的组合用作默认方法。由于有多个分派,程序员可以完全控制是使用默认方法还是使用更具体的方法。

需要注意的重要一点是,如果程序员依赖于其参数为抽象类型的函数,则不会降低性能,因为对于调用它的参数具体类型的每个元组,该函数都会重新编译。(但是,在函数参数是抽象类型的容器的情况下,可能会出现性能问题;请参阅性能提示。)

基本类型

基本类型是一种具体类型,其数据由普通旧位组成。基本类型的经典示例是整数和浮点值。与大多数语言不同,Julia使您可以声明自己的原始类型,而不是仅提供一组固定的内置类型。实际上,标准原始类型都是在语言本身中定义的:

代码语言:javascript
复制
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 32 end

primitive type Int8    <: Signed   8 end
primitive type UInt8   <: Unsigned 8 end
primitive type Int16   <: Signed   16 end
primitive type UInt16  <: Unsigned 16 end
primitive type Int32   <: Signed   32 end
primitive type UInt32  <: Unsigned 32 end
primitive type Int64   <: Signed   64 end
primitive type UInt64  <: Unsigned 64 end
primitive type Int128  <: Signed   128 end
primitive type UInt128 <: Unsigned 128 end

声明基本类型的常规语法为:

代码语言:javascript
复制
primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end

位数表明类型需要多少存储空间,名称为新类型提供了名称。基本类型可以选择声明为某些超类型的子类型。如果省略了父类型,则该类型默认为具有Any其直接父类型。Bool因此,上面的声明意味着布尔值需要八位存储,并具有Integer其直接超类型。当前,仅支持8位倍数的大小。因此,布尔值尽管实际上只需要一个位,但是不能声明为小于8位的任何值。

类型BoolInt8并且UInt8都具有相同的表示:他们的记忆八位块。由于Julia的类型系统是主格,因此尽管结构相同,但它们是不可互换的。它们之间的根本区别是它们具有不同的超类型:Bool直接超类型是IntegerInt8的is SignedUInt8的is Unsigned。之间的所有其他方面的差异BoolInt8并且,UInt8是行为的问题-函数的定义给出这些类型作为参数的对象时的行为方式。这就是为什么必须使用名词性类型系统的原因:如果结构确定了类型,而类型又决定了行为,则不可能使Bool行为不同于Int8UInt8

复合类型

组合类型在各种语言中称为记录,结构或对象。复合类型是命名字段的集合,可以将其实例视为单个值。在许多语言中,复合类型是用户定义的唯一类型,并且它们也是迄今为止Julia中最常用的用户定义类型。

在主流的面向对象的语言(例如C ++,Java,Python和Ruby)中,复合类型还具有与之关联的命名函数,这种组合称为“对象”。在诸如Ruby或Smalltalk之类的纯面向对象的语言中,所有值都是对象,无论它们是否是复合的。在不太纯的面向对象的语言(包括C ++和Java)中,某些值(例如整数和浮点值)不是对象,而用户定义的复合类型的实例是具有关联方法的真实对象。在Julia中,所有值都是对象,但函数并未与它们所操作的对象捆绑在一起。这是必需的,因为Julia会选择通过多次分派使用哪个函数方法,这意味着所有类型选择方法时,将考虑函数的自变量,而不仅仅是第一个(请参见方法,以获取有关方法和调度的更多信息)。因此,函数仅“属于”它们的第一个参数是不合适的。将方法组织到功能对象中,而不是在每个对象内部“命名”方法包,最终成为语言设计的一个非常有益的方面。

组合类型通过struct关键字引入,后跟一个字段名称块,可以选择使用::操作符在类型中进行注释:

代码语言:javascript
复制
julia> struct Foo
           bar
           baz::Int
           qux::Float64
       end

没有类型注释的字段默认为Any,并且可以相应地保存任何类型的值。

Foo通过将Foo类型对象像函数一样应用到其字段的值来创建类型的新对象:

代码语言:javascript
复制
julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)

julia> typeof(foo)
Foo

当类型像函数一样应用时,它称为构造函数。系统会自动生成两个构造函数(称为默认构造函数)。一个接受任何参数并调用convert()以将其转换为字段的类型,另一个接受与字段类型完全匹配的参数。生成这两者的原因是,这使得添加新定义变得更加容易,而不会无意间替换默认构造函数。

由于bar字段的类型不受限制,因此任何值都可以。但是,baz必须将的值转换为Int

代码语言:javascript
复制
julia> Foo((), 23.5, 1)
ERROR: InexactError()
Stacktrace:
 [1] convert(::Type{Int64}, ::Float64) at ./float.jl:679
 [2] Foo(::Tuple{}, ::Float64, ::Int64) at ./none:2

您可以使用该fieldnames功能找到一个字段名称列表。

代码语言:javascript
复制
julia> fieldnames(foo)
3-element Array{Symbol,1}:
 :bar
 :baz
 :qux

您可以使用传统的foo.bar符号访问复合对象的字段值:

代码语言:javascript
复制
julia> foo.bar
"Hello, world."

julia> foo.baz
23

julia> foo.qux
1.5

用声明的复合对象struct不可变的;它们在构造后无法修改。乍一看这很奇怪,但是它有几个优点:

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

不可变的对象可能包含可变对象(例如数组)作为字段。这些包含的物体将保持可变。只有不可变对象本身的字段不能更改为指向不同的对象。

如有需要,可以使用关键字声明可变的复合对象mutable struct,这将在下一节中讨论。

没有字段的复合类型是单例。只能有一个此类实例:

代码语言:javascript
复制
julia> struct NoFields
       end

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

===函数确认“的两个”构造实例NoFields实际上是相同的。单例类型将在下面进一步详细描述。

关于如何创建复合类型的实例还有更多的话要说,但是这种讨论既取决于参数类型也取决于方法,并且非常重要,因此可以在其自己的部分中进行论述:构造函数。

可变复合类型

如果使用mutable struct而不是声明了复合类型struct,则可以修改其实例:

代码语言:javascript
复制
julia> mutable struct Bar
           baz
           qux::Float64
       end

julia> bar = Bar("Hello", 1.5);

julia> bar.qux = 2.0
2.0

julia> bar.baz = 1//2
1//2

为了支持变异,此类对象通常在堆上分配,并具有稳定的内存地址。可变对象就像一个小容器,随着时间的推移可能具有不同的值,因此只能通过其地址可靠地进行标识。相反,不可变类型的实例与特定的字段值相关联-单独的字段值可以告诉您有关对象的所有信息。在确定是否使类型可变时,请问是否具有相同字段值的两个实例将被视为相同,或者是否可能需要随时间进行独立更改。如果将它们视为相同,则类型可能应该是不变的。

回顾一下,Julia中的两个基本属性定义了不变性:

  • 具有不变类型的对象通过复制传递(在赋值语句和函数调用中),而可变类型通过引用传递。
  • 不允许修改复合不可变类型的字段。

考虑一下为什么这两个属性并存的原因,对那些具有C / C ++背景的读者尤其有启发性。如果它们是分开的,即,如果可以修改通过复制传递的对象的字段,那么将难以推理某些通用代码实例。例如,假设x是抽象类型的函数参数,并且假设函数更改了字段:x.isprocessed = true。根据x是通过复制传递还是通过引用传递,此语句可能会或可能不会更改调用例程中的实际参数。在这种情况下,Julia通过禁止修改通过复制传递的对象字段来避免创建功能未知的函数的可能性。

声明的类型

实际上,前三个部分讨论的三种类型都紧密相关。它们具有相同的关键属性:

  • 它们被明确声明。
  • 他们有名字。
  • 它们已明确声明超类型。
  • 他们可能有参数。

由于具有这些共享的属性,这些类型在内部表示为具有相同概念的实例,这些概念DataType是以下任何类型的类型:

代码语言:javascript
复制
julia> typeof(Real)
DataType

julia> typeof(Int)
DataType

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

系统中的每个具体价值都是其中一个实例DataType

类型联合

类型联合是一种特殊的抽象类型,它包括使用特殊Union功能构造的其任何参数类型的所有实例作为对象:

代码语言:javascript
复制
julia> IntOrString = Union{Int,AbstractString}
Union{AbstractString, Int64}

julia> 1 :: IntOrString
1

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

julia> 1.0 :: IntOrString
ERROR: TypeError: typeassert: expected Union{AbstractString, Int64}, got Float64

许多语言的编译器都有一个内部的并集构造来推理类型。Julia只是将其公开给程序员。

参数类型

Julia的类型系统的一个重要且强大的功能是参数化:类型可以带有参数,因此类型声明实际上引入了一整套新类型-每个可能的参数值组合一个。有许多语言支持某种版本的通用编程,其中可以指定数据结构和操作它们的算法,而无需指定所涉及的确切类型。例如,仅举几例,ML,Haskell,Ada,Eiffel,C ++,Java,C#,F#和Scala中存在某种形式的通用编程。这些语言中的某些支持真正的参数多态性(例如ML,Haskell,Scala),而其他一些则支持基于临时的,基于模板的通用编程样式(例如C ++,Java)。由于使用了多种语言的通用编程和参数类型种类繁多,我们甚至不会尝试将Julia的参数类型与其他语言进行比较,而是将重点放在自行解释Julia的系统上。但是,我们会注意到,由于Julia是一种动态类型的语言,不需要在编译时做出所有类型的决定,

DataType可以使用所有相同的语法对所有声明的类型(变体)进行参数化。我们将按以下顺序讨论它们:首先是参数复合类型,然后是参数抽象类型,最后是参数位类型。

参数复合类型

类型参数在类型名称之后立即引入,并用花括号括起来:

代码语言:javascript
复制
julia> struct Point{T}
           x::T
           y::T
       end

该声明定义了一个新的参数类型,其中Point{T}包含type的两个“坐标” T。有人可能会问是T什么?嗯,这正是参数类型的意义:它可以是任何类型(实际上,也可以是任何位类型的值,尽管在这里显然用作类型)。Point{Float64}是一个具体类型等效于通过更换定义的类型T中的定义PointFloat64。因此,该单个声明实际上声明的类型的数量不受限制:Point{Float64}Point{AbstractString}Point{Int64}等。每个这些是现在可用的具体类型:

代码语言:javascript
复制
julia> Point{Float64}
Point{Float64}

julia> Point{AbstractString}
Point{AbstractString}

类型Point{Float64}是一个点,其坐标是64位浮点值,而类型Point{AbstractString}是一个“点”,其“坐标”是字符串对象(请参见Strings)。

Point本身也是一个有效的类型对象,包含所有实例Point{Float64}Point{AbstractString}等,如亚型:

代码语言:javascript
复制
julia> Point{Float64} <: Point
true

julia> Point{AbstractString} <: Point
true

当然,其他类型不是其子类型:

代码语言:javascript
复制
julia> Float64 <: Point
false

julia> AbstractString <: Point
false

Point值不同的具体类型T永远不会是彼此的子类型:

代码语言:javascript
复制
julia> Point{Float64} <: Point{Int64}
false

julia> Point{Float64} <: Point{Real}
false

警告

最后这一点是非常重要的:尽管Float64 <: Real我们不要有Point{Float64} <: Point{Real}

换句话说,用类型论的话来说,Julia的类型参数是不变的,而不是协变的(甚至是协变的)。这是出于实际的原因:尽管任何实例在Point{Float64}概念上也可能像实例Point{Real},但两种类型在内存中具有不同的表示形式:

  • 的一个实例Point{Float64}可以紧凑而有效地表示为64位值的直接对。
  • 的一个实例Point{Real}必须能够容纳的任意一对实例Real。由于作为实例的对象Real可以具有任意大小和结构,因此在实践中,的实例Point{Real}必须表示为指向单独分配的Real对象的一对指针。

Point{Float64}在数组的情况下,通过存储具有立即值的对象而获得的效率大大提高:Array{Float64}可以将an 存储为具有64位浮点值的连续存储块,而Array{Real}必须将数组存储为单独分配的指针Real对象-可能是装箱的 64位浮点值,但也可能是任意大而复杂的对象,这些对象被声明为Real抽象类型的实现。

由于Point{Float64}不是的子类型Point{Real},因此以下方法不能应用于类型的参数Point{Float64}

代码语言:javascript
复制
function norm(p::Point{Real})
    sqrt(p.x^2 + p.y^2)
end

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

代码语言:javascript
复制
function norm(p::Point{<:Real})
    sqrt(p.x^2 + p.y^2)
end

(等效地,可以定义function norm{T<:Real}(p::Point{T})function norm(p::Point{T} where T<:Real);请参见UnionAll Types。)

更多示例将在后面的“ 方法”中进行讨论。

一个人如何构造一个Point物体?可以为复合类型定义自定义构造函数,这将在“ 构造函数”中详细讨论,但是在没有任何特殊构造函数声明的情况下,有两种创建新复合对象的默认方法,一种是显式给出类型参数另一种是在对象构造函数的参数中暗含它们。

由于类型Point{Float64}是一个具体的类型,等效于PointFloat64代替声明T,因此可以将其相应地用作构造函数:

代码语言:javascript
复制
julia> Point{Float64}(1.0, 2.0)
Point{Float64}(1.0, 2.0)

julia> typeof(ans)
Point{Float64}

对于默认构造函数,必须为每个字段提供一个参数:

代码语言:javascript
复制
julia> Point{Float64}(1.0)
ERROR: MethodError: Cannot `convert` an object of type Float64 to an object of type Point{Float64}
This may have arisen from a call to the constructor Point{Float64}(...),
since type constructors fall back to convert methods.
Stacktrace:
 [1] Point{Float64}(::Float64) at ./sysimg.jl:24

julia> Point{Float64}(1.0,2.0,3.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)

由于无法覆盖参数类型,因此仅生成一个默认的构造函数。此构造函数接受任何参数并将其转换为字段类型。

在许多情况下,提供Point一个想要构造的对象类型是多余的,因为构造函数调用的参数类型已经隐式提供了类型信息。因此,只要Point参数类型的隐含值T是明确的,您还可以将其自身用作构造函数:

代码语言:javascript
复制
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}

在的情况下PointT当且仅当两个参数Point具有相同的类型时,类型才会明确隐含。如果不是这种情况,则构造函数将失败并显示MethodError

代码语言:javascript
复制
julia> Point(1,2.5)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
Closest candidates are:
  Point(::T, !Matched::T) where T at none:2

可以定义适当处理这种混合情况的构造方法,但是稍后将在Constructors中进行讨论。

参数抽象类型

参数抽象类型声明以相似的方式声明抽象类型的集合:

代码语言:javascript
复制
julia> abstract type Pointy{T} end

使用此声明,Pointy{T}是的每个类型或整数值都有一个不同的抽象类型T。与参数组合类型一样,每个此类实例都是的子类型Pointy

代码语言:javascript
复制
julia> Pointy{Int64} <: Pointy
true

julia> Pointy{1} <: Pointy
true

参数抽象类型是不变的,与参数组合类型一样:

代码语言:javascript
复制
julia> Pointy{Float64} <: Pointy{Real}
false

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

符号Pointy{<:Real}可以被用于表达的朱类似物协变型,而Pointy{>:Int}一个的类似物逆变型,但在技术上这些代表的类型(参见UnionAll类型)。

代码语言:javascript
复制
julia> Pointy{Float64} <: Pointy{<:Real}
true

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

就像普通的旧抽象类型用来在具体类型上创建有用的类型层次结构一样,参数抽象类型对于参数组合类型也可以达到相同的目的。例如,我们可以声明Point{T}为以下子类型Pointy{T}

代码语言:javascript
复制
julia> struct Point{T} <: Pointy{T}
           x::T
           y::T
       end

给定这样的声明,对于的每个选择T,我们都有Point{T}以下子类型Pointy{T}

代码语言:javascript
复制
julia> Point{Float64} <: Pointy{Float64}
true

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

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

这种关系也是不变的:

代码语言:javascript
复制
julia> Point{Float64} <: Pointy{Real}
false

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

像参数这样的抽象类型有什么用途Pointy?考虑一下,如果我们创建一个只需要一个坐标的类点实现,因为该点位于对角线上x = y

代码语言:javascript
复制
julia> struct DiagPoint{T} <: Pointy{T}
           x::T
       end

现在Point{Float64}DiagPoint{Float64}都是Pointy{Float64}抽象的实现,并且类似地适用于其他所有可能的type选择T。这样就可以编程为所有Pointy对象共享的通用接口,Point并为和实现DiagPoint。但是,直到我们在下一节“ 方法”中介绍方法并进行分派之前,才能完全证明这一点。

在某些情况下,类型参数在所有可能的类型上自由范围可能没有意义。在这种情况下,可以T像这样限制范围:

代码语言:javascript
复制
julia> abstract type Pointy{T<:Real} end

有了这样的声明,可以使用任何作为的子类型的类型来Real代替T,但不能使用不是以下子类型的类型Real

代码语言:javascript
复制
julia> Pointy{Float64}
Pointy{Float64}

julia> Pointy{Real}
Pointy{Real}

julia> Pointy{AbstractString}
ERROR: TypeError: Pointy: in T, expected T<:Real, got Type{AbstractString}

julia> Pointy{1}
ERROR: TypeError: Pointy: in T, expected T<:Real, got Int64

可以用相同的方式来限制参数组合类型的类型参数:

代码语言:javascript
复制
struct Point{T<:Real} <: Pointy{T}
    x::T
    y::T
end

为了举例说明所有这些参数类型机制如何有用,下面是Julia Rational不可变类型的实际定义(除了为简单起见,这里省略了构造函数),它表示整数的精确比例:

代码语言:javascript
复制
struct Rational{T<:Integer} <: Real
    num::T
    den::T
end

仅取整数值的比率是有意义的,因此将参数类型T限制为的子类型Integer,并且整数比率表示实数行上的值,因此任何Rational实例都是Real抽象的实例。

元组类型

元组是函数参数的抽象,而没有函数本身。函数参数的主要方面是它们的顺序和类型。因此,元组类型类似于参数化的不可变类型,其中每个参数都是一个字段的类型。例如,2元素元组类型类似于以下不可变类型:

代码语言:javascript
复制
struct Tuple2{A,B}
    a::A
    b::B
end

但是,存在三个主要区别:

  • 元组类型可以具有任意数量的参数。
  • 元组类型的参数是协变的:Tuple{Int}是的子类型Tuple{Any}。因此,Tuple{Any}将其视为抽象类型,并且元组类型仅在其参数为实的情况下才是具体的。
  • 元组没有字段名称;字段只能按索引访问。

元组值用括号和逗号书写。构建元组时,会根据需要生成适当的元组类型:

代码语言:javascript
复制
julia> typeof((1,"foo",2.5))
Tuple{Int64,String,Float64}

注意协方差的含义:

代码语言:javascript
复制
julia> Tuple{Int,AbstractString} <: Tuple{Real,Any}
true

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

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

直观地,这对应于作为函数签名的子类型的函数自变量的类型(当签名匹配时)。

Vararg元组类型

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

代码语言:javascript
复制
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元组类型用于表示varargs方法接受的参数(请参见Varargs函数)。

类型Vararg{T,N}完全对应于Ntype的元素TNTuple{N,T}是一个方便的别名,例如Tuple{Vararg{T,N}},一个元组类型正好包含type的N元素T

单例类型

这里必须提到一种特殊的抽象参数类型:单例类型。对于每种类型,T“单一类型” Type{T}都是抽象类型,其唯一实例是object T。由于定义有点难以解析,因此让我们看一些示例:

代码语言:javascript
复制
julia> isa(Float64, Type{Float64})
true

julia> isa(Real, Type{Float64})
false

julia> isa(Real, Type{Real})
true

julia> isa(Float64, Type{Real})
false

换句话说,isa(A,Type{B})当且仅当AB是相同的对象并且该对象是一种类型时,才为true 。没有参数,Type只是一个抽象类型,它以所有类型对象作为其实例,当然包括单例类型:

代码语言:javascript
复制
julia> isa(Type{Float64}, Type)
true

julia> isa(Float64, Type)
true

julia> isa(Real, Type)
true

任何不是类型的对象都不是的实例Type

代码语言:javascript
复制
julia> isa(1, Type)
false

julia> isa("foo", Type)
false

在我们讨论参数化方法和转换之前,很难解释单例类型构造的实用性,但是简而言之,它允许人们将函数行为专门化为特定类型。这对于编写其行为取决于作为显式参数给出的类型而不是其参数之一的类型所隐含类型的方法(尤其是参数化方法)很有用。

一些流行的语言具有单例类型,包括Haskell,Scala和Ruby。通常,术语“单一类型”是指其唯一实例是单个值的类型。此含义适用于Julia的单例类型,但需要注意的是,只有类型对象具有单例类型。

参数基本类型

基本类型也可以通过参数声明。例如,指针表示为原始类型,将在Julia中声明如下类型:

代码语言:javascript
复制
# 32-bit system:
primitive type Ptr{T} 32 end

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

与典型的参数组合类型相比,这些声明的稍微奇怪的特征是,类型参数T未用于类型本身的定义中,它只是一个抽象标记,本质上定义了具有相同结构,仅按其类型参数。因此,Ptr{Float64}Ptr{Int64}是不同的类型,即使它们具有相同的表示形式。当然,所有特定的指针类型都是伞形Ptr类型的子类型:

代码语言:javascript
复制
julia> Ptr{Float64} <: Ptr
true

julia> Ptr{Int64} <: Ptr
true

UnionAll类型

我们已经说过,像这样的参数类型将Ptr充当其所有实例(Ptr{Int64}等等)的超类型。这是如何运作的?Ptr它本身不能是普通的数据类型,因为在不知道引用数据的类型的情况下,该类型显然不能用于内存操作。答案是Ptr(或其他参数类型,如Array)是另一种类型,称为UnionAll类型。这种类型表示某个参数的所有值的类型的迭代联合

UnionAll类型通常使用关键字where。例如Ptr,可以更准确地写为Ptr{T} where T,表示类型为Ptr{T}的某个值的所有值T。在这种情况下,该参数T通常也称为“类型变量”,因为它就像一个跨类型的变量。每个where变量都引入一个类型变量,因此这些表达式嵌套在具有多个参数的类型中,例如Array{T,N} where N where T

类型应用程序语法A{B,C}必须AUnionAll类型,并且首先替换B中最外面的类型变量A。预期结果是另一种UnionAll类型,C然后将其替换。所以A{B,C}等于A{B}{C}。这就解释了为什么可以部分实例化一个类型,例如Array{Float64}:第一个参数值是固定的,但是第二个参数值仍在所有可能的值范围内。使用显式where语法,可以固定任何参数子集。例如,所有一维数组的类型都可以写成Array{T,1} where T

类型变量可以受子类型关系限制。Array{T} where T<:Integer引用元素类型为的所有数组Integer。该语法Array{<:Integer}是的便捷简写Array{T} where T<:Integer。类型变量可以具有上限和下限。Array{T} where Int<:T<:Number引用Number能够包含Ints的所有s 数组(因为T必须至少与一样大Int)。该语法where T>:Int还可以仅指定类型变量的下限,并且Array{>:Int}等效于Array{T} where T>:Int

由于where表达式嵌套,所以类型变量边界可以引用外部类型变量。例如,Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real引用2元组,其第一个元素为some Real,并且其第二个元素为Array任何类型的数组,其元素类型包含第一个元组元素的类型。

where关键字本身可以被嵌套在一个更复杂的声明中。例如,考虑以下声明创建的两种类型:

代码语言:javascript
复制
julia> const T1 = Array{Array{T,1} where T, 1}
Array{Array{T,1} where T,1}

julia> const T2 = Array{Array{T,1}, 1} where T
Array{Array{T,1},1} where T

类型T1定义一维数组的一维数组;每个内部数组都由相同类型的对象组成,但是此类型可能因一个内部数组而异。另一方面,type T2定义一维数组,其中一维数组的所有内部数组必须具有相同的类型。请注意,这T2是抽象类型,例如Array{Array{Int,1},1} <: T2,而是T1具体类型。结果,T1可以使用零参数构造函数构造a=T1()T2不能构造。

有一种方便的语法来命名此类类型,类似于函数定义语法的简称:

代码语言:javascript
复制
Vector{T} = Array{T,1}

这等效于const Vector = Array{T,1} where T。编写Vector{Float64}与编写等效Array{Float64,1},并且伞形类型Vector具有所有Array对象的实例,其中第二个参数(数组维数)为1,无论元素类型是什么。在必须始终完整指定参数类型的语言中,这并不是特别有帮助,但是在Julia中,这允许人们只Vector为抽象类型编写代码,包括任何元素类型的所有一维密集数组。

类型别名

有时,为已经可以表达的类型引入新名称很方便。这可以通过一个简单的赋值语句来完成。例如,对于系统上的指针大小,UInt别名为UInt32或,别名UInt64是:

代码语言:javascript
复制
# 32-bit system:
julia> UInt
UInt32

# 64-bit system:
julia> UInt
UInt64

这是通过以下代码完成的base/boot.jl

代码语言:javascript
复制
if Int === Int64
    const UInt = UInt64
else
    const UInt = UInt32
end

当然,这要看是什么Int的别名-但被预定是正确的类型-无论是Int32Int64

(请注意,与不同IntFloat并不作为特定大小的类型别名存在AbstractFloat。与整数寄存器不同,浮点寄存器的大小由IEEE-754标准指定。而的大小Int反映了该计算机上本机指针的大小。)

类型操作

由于Julia中的类型本身就是对象,因此普通函数可以对其进行操作。已经引入了一些对于处理或浏览类型特别有用的函数,例如<:运算符,该运算符指示其左手操作数是否是其右手操作数的子类型。

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

代码语言:javascript
复制
julia> isa(1, Int)
true

julia> isa(1, AbstractFloat)
false

typeof()函数已在示例的整个手册中使用,返回其参数的类型。如上所述,由于类型是对象,因此它们也具有类型,我们可以询问它们的类型是什么:

代码语言:javascript
复制
julia> typeof(Rational{Int})
DataType

julia> typeof(Union{Real,Float64,Rational})
DataType

julia> typeof(Union{Real,String})
Union

如果我们重复该过程怎么办?类型的类型是什么类型?碰巧的是,类型都是复合值,因此都具有以下类型DataType

代码语言:javascript
复制
julia> typeof(DataType)
DataType

julia> typeof(Union)
DataType

DataType 是它自己的类型。

适用于某些类型的另一个操作是supertype(),它揭示了类型的超类型。只有声明的类型(DataType)具有明确的超类型:

代码语言:javascript
复制
julia> supertype(Float64)
AbstractFloat

julia> supertype(Number)
Any

julia> supertype(AbstractString)
Any

julia> supertype(Any)
Any

如果将其应用于supertype()其他类型对象(或非类型对象),MethodError则会引发a:

代码语言:javascript
复制
julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
Closest candidates are:
  supertype(!Matched::DataType) at operators.jl:41
  supertype(!Matched::UnionAll) at operators.jl:46

定制漂亮印刷

通常,人们想要自定义类型实例的显示方式。这是通过重载show()功能来实现的。例如,假设我们定义了一种类型,以极性形式表示复数:

代码语言:javascript
复制
julia> struct Polar{T<:Real} <: Number
           r::T
           Θ::T
       end

julia> Polar(r::Real,Θ::Real) = Polar(promote(r,Θ)...)
Polar

在这里,我们添加了一个自定义的构造函数,以便它可以接受不同Real类型的参数并将其提升为通用类型(请参阅构造函数以及Conversion和Promotion)。(当然,我们必须定义很多其他的方法,也使它像一个Number,例如+*onezero,促销规则等等。)默认情况下,而只是这种类型的显示器的情况下,与有关信息类型名称和字段值,例如Polar{Float64}(3.0,4.0)

如果我们希望将其显示为3.0 * exp(4.0im),则可以定义以下方法将对象打印到给定的输出对象io(代表文件,终端,缓冲区等;请参见网络和流):

代码语言:javascript
复制
julia> Base.show(io::IO, z::Polar) = print(io, z.r, " * exp(", z.Θ, "im)")

Polar可以对对象的显示进行更细粒度的控制。特别地,有时人们既想要用于在REPL和其他交互环境中显示单个对象的冗长的多行打印格式,又想要一种更紧凑的用于print()或作为另一对象的一部分显示对象的单行格式。(例如,在数组中)。尽管默认情况下show(io, z)会在两种情况下都调用该函数,但是您可以通过重载以MIME类型作为其第二个参数的三参数形式来定义用于显示对象的不同多行格式(请参见Multimedia I / O),例:showtext/plain

代码语言:javascript
复制
julia> Base.show{T}(io::IO, ::MIME"text/plain", z::Polar{T}) =
           print(io, "Polar{$T} complex number:\n   ", z)

(请注意,print(..., z)这里将调用2参数show(io, z)方法。)这将导致:

代码语言:javascript
复制
julia> Polar(3, 4.0)
Polar{Float64} complex number:
   3.0 * exp(4.0im)

julia> [Polar(3, 4.0), Polar(4.0,5.3)]
2-element Array{Polar{Float64},1}:
 3.0 * exp(4.0im)
 4.0 * exp(5.3im)

单行show(io, z)形式仍然用于Polar值数组。技术上,REPL来电display(z)显示执行一条线,其默认的结果show(STDOUT, MIME("text/plain"), z),而这又默认show(STDOUT, z),但你应该定义新的display(),除非你要定义一个新的多媒体显示处理器(参见方法多媒体I / O)。

此外,您还可show以为其他MIME类型定义方法,以在支持此功能的环境(例如IJulia)中更丰富地显示对象(HTML,图像等)。例如,我们可以通过以下方式定义Polar对象的带格式的HTML显示:上标和斜体

代码语言:javascript
复制
julia> Base.show{T}(io::IO, ::MIME"text/html", z::Polar{T}) =
           println(io, "<code>Polar{$T}</code> complex number: ",
                   z.r, " <i>e</i><sup>", z.Θ, " <i>i</i></sup>")

然后,Polar对象将在支持HTML显示的环境中使用HTML自动显示,但是如果需要,您可以show手动调用以获取HTML输出:

代码语言:javascript
复制
julia> show(STDOUT, "text/html", Polar(3.0,4.0))
<code>Polar{Float64}</code> complex number: 3.0 <i>e</i><sup>4.0 <i>i</i></sup>

HTML渲染器将显示为:Polar{Float64}复数:3.0 e 4.0 i

“值类型”

在Julia中,您无法分派诸如或的。但是,您可以分派参数类型,Julia允许您将“普通位”值(类型,符号,整数,浮点数,元组等)包括为类型参数。一个常见的示例是中的Dimensionity参数,其中是类型(例如),但仅仅是一个。truefalseArray{T,N}TFloat64NInt

您可以创建自己的以值作为参数的自定义类型,并使用它们来控制自定义类型的调度。为了说明这一点,让我们介绍一个参数类型,Val{T}它是在不需要更详尽的层次结构的情况下利用此技术的一种常用方法。

Val 定义为:

代码语言:javascript
复制
julia> struct Val{T}
       end

除了实现之外,没有更多的实现了Val。Julia的标准库中的某些函数接受Val类型作为参数,您也可以使用它编写自己的函数。例如:

代码语言:javascript
复制
julia> firstlast(::Type{Val{true}}) = "First"
firstlast (generic function with 1 method)

julia> firstlast(::Type{Val{false}}) = "Last"
firstlast (generic function with 2 methods)

julia> firstlast(Val{true})
"First"

julia> firstlast(Val{false})
"Last"

为了确保Julia的一致性,呼叫站点应始终传递Val类型而不是创建实例,即使用foo(Val{:bar})而不是foo(Val{:bar}())

值得注意的是,滥用参数“值”类型非常容易,包括Val; 在不利的情况下,您很容易最终使代码的性能变差。特别是,您永远都不想编写如上所述的实际代码。有关正确(和不正确)使用的更多信息Val,请阅读性能提示中的更广泛的讨论。

可空类型:代表缺失值

在许多设置中,您需要与T可能存在或可能不存在的类型的值进行交互。为了处理这些设置,Julia提供了称为的参数类型Nullable{T},可以将其视为可以包含零或一个值的特殊容器类型。Nullable{T}提供了一个最小的界面,旨在确保与缺失值的交互是安全的。目前,该界面包含几种可能的交互方式:

  • 构造一个Nullable对象。
  • 检查Nullable对象是否缺少值。
  • 访问Nullable对象的值并保证NullException如果缺少该对象的值将抛出a 。
  • 访问Nullable对象的值,并保证T如果缺少该对象的值,将返回默认类型的值。
  • 对对象的值(如果存在)执行操作Nullable,得到Nullable结果。如果缺少原始值,则结果将丢失。
  • 对对象的值(如果存在)执行测试,Nullable如果Nullable本身丢失或测试失败,则得到缺少的结果。
  • 对单个Nullable对象执行常规操作,以传播丢失的数据。

构造Nullable对象

要构造表示缺少类型的值的对象T,请使用以下Nullable{T}()函数:

代码语言:javascript
复制
julia> x1 = Nullable{Int64}()
Nullable{Int64}()

julia> x2 = Nullable{Float64}()
Nullable{Float64}()

julia> x3 = Nullable{Vector{Int64}}()
Nullable{Array{Int64,1}}()

要构造一个表示type的非缺失值的对象T,请使用以下Nullable(x::T)函数:

代码语言:javascript
复制
julia> x1 = Nullable(1)
Nullable{Int64}(1)

julia> x2 = Nullable(1.0)
Nullable{Float64}(1.0)

julia> x3 = Nullable([1, 2, 3])
Nullable{Array{Int64,1}}([1, 2, 3])

请注意,这两种构造Nullable对象的方式之间的核心区别是:在一种样式中,您提供了类型T,作为函数参数;在另一种样式中,您提供一个type值T作为参数。

检查Nullable对象是否具有值

您可以使用以下命令检查Nullable对象是否具有任何值isnull()

代码语言:javascript
复制
julia> isnull(Nullable{Float64}())
true

julia> isnull(Nullable(0.0))
false

安全地访问Nullable对象的值

您可以使用以下命令安全地访问Nullable对象的值get()

代码语言:javascript
复制
julia> get(Nullable{Float64}())
ERROR: NullException()
Stacktrace:
 [1] get(::Nullable{Float64}) at ./nullable.jl:92

julia> get(Nullable(1.0))
1.0

如果不存在该值Nullable{Float64}NullException则将引发错误。该get()函数的错误抛出性质可确保任何访问缺失值的尝试均立即失败。

如果某个合理的默认值存在,而当某个Nullable对象的值丢失时可以使用该默认值,则可以将该默认值作为第二个参数提供给get()

代码语言:javascript
复制
julia> get(Nullable{Float64}(), 0.0)
0.0

julia> get(Nullable(1.0), 0.0)
1.0

小费

确保传递给默认值的类型get()Nullable对象的类型匹配,以避免类型不稳定,这可能会损害性能。convert()如果需要,请手动使用。

Nullable对象执行操作

Nullable对象表示可能丢失的值,可以通过使用进行测试,首先检查值是否丢失isnull(),然后执行适当的操作,从而使用这些对象编写所有代码。但是,在一些常见的用例中,通过使用高阶函数可以使代码更简明。

map函数将一个函数f和一个Nullable值作为参数x。它产生一个Nullable

  • 如果x是缺失值,则产生缺失值;
  • 如果x具有值,则产生一个Nullable包含f(get(x))值。

如果期望的行为是简单地向前传播缺失的值,则对于在可能缺失的值上执行简单操作很有用。

filter函数将谓词函数p(即返回布尔值的函数)和Nullablevalue 作为参数x。它产生一个Nullable值:

  • 如果x是缺失值,则产生缺失值;
  • 如果p(get(x))为true,则产生原始值x
  • 如果p(get(x))为false,则产生一个缺失值。

这样,filter可以认为仅选择允许的值,然后将不允许的值转换为缺失值。

虽然mapfilter在特定情况下很有用,但到目前为止,最有用的高阶函数是broadcast,它可以处理各种情况,包括使现有操作正常工作和传播Nullable。一个例子将激发对的需求broadcast。假设我们有一个函数,可以使用二次公式来计算二次方程的两个实根中的较大者:

代码语言:javascript
复制
julia> root(a::Real, b::Real, c::Real) = (-b + √(b^2 - 4a*c)) / 2a
root (generic function with 1 method)

正如我们期望的那样,我们可以验证的结果root(1, -9, 20)5.0,因为它5.0是二次方程的两个实根中的较大者。

现在假设我们想找到一个二次方程的最大实根,其中系数可能缺少值。数据集中缺少值是现实数据中的常见现象,因此能够处理它们很重要。但是,如果我们不知道所有系数,就无法找到方程式的根。最好的解决方案将取决于特定的用例。也许我们应该抛出一个错误。但是,对于本示例,我们将假定最佳解决方案是将丢失的值向前传播。也就是说,如果缺少任何输入,我们只会产生一个缺少的输出。

broadcast()功能使此任务变得容易。我们可以简单地传递root我们写给的函数broadcast

代码语言:javascript
复制
julia> broadcast(root, Nullable(1), Nullable(-9), Nullable(20))
Nullable{Float64}(5.0)

julia> broadcast(root, Nullable(1), Nullable{Int}(), Nullable{Int}())
Nullable{Float64}()

julia> broadcast(root, Nullable{Int}(), Nullable(-9), Nullable(20))
Nullable{Float64}()

如果缺少一个或多个输入,则输出broadcast()将丢失。

对于broadcast()使用点表示法的功能,存在特殊的语法糖:

代码语言:javascript
复制
julia> root.(Nullable(1), Nullable(-9), Nullable(20))
Nullable{Float64}(5.0)

特别是,可以broadcast()使用.-prefixed 运算符方便地使用常规算术运算符:

代码语言:javascript
复制
julia> Nullable(2) ./ Nullable(3) .+ Nullable(1.0)
Nullable{Float64}(1.66667)
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 云深之无迹 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 类型声明
  • 抽象类型
  • 基本类型
  • 复合类型
  • 可变复合类型
  • 声明的类型
  • 类型联合
  • 参数类型
    • 参数复合类型
      • 参数抽象类型
        • 元组类型
          • Vararg元组类型
            • 单例类型
          • 参数基本类型
          • UnionAll类型
          • 类型别名
          • 类型操作
          • 定制漂亮印刷
          • “值类型”
          • 可空类型:代表缺失值
            • 构造Nullable对象
              • 检查Nullable对象是否具有值
                • 安全地访问Nullable对象的值
                  • 对Nullable对象执行操作
                  相关产品与服务
                  容器服务
                  腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档