在本文中,我们将考虑向Julia程序中添加自定义数据类型,并重载标准函数,以方便使用新类型。
什么是自定义类型
一个可怕的秘密-Julia中“用户”和“内置”数据类型之间的界线实际上不存在。 在任何Julia程序中,您都可以定义自己的数据类型,并且在编写代码或执行此代码的速度方面,使用内置数据与使用内置数据几乎没有什么不同(您好,Python)。 输入数据类型可以是原始数据或复合数据。 后者也分为抽象和具体类型。
此外,我们将考虑复合混凝土类型的示例,因为 内存中的特定对象是特定类型,抽象对象仅用于构建层次结构。
复合类型由关键字struct
和mutable struct
定义。 在第一种情况下,将创建一个不可变的类型:
Point
类型的对象Point
不可变的,即 一旦创建的对象中的字段值不能更改。
julia> p = Point(2, 3) Point(2, 3) julia> px 2 julia> py 3 julia> px = 4 ERROR: setfield! immutable struct of type Point cannot be changed
但是您可以创建一个可变类型:
mutable struct MutablePoint x y end julia> mp = MutablePoint(3, 4) MutablePoint(3, 4) julia> mp.y = 2 2 julia> mp MutablePoint(3, 2)
为了提高效率,可以将类型注释添加到结构字段和结构本身。 例如:
struct ParametricPoint{T<:Real} x::T y::T end
这意味着类型ParametricPoint
由类型T
参数化,并且字段x
和y
现在应该是相同类型T
值T
该行为将如下所示:
julia> ParametricPoint(1, 1) ParametricPoint{Int64}(1, 1) julia> ParametricPoint{Float64}(1, 1) ParametricPoint{Float64}(1.0, 1.0) julia> ParametricPoint{Rational}(1, 1) ParametricPoint{Rational}(1//1, 1//1)
接下来,让我们看一个稍微复杂一些的容器类型示例,以查看标准库中用于处理此类类型的方法。
两路排队
我们将从DequeNode
类型的节点创建一个双向队列,该队列将包含数据记录以及指向该队列的上一个和下一个元素的链接。 DequeNode
类型必须是可变的,因为 链接将需要重写。
mutable struct DequeNode{T} data::T prev::DequeNode{T} next::DequeNode{T}
在这种情况下,需要内部构造函数,因为默认情况下,在构造对象时,您需要将所有字段的值与参数一起传递。 由于类型是自引用的,因此标准构造函数无法创建此类型的第一个对象。 因此,我们使用特殊的构造函数来创建引用自身的队列节点。
队列本身可以表示为不包含任何数据的虚拟“输入”节点。 它的next
链接将指向队列的第一个元素,而上next
则指向最后一个。 对于空队列,两个链接都指向输入节点。 这样的组织简化了添加和删除节点的过程。
struct Deque{T} entry::DequeNode{T} end Deque{T}() where T = Deque{T}(DequeNode{T}()) Deque() = Deque{Any}()
队列本身的结构不再需要内部构造函数。
新增项目
队列的标准过程是在开始和结束处添加元素,并从开始和结束处提取元素。 对于这些过程,标准库已经具有一些函数,您只需要向这些函数添加用于其他类型的方法:
push!(collection, element)
-向集合中添加元素(对于有序集合-最后)pushfirst!(collection, element)
-将元素添加到有序集合的开头pop!(collection)
-从集合中删除项目(对于有序集合-从最后开始)popfirst!(collection)
-从有序集合中删除第一项
所有这些功能都在Base
模块中,您需要向其添加新方法。 另外,我们将编写一个函数来检查集合是否不为空Base.isempty(collection)
。
Base.isempty(deq::Deque) = deq.entry.prev ≡ deq.entry function Base.push!(deq::Deque{T}, elt) where T tail = deq.entry.prev new_item = DequeNode{T}(elt) new_item.prev, new_item.next = tail, deq.entry tail.next = deq.entry.prev = new_item return deq end function Base.pushfirst!(deq::Deque{T}, elt) where T head = deq.entry.next new_item = DequeNode{T}(elt) new_item.prev, new_item.next = deq.entry, head head.prev = deq.entry.next = new_item return deq end function Base.pop!(deq::Deque) !isempty(deq) || throw(ArgumentError("deque must be non-empty")) last = deq.entry.prev last.prev.next = deq.entry deq.entry.prev = last.prev return last.data end function Base.popfirst!(deq::Deque) !isempty(deq) || throw(ArgumentError("deque must be non-empty")) first = deq.entry.next first.next.prev = deq.entry deq.entry.next = first.next return first.data end
push!()
函数使得基于任意集合编写队列构造函数几乎变得微不足道:
function Deque(itr) d = Deque{eltype(itr)}() for elt in itr push!(d, elt) end return d end
现在,如果我们只需要添加和删除操作,并且不需要像peek()
这样的函数(请查看队列开头或结尾的元素而不删除它),则几乎可以将新类型用作队列。 对于元素的非破坏性访问,在容器上定义迭代函数很方便。
迭代
Julia支持遍历容器元素的迭代器的概念。 对于迭代,您需要定义一个函数iterate(container, state)
,其中state
确定迭代器的当前状态。 特别地, for x in collection
构造是类似以下内容的语法糖:
let nextstate = iterate(collection) while nextstate ≢ nothing (x, state) = nextstate <- > nextstate = iterate(collection, state) end end
iterate()
函数将容器和“迭代器状态”作为参数,并从容器元素和下一个迭代器状态返回元组,如果容器已结束则不返回任何内容。
对于队列,将迭代达到的队列节点作为“状态”是合乎逻辑的:
function Base.iterate(deq::Deque{T}, state::DequeNode{T}=deq.entry) where T nextstate = state.next nextstate ≡ deq.entry ? nothing : (nextstate.data, nextstate) end
现在可以使用for
循环迭代队列的元素:
julia> for x in Deque(1:10) println(x); end 1 2 3 4 5 6 7 8 9 10
最大的优点是标准Julia库的许多功能都是使用通用接口(尤其是迭代)编写的。 因此,将自动确定是否自动检查元素是否属于容器:
julia> 5 ∈ Deque(3:10) true julia> 100 ∈ Deque(-5:5) false
我们还发现,返回集合的第一个元素的first()
方法开始起作用。 为了完成图片,我们补充了last()
方法以获得最后一个元素并以相反的顺序进行迭代:
function Base.iterate(r::Base.Iterators.Reverse{Deque{T}}, state::DequeNode{T}=r.itr.entry) where T nextstate = state.prev nextstate ≡ r.itr.entry ? nothing : (nextstate.data, nextstate) end function Base.last(deq::Deque) isempty(deq) && throw(ArgumentError("deque must be non-empty")) deq.entry.prev.data end julia> for x in Iterators.reverse(Deque(1:10)) println(x); end 10 9 8 7 6 5 4 3 2 1
因此,队列接口作为数据结构现已完全定义
打印输出结构
尽管队列可以完全用作数据结构,但在打印时,它仍然代表无形的混乱:
julia> Deque() Deque{Any}(DequeNode{Any}(
添加一种以更人性化的形式打印队列的方法。 结构到终端的输出由Base.show()
函数控制,我们将对其进行重载:
function Base.show(io::IO, deq::Deque{T}) where T print(io, "Deque{", T, "}(") next = iterate(deq) if next ≢ nothing item, state = next show(io, item) next = iterate(deq, state) while next ≢ nothing item, state = next print(io, ", ") show(io, item) next = iterate(deq, state) end end print(io, ")") end
按索引访问和更改
原则上,队列也可以用作按索引访问的容器。 通过索引检索和更改值由getindex()
和setindex!()
。 我们为队列实现了必要的方法。
function Base.getindex(deq::Deque, i::Integer) i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing throw(BoundsError(deq, i)) else return next[1] end end function Base.setindex!(deq::Deque, val, i::Integer) i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing throw(BoundsError(deq, i)) else record = next[2] record.data = val return val end end
将插入元素的功能添加到任意位置insert!()
并从任意位置删除元素也很有用:
function Base.insert!(deq::Deque{T}, i::Integer, val) where T i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing push!(deq, val) else record = next[2] new_node = DequeNode{T}(val) new_node.prev, new_node.next = record.prev, record record.prev = record.prev.next = new_node deq end end function Base.deleteat!(deq::Deque, i::Integer) i > 0 || throw(BoundsError(deq, i)) next = iterate(deq) for idx in 1:i-1 next ≡ nothing && throw(BoundsError(deq, i)) _, state = next next = iterate(deq, state) end if next ≡ nothing throw(BoundsError(deq, i)) else record = next[2] record.prev.next, record.next.prev = record.next, record.prev end end
其他
如前所述,标准库的许多函数都是使用迭代器编写的,这使它们可以自动使用定义了iterate()
函数的任何类型。 特别是,可以使用诸如map()
, collect()
函数以及诸如sum()
或minimum()
sum()
归约方法。
例如,可以这样编写查找队列长度的函数:
Base.length(deq::Deque) = sum(x->1, deq) julia> length(Deque(1, 1, 2, 3, 5, 8, 13)) 7
结论
如您所见,在Julia中创建自己的数据类型并使用它来组织方便的工作非常简单。 由于在标准库中使用了通用编程,因此通常可以定义几个基本功能,并且许多从属功能将自动运行。 因此,通过定义push!(container, element)
以添加单个元素,您可以发现push!(container, elements...)
将可以添加任意数量的参数。 定义了迭代器后,通常,我们会自动获取标准库的所有功能以用于抽象容器。
骇客入侵!