Engee 文档

类别

传统上,类型系统分为两类:静态的,在执行程序之前确定每个表达式的类型;动态的,在执行之前对类型一无所知,直到实际值可用。 在具有静态类型的语言中,对象方法允许一些灵活性:在编写代码时,不需要在编译时确切地知道值的类型。 同一个代码可以对不同类型进行操作的现象称为多态。 在经典的动态类型语言中,所有代码都是多态的:只有在显式检查类型或运行时不支持对对象的操作时,值类型才会受到限制。

Julia的类型系统是动态的,但它具有静态系统的一些优点。:您可以指定对象具有特定类型。 这不仅有助于创建更高效的代码,而且更重要的是,允许将基于参数类型的方法调度深度集成到语言中。 调度方法将在本章中详细讨论 方法,但它的根源在于这里描述的类型系统。

如果在Julia中未指定值类型,则默认情况下假定该类型可以是任何类型。 因此,在Julia中,您可以编写各种函数,而无需显式使用类型。 但是,如果您需要使函数的行为更显式,则可以逐步向以前未键入的代码添加显式类型注释。 添加注解有三个主要目标:使用Julia高效的多重分派机制,提高代码可读性,拦截程序员错误。

方面的https://en.wikipedia.org/wiki/Type_system [类型系统]Julia语言可以被描述为动态,主格和参数。 通用类型可以参数化,类型之间的层次关系可以是https://en.wikipedia.org/wiki/Nominal_type_system [被明确声明]而不是https://en.wikipedia.org/wiki/Structural_type_system [根据结构暗示]。 Julia类型系统的一个重要区别特征是具体类型不能是彼此的子类型:所有具体类型都是有限的,只有抽象类型可以是它们的超类型。 虽然乍一看这个限制可能看起来不必要地严格,但它有许多积极的影响和令人惊讶的少量缺点。 事实证明,继承行为的能力比继承结构的能力重要得多,并且继承两者在传统的面向对象语言中造成了严重的困难。 此外,我们应该立即提到Julia类型系统的其他一些重要方面。

  • 值不分为对象和非对象:Julia中的所有值都是真正的对象,其类型属于单个全连接类型图。 此图的所有节点都是相等的类型。

  • Julia中"编译时类型"的概念没有意义:一个值可以拥有的唯一类型是它在程序执行时的实际类型。 在面向对象语言中,这被称为运行时类型,由于静态编译和多态的结合,这种差异是必不可少的。

  • 只有值有类型,而不是变量:变量只是与值相关联的名称,尽管为了简洁起见,您可以说"变量类型"而不是"变量引用的值类型"。

  • 抽象类型和具体类型都可以由其他类型参数化。 它们也可以通过符号参数化,函数的值 'isbits'返回true(特别是存储为C类型或结构(struct)而没有指向其他对象的指针的数值和布尔值),以及此类值的元组。 如果类型参数不需要被引用或限制,那么它们可以被省略。

朱莉娅类型系统的设计是有效和表达,但在同一时间清晰组织,直观和不引人注目。 许多Julia程序员根本不必编写显式应用类型的代码。 但是,在某些情况下,声明类型允许您使代码更清晰,更简单,更快,更可靠。

类型声明

使用运算符`:’可以为代码中的表达式和变量添加类型注释。 这可能有两个主要原因:

  1. 有助于确保程序正确运行的语句。

  2. 向编译器提供额外的类型信息,在某些情况下可用于提高性能。

当添加到用于计算值的表达式时,运算符`:’表示"是类型的实例"。 因此,他声称左侧表达式的值是右侧类型的实例。 如果右侧的类型是特定的,则左侧的值必须由该类型实现。 回想一下,所有具体类型都是有限的,因此类型的实现不能是另一个实现的子类型。 如果右侧的类型是abstract,则值由特定类型实现就足够了,该类型是此abstract类型的子类型。 如果type语句不为true,则抛出异常;否则,返回左侧表达式的值。

julia> (1+2)::AbstractFloat
ERROR: TypeError: in typeassert, expected AbstractFloat, got a value of type Int64

julia> (1+2)::Int
3

这允许您将type语句添加到任何表达式中。

当添加到赋值左侧的变量或作为"本地"声明的一部分时`"::"运算符的含义略有不同:它声明变量始终具有指定的类型,类似于静态类型语言(如c)中的类型声明。 '转换'

julia> function foo()
           x::Int8 = 100
           x
       end
foo (generic function with 1 method)

julia> x = foo()
100

julia> typeof(x)
Int8

此应用程序允许您避免由于分配意外类型的变量而导致性能问题。

此选项(类型声明)仅在某些情况下使用。:

local x::Int8  # в объявлении local
x::Int8 = 10   # в левой части присваивания

甚至在宣布之前,它也适用于整个当前区域。

从Julia1.8开始,类型声明可以在全局作用域中使用,这意味着您可以向全局变量添加类型注释,以便它们是类型稳定的。

julia> x::Int = 10
10

julia> x = 3.5
ERROR: InexactError: Int64(3.5)

julia> function foo(y)
           global x = 15.8    # при вызове foo выдается ошибка
           return x + y
       end
foo (generic function with 1 method)

julia> foo(10)
ERROR: InexactError: Int64(15.8)

声明也可以应用于函数定义。

function sinc(x)::Float64
    if x == 0
        return 1
    end
    return sin(pi*x)/(pi*x)
end

当这个函数返回一个值时,会发生与为具有声明类型的变量赋值时相同的事情:该值总是转换为`Float64`类型。

抽象类型

抽象类型不允许实例化,而只是作为类型图中的节点,描述相关具体类型的集合-类型是该抽象类型的后代。 虽然抽象类型不涉及实例化,但我们将从它们开始,因为它们是类型系统的骨干。 它们形成了一个概念层次结构,因此Julia类型系统不仅仅是一组对象实现。

你可能还记得,在章节中 整数和浮点数介绍了各种特定类型的数值: 'Int8`, 'UInt8`, 'Int16`, 'UInt16`, 'Int32`, 'UInt32`, 'Int64`, 'UInt64`, 'Int128`, 'UInt128`, '浮16`, 'Float32''Float64'。 虽然类型`Int8','Int16`,Int32Int64`和’Int128’具有不同的表示大小,但将它们统一起来的是它们都是有符号整数类型。 反过来’UInt8UInt16UInt32UInt64`和`UInt128`是无符号整数类型,而`Float16,`Float32`和`Float64’是浮点类型。 通常,在代码的某个部分中,参数是一些整数可能很重要,但命名哪些并不重要。 因此,找到最大公约数的算法将适用于任何整数,但不适用于浮点数。 抽象类型允许您构建适合特定类型的类型层次结构。 例如,它可以很容易地编写一个算法,该算法将不受限制地与任何整数类型一起工作。

抽象类型使用关键字声明 '抽象类型'。 声明抽象类型的标准语法结构如下所示:

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

关键字’abstract type’引入了一个名为`"name"`的新抽象类型。 在此名称之后,您可以指定 '<’和任何现有类型。 这意味着要声明的抽象类型是此父类型的子类型。

如果没有指定超类型,默认情况下它是’Any'--一个预定义的抽象类型,其中所有对象都是实例,所有其他类型都是子类型。 在类型理论中,类型’Any’通常被称为最高类型,因为它位于类型图的最顶端。 Julia还有一个预定义的抽象基类型,它位于类型图的最低点。 它被写成’联合{}`. Этот тип является прямой противоположностью типу Any: ни один объект не является экземпляром Union{},并且所有类型都是’Union{}'的超类型。

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

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`是它的后代。 反过来’真实’有两个后代(实际上他们有更多,但只有两个在这里呈现;我们将在稍后返回其余部分)。 '整数''AbstractFloat'。 它们分别表示整数和实数。 实数的表示包括浮点类型,但也有其他类型,如有理类型。 'AbstractFloat’只包括浮动’点实数的表示。 整数类型进一步细分为 '签名'(带符号)和 'Unsigned'(无符号)。

运算符'<:在一般意义上,它表示"是子类型",并声明右侧的类型是被声明类型的直接超类型。 如果左操作数是右操作数的子类型,它也可以用作返回`true’的表达式中的子类型运算符。

julia> Integer <: Number
true

julia> Integer <: AbstractFloat
false

抽象类型的一个重要目的是为特定类型提供默认实现。 下面是一个简单的例子。

function myplus(x,y)
    x+y
end

首先,您应该注意这样一个事实,即上述参数声明等同于’x::Any`和`y::Any`。 当调用此函数时,例如`myplus(2,5)`,调度程序选择与传递的参数相对应的名为`myplus’的最具体的方法。 (有关多个调度的更多信息,请参阅章节 方法。)

如果没有找到比上面更具体的方法,Julia会创建一个内部定义,并根据上面的通用函数为两个类型为"Int"的参数编译一个名为"myplus"的方法。 换句话说,下面的方法是隐式定义和编译的。

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

最后,调用此特定方法。

因此,抽象类型允许开发人员编写通用函数,这些函数以后可以用作特定类型的各种组合的默认方法。 由于多次调度,程序员可以完全控制使用哪种方法:默认方法或更具体的方法。

重要的是要注意,使用具有抽象类型参数的函数不会影响性能,因为该函数是针对调用它的特定类型参数的每个元组重复编译的。 (但是,如果函数参数是抽象类型的容器,则可能会出现性能问题;请参阅本章 性能提示。)

原始类型

将现有的基元类型包含在新的复合类型中几乎总是可取的,而不是定义自己的基元类型。

此功能是必要的,以便Julia环境可以初始化LLVM支持的标准基元类型。 在确定它们之后,实际上没有理由确定其他此类类型。

基元类型是由普通位组成的特定类型。 基本类型的经典示例是整数和浮点值。 与大多数语言不同的是,只有一组固定的内置基元类型可用,Julia允许您定义自己的此类类型。 实际上,所有的标准基元类型都是在语言本身中定义的。

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
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

用于声明原始类型的标准语法结构如下所示:

primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end

Bits元素指示存储类型需要多少位,name元素定义新类型的名称。 您还可以将原始类型声明为超类型的子类型。 如果没有指定超类型,那么直接的默认超类型是`Any'。 因此,上述声明 `Bool'表示需要八位来存储一个布尔值,它的直接超类型是类型 '整数'。 目前,仅支持8位倍数的大小,并且在使用上面列出的大小以外的大小时可能会出现LLVM错误。 因此,虽然只有一位足以表示布尔值,但不能将其声明为小于8位的大小。

类别 'Bool', 'Int8'UInt8'在内存中具有相同的表示:它们都占用8位。 然而,由于Julia类型系统是主格的,尽管结构相同,但这些类型是不可互换的。 它们之间的根本区别在于它们具有不同的超类型: `Bool'直接超类型 — '整数',y 'Int8 — '签名',和y 'UInt8` — '未签名`。 否则,之间的差异 'Bool', 'Int8''UInt8'归结为当将这些类型的对象作为参数传递时,为函数定义了什么行为。 这就是为什么主格类型系统是必要的:如果结构定义了类型,而它反过来又决定了行为,那么就不可能实现 `Bool'不同于for的行为 'Int8''UInt8'

复合类型

在不同的语言https://en.wikipedia.org/wiki/Composite_data_type [复合类型]以不同的方式调用:记录,结构,对象。 复合类型是命名字段的集合,您可以将其实例作为单个值使用。 在许多语言中,用户只能定义复合类型,但在Julia中,大多数用户定义类型都是复合的。

在流行的面向对象语言,如C++ 在Java,Python和Ruby中,命名函数也与复合类型相关联:统称为对象。 在Ruby或Smalltalk等更简洁的面向对象语言中,对象都是值,无论是否复合。 在不那么纯粹的面向对象语言中,包括C++ 而Java中,一些值,如整数和浮点,不是对象。 True对象是具有关联方法的自定义复合类型的实例。 在Julia中,对象是任何值,但函数不绑定到它们操作的对象。 这是由于在Julia中,所需的函数方法是通过多次调度来选择的,这意味着在选择方法时,考虑了函数的所有参数的类型,而不仅仅是第一个(有关方法和调度的更多信息,请参阅章节 方法)。 因此,仅依赖于第一个参数的函数是错误的。 将方法组织为函数对象,而不是在每个对象中命名的方法集,这是语言设计的一个非常有用的特性。

复合类型使用关键字声明 'struct',后跟一个通过`::`运算符具有可选类型注释的字段名称块。

julia> struct Foo
           bar
           baz::Int
           qux::Float64
       end

没有类型注释的字段默认为"Any"类型,因此可以包含任何类型的值。 `Foo’类型的新对象是通过将`Foo’类型作为函数应用于字段值来创建的。

julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)

julia> typeof(foo)
Foo

当一个类型用作函数时,它被称为_constructor_。 自动创建两个构造函数(它们被default_称为_constructors)。 其中一个接受任何参数并调用函数 'convert'将它们转换为字段类型,另一个接受与字段类型完全匹配的参数。 创建这两个构造函数是为了使添加新定义更容易,而不会意外替换默认构造函数。

由于`bar`字段的类型是无限的,因此任何值都可以。 但是,baz`的值必须是这样的,以便它可以转换为`Int

julia> Foo((), 23.5, 1)
ERROR: InexactError: Int64(23.5)
Stacktrace:
[...]

可以使用函数获取字段名称列表 '字段名'

julia> fieldnames(Foo)
(:bar, :baz, :qux)

复合对象的字段值可以使用普遍接受的符号`foo访问。酒吧'。

julia> foo.bar
"Hello, world."

julia> foo.baz
23

julia> foo.qux
1.5

使用关键字`struct’声明的复合对象不是change_:它们在创建后不能更改。 起初可能看起来很奇怪,但这种方法有几个优点。

  • 它可以更有效。 一些结构可以有效地打包到数组中,在某些情况下,编译器可以完全免除为不可变对象分配内存。

  • 不可能违反类型构造函数提供的不变量。

  • 使用不可变对象的代码可能更容易理解。

不可变对象的字段可以包含可变对象,例如数组。 这样的嵌套对象保持可变:您不能只更改不可变对象本身的字段,以便它们指向其他对象。

如有必要,可以使用关键字声明可变复合对象。 '可变结构'。 这将在下一节中讨论。

如果不可变结构的所有字段彼此不可区分’===`),那么具有此类字段的两个不可变值也是不可区分的。

julia> struct X
           a::Int
           b::Float64
       end

julia> X(1, 2) === X(1, 2)
true

创建复合类型的实例具有许多其他重要功能,但是,它们与 参数类型方法,因此将他们的讨论放在单独的章节中是有意义的。: 构造函数

对于许多自定义’X’类型,您可能需要定义一个方法 '基地。broadcastable(x::X)=Ref(x)`,以便类型的实例充当0维"标量",用于 广播

可变复合类型

如果使用关键字`mutable struct`而不是`struct’声明复合类型,则可以修改其实例。

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中的不变性有两个主要特征。

  • 不能更改不可变类型的值。 对于位类型,这意味着值的位模式在分配后永远不会改变,并且该值允许识别位类型。 对于复合类型,这意味着字段值的标识保持不变。 如果字段是位类型,则意味着它们的位永远不会改变。 如果它们属于可变类型,例如数组,这意味着该字段将始终引用相同的可变值,尽管该值的内容可能会发生变化。

  • 不可变类型的对象可以由编译器自由复制,因为由于不可变性,源对象和副本对于程序来说是不可区分的。 特别是,这意味着相当小的不可变值,如整数和浮点,通常通过寄存器传递给函数(或放置在堆栈上)。 反过来,可变值被放置在堆上,并且指向堆上这些值的指针被传递给函数,除非编译器知道不可能确定相反的情况。

如果已知只有可变结构的某些字段是不可变的,则可以使用关键字`const`声明这些字段,如下所示。 这为不可变结构提供了许多优化,并允许您将固定条件应用于标记为"const"的某些字段。

兼容性:Julia1.8

要将"const"注释应用于可变结构的字段,它需要至少Julia1.8的版本。

julia> mutable struct Baz
           a::Int
           const b::Float64
       end

julia> baz = Baz(1, 1.5);

julia> baz.a = 2
2

julia> baz.b = 2.0
ERROR: setfield!: const field .b of type Baz cannot be changed
[...]

声明的类型

前面几节讨论的三类类型(抽象、基元和复合)实际上是密切相关的。 它们都有共同的重要特性。

  • 它们是明确声明的。

  • 他们有名字。

  • 他们已经明确声明了超类型。

  • 它们可能有参数。

由于这些公共属性,所有这些类型在内部表示为`DataType’的实例-所有这些类型所属的类型。

julia> typeof(Real)
DataType

julia> typeof(Int)
DataType

'DataType’类型可以是抽象的,也可以是具体的。 如果它是特定的,它具有特定的大小,内存布局和(可选)字段名称。 因此,基元类型是非零大小的’数据类型’类型,但没有字段名称。 复合类型是具有字段名称或空类型(零大小)的"数据类型"类型。

系统中的每个特定值都是某种类型的"数据类型"的实例。

组合类型

类型联合是一种特殊的抽象类型,它包括所有参数类型的所有实例作为对象。 它是使用特殊关键字创建的 '联盟'

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

许多语言的编译器提供了一个用于组合的内部结构,允许您定义类型;在Julia中,它可供程序员使用。 当存在包含少量脚注类型的"联合"联合时,Julia编译器可以生成高效的代码。:1["小数字"由`max_union_splitting`配置确定,当前默认值为4。]为此,在单独的分支中为每个可能的类型生成专门的代码。

'Union’类型的一个特别有用的应用是’Union{T, Nothing}`,其中’T’可以是任何类型,并且 'Nothing'是一个单一的类型,它的唯一实例是一个对象。 '没什么`。 Julia中的这样一个模板是等效的类型https://en.wikipedia.org/wiki/Nullable_type 其他语言中的[Nullable,`Option`或`Maybe']。 如果您将函数参数或字段声明为’Union{T, Nothing}',它们可以被分配像`T`或`nothing`这样的值,这意味着没有值。 有关详细信息,请参阅 在这个常见问题部分

参数类型

Julia类型系统的一个重要而有用的特性是它是参数化的:类型可以接受参数,因此在声明类型时,引入了整个新类型家族,每个可能的参数值组合一个。 在许多语言中,它以一种或另一种形式支持https://en.wikipedia.org/wiki/Generic_programming [通用编程],它允许您定义数据结构和算法来处理它们,而无需指定确切的类型。 例如,通用编程在ML,Haskell,Ada,Eiffel,C中以这种或那种方式实现++、Java、C#、F#、Scala等语言。 其中一些语言(例如ML,Haskell,Scala)支持真正的参数化多态性,而其他语言(例如C++ Java)--基于模板的通用编程的特殊样式。 由于通用编程和参数类型的各种方法,我们甚至不会尝试在这方面将Julia与其他语言进行比较。 相反,我们将专注于在Julia proper中查看这个系统。 但是,我们注意到静态参数类型系统的许多典型困难可以相对容易地克服,因为Julia是一种动态类型语言,不需要在编译时定义所有类型。

所有声明的类型(`DataType’的品种)都可以使用相同的语法进行参数化。 我们将按以下顺序考虑它们:首先是参数化复合类型,然后是参数化抽象类型,最后是参数化基元类型。

参数化复合类型

类型参数是在花括号中的名称后立即指定的。

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

因此,定义了一个新的参数类型'Point'。{T}'包含两个类型为`T’的坐标。 问题可能会出现:`T’到底是什么? 这正是参数类型的本质。:它可以是任何类型(或任何位类型的值,尽管在这种情况下很明显它是一种类型)。 '+点{Float64}+'是一个特定的类型,相当于如果我们在`Point`的定义中用`T’替换’T’就会得到的类型。 'Float64'。 因此,使用单个定义,实际上声明了无限数量的类型’Point{Float64}','点{AbstractString}','点{Int64}'等等。 它们中的每一个现在都可以作为一个特定的。

julia> Point{Float64}
Point{Float64}

julia> Point{AbstractString}
Point{AbstractString}

'点的类型{Float64}'表示坐标为64位浮点值的点,类型为'Point{AbstractString}'--坐标为字符串对象的点(参见部分 线)。

就其本身而言,类型`Point’也是类型的有效对象,所有`Point'的实例都是子类型。{Float64}','点{AbstractString}'等等。

julia> Point{Float64} <: Point
true

julia> Point{AbstractString} <: Point
true

其他类型当然不是它的亚型。

julia> Float64 <: Point
false

julia> AbstractString <: Point
false

具有不同值的"T"的特定类型的"点"从不是彼此的子类型。

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

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

最后一点非常重要:尽管表达式`Float64<:Real`是真的,但在`Point的情况下{Float64} <:点{Real}'这是*不是真的*。

换句话说,就类型理论而言,Julia的类型参数是不变的,而不是https://en.wikipedia.org/wiki/Covariance_and_contravariance_%28computer_science%29 [协变(或逆变)]。 对此有一个实际的解释:虽然`Point的实例{Float64}'本质上类似于`Point的实例{Real}`,这两种类型在内存中有不同的表示。

  • `点的实例{Float64}`可以紧凑且高效地表示为并排存储的一对64位值。

  • 到`点的实例{Real}'必须放置类型的任何一对实例 '真实`。 由于作为"Real"实例的对象可以具有任意大小和结构,因此实际上实例是"Point"。{Real}'必须由一对指针来表示,以分隔内存中的"真实"对象。

存储"点"值带来的效率优势{Float64}'内存中的附近在数组的情况下更加明显:'Array{Float64}`可以作为64位浮点值的连续块存储在内存中,而'Array的实例{Real}'必须是指向单个对象的指针数组 `真实'在记忆中-它可以像https://en.wikipedia.org/wiki/Object_type_%28object-oriented_programming%29#Boxing [打包]64位浮点值,以及作为抽象类型`Real’的实现声明的任意大小的复杂对象。

自'点{Float64}不是'Point的子类型{Real},下面的方法不能应用于类型为'Point的参数{Float64}`.

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

定义接受`Point类型的所有参数的方法的正确方法{T}',其中’T’是子类型 `Real',看起来像这样:

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

(同样,可以定义'function norm(p::Point{T} 其中T<:Real)`或'函数范数(p::点{T})其中T<:Real';见部分 unionAll类型。)

其他示例将在本章后面给出。 方法

如何创建一个’点’对象? 您可以为复合类型定义自定义构造函数;这将在本章中详细讨论 构造函数。 在没有特殊构造函数声明的情况下,有两种标准的方法来创建复合对象:使用显式类型参数和基于传递给对象构造函数的参数的输出。

自'点{Float64}'是一个特定的类型,相当于类型’点`,声明与指示 'Float64`代替`T',它可以用作构造函数。

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

julia> typeof(p)
Point{Float64}

使用默认构造函数时,必须为每个字段传递一个参数。

julia> Point{Float64}(1.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64)
The type `Point{Float64}` exists, but no method is defined for this combination of argument types when trying to construct it.
[...]

julia> Point{Float64}(1.0, 2.0, 3.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)
The type `Point{Float64}` exists, but no method is defined for this combination of argument types when trying to construct it.
[...]

对于参数类型,只创建一个默认构造函数,因为它不能被复盖。 此构造函数接受任何参数并将其转换为字段类型。

通常没有必要指定要创建的’Point’对象的类型,因为它是根据构造函数调用中传递的参数类型隐式确定的。 因此,您可以使用`Point`类型本身作为构造函数,前提是`T`参数类型的隐含值是明确的。

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

julia> typeof(p1)
Point{Float64}

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

julia> typeof(p2)
Point{Int64}

在’Point`的情况下,当且仅当`Point`的两个参数具有相同的类型时,隐含类型`T`是明确的。 如果不是这种情况,构造函数将抛出异常。 'MethodError'

julia> Point(1,2.5)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
The type `Point` exists, but no method is defined for this combination of argument types when trying to construct it.

Closest candidates are:
  Point(::T, !Matched::T) where T
   @ Main none:2

[医]堆垛机:
[...]

要正确处理这种混合情况,可以定义特殊的构造函数方法,但这将在本章后面讨论。 构造函数

参数化抽象类型

声明参数化抽象类型时,抽象类型集合的声明方式大致相同。

julia> abstract type Pointy{T} end

有了这个声明,'Pointy{T}'表示每个类型或`T’的整数值的单独抽象类型。 就像在参数化复合类型的情况下一样,每个这样的实例都是`Pointy’的子类型。

julia> Pointy{Int64} <: Pointy
true

julia> Pointy{1} <: Pointy
true

参数化抽象类型,如复合类型,是不变的。

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

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

"尖"符号{<:Real}'允许您在Julia中表达_variant_类型的模拟,并且表示法’Pointy{>:Int}'--是逆变类型的类似物,但形式上它们表示类型集(参见部分 unionAll类型)。

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

在这样的广告的情况下,'点{T}'是'Pointy的子类型{T}'为`T’的每个值。

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

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

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

关系也是不变的。

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

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

什么是像`Pointy`这样的参数化抽象类型? 假设我们创建了一个只需要一个坐标的point对象的实现,因为该点位于对角线_x=y_上。

julia> struct DiagPoint{T} <: Pointy{T}
           x::T
       end

现在和'点{Float64},和'DiagPoint{Float64}'是抽象的实现'Pointy{Float64}. 对于类型`T’的其他可能变体也是如此。 这允许您为所有`Pointy`对象编程一个通用接口,为`Point`和`DiagPoint’实现。 但是,目前还无法充分展示这个概念,因为方法和调度将只在下一章中讨论。 方法

在某些情况下,类型参数不应将所有可能的类型作为值,因为其中一些类型没有意义。 在这种情况下,可以如下限制`T`的可能值的集合。

julia> abstract type Pointy{T<:Real} end

使用这样的声明,可以使用任何子类型的类型来代替’T'。 'Real`,但不是其他类型是`Real’的子类型。

julia> Pointy{Float64}
Pointy{Float64}

julia> Pointy{Real}
Pointy{Real}

朱莉娅>尖尖的{AbstractString}
ERROR:TypeError:in Pointy,in T,expected T<:Real,got Type{AbstractString}

朱莉娅>尖尖的{1}
ERROR:TypeError:in Pointy,in T,expected T<:Real,get a value of type Int64

参数复合类型的参数可以以相同的方式限制。

struct Point{T<:Real} <: Pointy{T}
    x::T
    y::T
end

作为参数类型有用使用的一个例子,这里是一个不可变类型的真正定义。 'Rational',表示整数分数的确切值(为了简单起见,省略了构造函数)。

struct Rational{T<:Integer} <: Real
    num::T
    den::T
end

带有分数的运算仅对整数值有意义,因此`T’参数的值仅限于子类型。 'Integer',并且两个整数的商是实数轴上的值,因此任何值 'Rational'是抽象的一个实例 '真实`

元组的类型

元组是没有函数本身的函数参数的抽象。 函数参数的基本方面是它们的顺序和类型。 因此,元组类型类似于参数化的不可变类型,其每个参数都是单个字段的类型。 例如,由两个元素组成的元组的类型类似于以下不可变类型。

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

但是,有三个主要区别。

  • 元组类型可以有任意数量的参数。

  • 元组类型在其参数中是_variant_’元组{Int}'是'元组的子类型{Any}`. 因此,'元组{Any}'被认为是抽象类型,元组类型只有在它们的参数是这样的情况下才是具体的。

  • 元组没有字段名称;字段只能由索引访问。

元组值写在用逗号分隔的括号中。 创建元组时,按需生成相应的元组类型。

julia> typeof((1,"foo",2.5))
Tuple{Int64, String, Float64}

注意协方差的后果。

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}}

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参数的元组类型用于表示具有可变数量参数的方法所接受的参数(请参阅部分 参数数量可变的函数(Vararg))。

'Vararg的特殊值{T,N}`(用作最后一个元组类型参数)恰好对应于`T`类型的`N’元素。 'NTuple{N,T}`--这是`Tuple的方便别名{Vararg{T,N}}`,即完全包含`T`类型的`N’元素的元组的类型。

命名元组类型

命名元组是类型的实例 'NamedTuple',它有两个参数:具有字段名称的字符元组和具有字段类型的元组类型。 为了方便起见,'NamedTuple’类型使用宏输出 '@NamedTuple',它提供了更方便的结构样式语法('struct`),用于通过`key::Type`声明声明这些类型,其中缺少的`::Type`对应于`::Any`。

julia> typeof((a=1,b="hello")) # выводит в виде макроса
@NamedTuple{a::Int64, b::String}

julia> NamedTuple{(:a, :b), Tuple{Int64, String}} # длинная форма типа
@NamedTuple{a::Int64, b::String}

'开始。.. `@NamedTuple`宏的end`形式允许您将声明拆分为几行(就像struct声明的情况一样)。 但除此之外,它相当于以下内容:

julia> @NamedTuple begin
           a::Int
           b::String
       end
@NamedTuple{a::Int64, b::String}

'NamedTuple’类型可以用作接受单个参数作为元组的构造函数。 正在创建的’NamedTuple’类型可以是指定两个参数的特定类型,也可以是仅具有字段名称的类型。

julia> @NamedTuple{a::Float32,b::String}((1, ""))
(a = 1.0f0, b = "")

julia> NamedTuple{(:a, :b)}((1, ""))
(a = 1, b = "")

如果指定了字段类型,则会转换参数。 否则,直接使用参数类型。

参数基元类型

基元类型也可以参数化声明。 例如,指针表示为可以在Julia中声明的基本类型,如下所示。

# 32-разрядная система:
primitive type Ptr{T} 32 end

# 64-разрядная система:
primitive type Ptr{T} 64 end

与通常的参数复合类型相比,这些声明的一个小奇怪之处在于类型参数`T`在类型本身的定义中没有使用-它只是一个抽象标签,定义了具有相同结构的 因此’Ptr{Float64}'和'Ptr{Int64}'是单独的类型,尽管它们具有相同的表示。 当然,所有单独的指针类型都是通用类型的子类型。 'Ptr'

julia> Ptr{Float64} <: Ptr
true

朱莉娅>Ptr{Int64} <:Ptr
真的

UnionAll的类型

我们已经注意到,参数类型,如`Ptr`,作为其所有实例的超类型('Ptr{Int64}等。). 它是如何工作的? 就其本身而言,`Ptr’类型不能是普通数据类型:如果相应数据的类型未知,则不能用于内存操作。 解释是’Ptr'(或其他参数类型,如’Array)是一种特殊的类型,称为 'UnionAll'。 此类型意味着某个参数的所有值的类型的可迭代组合。

"UnionAll"的类型通常使用关键字"where"编写。 例如,'Ptr’可以更准确地写为'Ptr{T} 其中T`,它对应于类型'Ptr的所有值{T}'对于`T’的一些值。 在这种情况下,参数`T’通常也被称为类型变量,因为它作为一个变量,其值可以是一组特定的类型。 每个`where’关键字都会引入一个类型变量,因此对于具有多个参数的类型可以嵌套这样的表达式,例如’Array{T,N} 在哪里N在哪里T'。

使用类型’A的语法{B,C}'要求’a`是’unionAll’类型,并且第一个’B`被替换为`A’中的外部类型变量。 结果应该是一个不同类型的’unionAll',然后用’C’替换。 因此’A{B,C}'相当于'A{B}{C}. 这就解释了为什么像Array'这样的类型的部分实例化是可能的。{Float64}`:第一个参数的值是固定的,但第二个参数仍然接受所有可能的值。 当使用"where"语法时,任何参数子集都可以显式固定。 例如,所有一维数组的类型可以写为’Array{T,1} 在哪里T'。

类型变量可以通过子类型关系进行限制。 '数组{T} 其中T<:Integer'表示元素类型为某些子类型的所有数组。 '整数'。 对于句法构造'数组{T} 其中T<:Integer'有一个方便的简短写法:'Array{<:Integer}`. 类型的变量可以有下限和上限。 '+数组{T} 其中Int<:T<:Number+'表示所有元素数组 'Number',它可以包含’Int'(T`的大小必须至少是`Int)。 使用’where T>语法:Int’也可以仅指定类型变量的下界和’Array'{>:Int}'等价于'数组{T} 其中T>:Int`。

由于’where’表达式可以嵌套,因此类型变量的边界可以与外部类型变量相关。 例如,'元组{T,Array{S}}其中S<:AbstractArray{T} 其中T<:Real'表示双元素元组,其中第一个元素属于某个子类型 'Real',并且第二个元素是任何数组,其元素是元组的第一个元素的类型。

"Where"关键字本身可以嵌入更复杂的广告中。 例如,考虑通过以下声明创建的两种类型。

julia> const T1 = Array{Array{T, 1} where T, 1}
Vector{Vector} (alias for Array{Array{T, 1} where T, 1})

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

类型’T1’定义了一维数组的一维数组;每个内部数组由相同类型的对象组成,但内部数组中的这些类型的对象可能不同。 反过来,类型’T2’定义一维数组的一维数组,其中所有内部数组的对象属于同一类型。 请注意’T2’是抽象类型(例如,'Array{Array{Int,1},1}<:T2`),而`T1`是特定的。 出于这个原因,可以使用不带参数的构造函数(a=T1())创建`T1`的实例,但`T2`的实例不能。

有一个方便的语法来命名这些类型,类似于定义函数的简短形式。

Vector{T} = Array{T, 1}

这相当于’const Vector=Array{T,1} 在哪里T'。 记录'向量{Float64}'相当于’数组{Float64,1}',和一般类型’Vector’的实例都是’Array`的对象,其中第二个参数-数组的维数-是1,而不管元素的类型如何。 在参数类型必须始终完整指定的语言中,这并不是特别有用,但在Julia中,由于这一点,您可以简单地编写一个`Vector`来定义一个抽象类型,其中包括所有具有任

单一类型

没有字段的不可变复合类型称为单一类型。 正式地说,如果

  1. 'T’是一个不可变的复合类型(使用关键字`struct’定义)和

  2. 从’a isa T&&b isa T`跟随’a===b`,

"T"是单一类型。脚注:2[单一类型用于许多流行的语言,包括Haskell,Scala和Ruby。]要检查类型是否单一,请使用函数 '基地。issingletontype'抽象类型不能以其性质为单数。

根据定义,单个类型只能有一个实例。

julia> struct NoFields
       end

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

julia> Base.issingletontype(NoFields)
true

功能 `==='确认创建的’NoFields’实例彼此相同。

如果满足上述条件,参数类型可以是单一的。 例子::

julia> struct NoFieldsParam{T}
       end

julia> Base.issingletontype(NoFieldsParam) # Не может быть одинарным типом...
false

julia> NoFieldsParam{Int}() isa NoFieldsParam # ...так как имеет...
true

julia> NoFieldsParam{Bool}() isa NoFieldsParam # ...несколько экземпляров.
true

julia> Base.issingletontype(NoFieldsParam{Int}) # В параметризованной форме является одинарным.
true

julia> NoFieldsParam{Int}() === NoFieldsParam{Int}()
true

函数类型

每个函数都有自己的类型,这是`Function’的子类型。

julia> foo41(x) = x + 1
foo41 (generic function with 1 method)

julia> typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)

请注意,调用的输出是"typeof(foo41)",类似于挑战本身。 这只是一个约定,因为这个对象是完全独立的,可以用作任何其他值。

julia> T = typeof(foo41)
typeof(foo41) (singleton type of function foo41, subtype of Function)

julia> T <: Function
true

顶层定义的函数类型是单一的。 如有必要,可以使用操作员对它们进行比较 ===.

闭包也有自己的类型,其名称通常以`#<number>`结尾输出。 在不同地方定义的函数具有唯一的名称和类型,并且输出可能因会话而异。

julia> typeof(x -> x + 1)
var"#9#10"

闭包类型不一定是单一的。

julia> addy(y) = x -> x + y
addy (generic function with 1 method)

julia> typeof(addy(1)) === typeof(addy(2))
true

julia> addy(1) === addy(2)
false

julia> Base.issingletontype(typeof(addy(1)))
false

类型选择器'类型{T}`

对于每种类型’T''类型{T}'是一个抽象的参数类型,其中唯一的实例是对象’T'。 他们还没有被审查 参数化方法transformations,这种构造的目的很难解释,但是,简而言之,它允许您将特定类型的函数行为专门化为_values。 这对于编写行为取决于作为参数显式传递的类型的方法(特别是参数方法)非常有用,而不是从其中一个参数的类型派生。

由于这个定义可能有点难以理解,让我们看看一些例子。

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,型{B})`当且仅当`A`和`B’是同一个对象并且这个对象是一个类型时才为true。

特别是,由于参数类型 不变:

julia> struct TypeParamExample{T}
           x::T
       end

julia> TypeParamExample isa Type{TypeParamExample}
true

julia> TypeParamExample{Int} isa Type{TypeParamExample}
false

julia> TypeParamExample{Int} isa Type{TypeParamExample{Int}}
true

没有’Type’参数,它只是一个抽象类型,其中所有类型的对象都是实例。

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

朱莉娅>isa(Float64,类型)
真的

julia>isa(真实,类型)
真的

类型以外的任何对象都不是’Type’的实例。

julia> isa(1, Type)
false

julia> isa("foo", Type)
false

尽管’Type’是Julia类型层次结构的一部分,就像任何其他抽象参数类型一样,它通常不会在方法签名之外使用,除非在一些特殊情况下。 'Type’的另一个重要用途是指定否则不太准确的字段类型,例如 'DataType'在下面的例子中。 在这种情况下,默认构造函数可能会导致使用外壳中包含的确切类型的代码的性能问题(类似地 抽象类型的参数)。

julia> struct WrapType{T}
       value::T
       end

julia> WrapType(Float64) # Конструктор по умолчанию, обратите внимание на DataType
WrapType{DataType}(Float64)

julia> WrapType(::Type{T}) where T = WrapType{Type{T}}(T)
WrapType

julia> WrapType(Float64) # Специализированный конструктор, обратите внимание на более точный тип Type{Float64}
WrapType{Type{Float64}}(Float64)

类型别名

有时为已经可表达的类型输入新名称很方便。 这可以使用简单的赋值运算符来完成。 例如,`UInt’的别名可以是 'UInt32''UInt64'取决于系统中指针的大小。

# 32-разрядная система:
julia> UInt
UInt32

# 64-разрядная система:
julia> UInt
UInt64

为此,在’base/boot.jl’使用以下代码。

if Int === Int64
    const UInt = UInt64
else
    const UInt = UInt32
end

当然,这取决于别名是为哪种类型的`Int`创建的,但它必须是正确的类型:要么 'Int32',或 'Int64'

(请注意,与`Int’不同’Float’不是类型的别名。 `AbstractFloat'。 与整数寄存器不同,"Int"的大小对应于系统中机器指针的大小,浮点寄存器的大小在IEEE-754标准中定义。)

类型别名可以参数化:

julia> const Family{T} = Set{T}
Set

julia> Family{Char} === Set{Char}
true

具有类型的操作

由于Julia中的类型本身就是对象,所以普通函数可以用它们执行操作。 您已经熟悉了一些旨在处理类型或分析它们的函数。 其中包括运算符'<’,表示左操作数是右操作数的子类型。

功能 `isa'检查对象是否属于某个类型并返回true或false。

julia> isa(1, Int)
true

julia> isa(1, AbstractFloat)
false

功能 typeof返回其参数的类型。 由于如前所述,类型是对象,因此它们本身具有类型并且可以识别。

julia> typeof(Rational{Int})
DataType

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

但如果我们重复这个操作呢? Type类型属于什么类型? 事实证明,所有类型都是复合值,因此具有类型’DataType'。

julia> typeof(DataType)
DataType

julia> typeof(Union)
DataType

'数据类型’是自己的类型。

适用于某些类型的另一种操作是 '超类型'。 它定义了类型的超类型。 只有声明的类型('DataType`)具有唯一定义的超类型。

julia> supertype(Float64)
AbstractFloat

julia> supertype(Number)
Any

julia> supertype(AbstractString)
Any

julia> supertype(Any)
Any

应用函数时 'supertype'对于类型的其他对象(或非类型的对象)引发异常。 'MethodError'

julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
The function `supertype` exists, but no method is defined for this combination of argument types.

Closest candidates are:
[...]

可定制的结构打印输出

有时需要配置类型实例的显示。 为此,请重载函数 '显示'。 例如,假设我们定义了一种用于以极坐标形式表示复数的类型。

julia> struct Polar{T<:Real} <: Number
           r::T
           Θ::T
       end

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

在这里,我们添加了一个自定义构造函数,可以接受不同类型的参数。 'Real`并将它们推广为一般类型(见章节 构造函数转型和提升)。 (当然,我们还必须定义许多其他方法,以便类型可以用作 '数字',例如``, `*`, ` 一`,'零`,促销规则等。)默认情况下,这种类型的实例显示得非常简单:显示类型名称和字段值,例如'+Polar{Float64}(3.0,4.0)`.

如果我们希望信息输出为"3.0*exp(4.0im)",我们将定义以下方法将信息输出到指定的输出对象"io"(表示文件、终端、缓冲区等)。;见部分 网络和流媒体)。

julia> Base.show(io::IO, z::Polar) = print(io, z.r, " * exp(", z.Θ, "im)")

对极地物体的显示进行更详细的控制也是可能的。 特别是,有时您既需要用于在REPL和其他交互式环境中显示有关单个对象的信息的详细多行格式,也需要用于函数的较短单行格式。 'print'或将对象显示为另一个对象(例如数组)的一部分。 虽然在这两种情况下都默认调用`show(io,z)`函数,但您可以使用带有三个参数的`show`函数重载定义另一种多行格式来显示对象,其中第二个参数是MIME类型’text/plain'( 多媒体数据输入/输出)。

julia> Base.show(io::IO, ::MIME"text/plain", z::Polar{T}) where{T} =
           print(io, "Polar{$T} complex number:\n   ", z)

(请注意,通过使用'print(。..,z)`,这里用两个参数调用`show(io,z)`方法。)结果如下:

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 Vector{Polar{Float64}}:
 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',除非您定义了用于显示多媒体数据的新处理程序(请参阅部分 多媒体数据输入/输出)。

此外,您还可以为其他MIME类型定义`show`方法,以确保有关具有更复杂格式(HTML标记,图像等)的对象的信息。)显示在支持此功能的环境中(例如,IJulia)。 例如,您可以使用上标和斜体定义HTML格式的"Polar"对象信息的显示,如下所示:

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

因此,在支持HTML的环境中,有关`Polar`对象的信息将使用HTML自动显示,但是,如果需要,您可以手动调用`show`函数以获取HTML格式的输出数据。

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_

作为一般规则,单行’show’方法应该输出一个有效的Julia表达式来创建显示的对象。 如果此’show’方法包含中缀运算符,例如乘法运算符(*) 在上面的`Polar`类型的单行`show`方法中,当作为另一个对象的一部分输出时,它可能会被错误地分析。 为了说明这一点,让我们采用一个表达式对象(参见部分 Program Representation),它是`Polar’类型的特定实例。

julia> a = Polar(3, 4.0)
Polar{Float64} complex number:
   3.0 * exp(4.0im)

julia>打印(:($a^2))
3.0*exp(4.0im)^2

由于运算符'^'优先于`*` ( 请参阅部分 运算符优先级和关联性),此输出错误地表示表达式`a^2,该表达式应对应于表达式(3.0*exp(4.0im))^2。 要解决此问题,您需要为`Base创建自定义方法。show_unquoted(io::IO,z::Polar,indent::Int,priority::Int),在信息输出时会被expression对象自动调用。

julia> function Base.show_unquoted(io::IO, z::Polar, ::Int, precedence::Int)
           if Base.operator_precedence(:*) <= precedence
               print(io, "(")
               show(io, z)
               print(io, ")")
           else
               show(io, z)
           end
       end

julia> :($a^2)
:((3.0 * exp(4.0im)) ^ 2)

上面定义的方法在调用运算符的优先级高于或等于乘法优先级时将`show`调用括在括号中。 此检查允许您在输出正确解析的表达式时省略括号(例如``:($a+2):($a==2)')。

julia> :($a + 2)
:(3.0 * exp(4.0im) + 2)

julia> :($a == 2)
:(3.0 * exp(4.0im) == 2)

在某些情况下,根据上下文自定义"show"方法的行为可能很有用。 为此,您可以使用类型 'IOContext',它允许将上下文属性与包装的I/O流一起传递。 例如,当`:compact`属性具有值`true`时,`show`方法可以产生更简洁的表示,或者当此属性具有值`false’或未设置时产生完整的表示。

julia> function Base.show(io::IO, z::Polar)
           if get(io, :compact, false)::Bool
               print(io, z.r, "ℯ", z.Θ, "im")
           else
               print(io, z.r, " * exp(", z.Θ, "im)")
           end
       end

当传输的I/O流是具有指定`:compact`属性的`IOContext`对象时,将使用此新的简短表示。 特别是,当输出具有多个列的数组时(当宽度受到限制时)会发生这种情况。

julia> show(IOContext(stdout, :compact=>true), Polar(3, 4.0))
3.0ℯ4.0im

julia> [Polar(3, 4.0) Polar(4.0,5.3)]
1×2 Matrix{Polar{Float64}}:
 3.0ℯ4.0im  4.0ℯ5.3im

在文档中 'IOContext'提供可用于自定义输出的常用属性列表。

值的类型

在Julia中,不可能通过_value进行调度,例如`true`或`false`。 但是,参数类型调度是可能的,Julia允许您包含简单的位值(类型,字符,整数,浮点数,元组等)。)作为类型参数。 一个常见的例子是`数组中的维度参数{T,N}`,其中’T’是类型(例如, 'Float64'),但’N’只是一个`Int'。

您可以创建自己的自定义类型,这些类型将值作为参数,并使用它们来管理自定义类型的调度。 为了说明这个概念,我们引入参数类型'Val{x}'及其构造函数'Val(x)=Val{x}()`,它允许您在不需要更发达的层次结构的情况下使用此技术。

'Val'定义如下:

julia> struct Val{x}
       end

julia> Val(x) = Val{x}()
Val

'Val’的实现仅限于此。 Julia标准库中的一些函数接受`Val’的实例作为参数,您可以使用此类型编写自己的函数。 例如:

julia> firstlast(::Val{true}) = "First"
firstlast (generic function with 1 method)

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

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

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

为了一致性,在Julia中,应该始终在调用位置传递_example_`Val`,而不是_type_,即应该使用’foo(val(:bar)),而不是’foo(Val{:bar}).

值得注意的是,包括`Val`在内的参数值类型非常容易错误地使用,并且在最坏的情况下,代码性能会显着下降。 特别是,您不应该编写如上所示的代码。 有关"Val"的适当(和不适当)应用的更多信息,请参阅 性能提示