Spec-Zone.ru › Julia 0.7

Конструкторы

Конструкторы [1] — это функции, которые создают новые объекты — конкретно, экземпляры композитных типов. В Julia объекты типов также служат в качестве функций-конструкторов: они создают новые экземпляры самих себя при применении к кортежу аргументов как функции. Это уже кратко упоминалось при введении композитных типов. Например:

julia> struct Foo
           bar
           baz
       end

julia> foo = Foo(1, 2)
Foo(1, 2)

julia> foo.bar
1

julia> foo.baz
2

Для многих типов создание новых объектов путём связывания их значений полей — всё, что необходимо для создания экземпляров. Однако существуют случаи, когда при создании композитных объектов требуется больше функциональности. Иногда необходимо обеспечить соблюдение инвариантов, либо проверяя аргументы, либо преобразуя их. Рекурсивные структуры данных, особенно те, которые могут быть самоссылочными, часто не могут быть созданы без предварительного создания в неполном состоянии и последующего программного изменения для завершения как отдельного шага от создания объекта. Иногда просто удобно создавать объекты с меньшим или другим типом параметров, чем у них есть поля. Система Julia для создания объектов решает все эти и другие случаи.

[1]

Номенклатура: хотя термин «конструктор» обычно относится к всей функции, которая строит объекты типа, принято немного злоупотреблять терминологией и ссылаться на конкретные методы конструктора как на «конструкторы». В таких ситуациях из контекста обычно ясно, что термин используется для обозначения «метода конструктора», а не «функции-конструктора», особенно когда он часто используется в смысле выделения конкретного метода конструктора из всех остальных.

Внешние методы конструкторов

Конструктор подобен любой другой функции в Julia, поскольку его общее поведение определяется совокупным поведением его методов. Соответственно, вы можете добавить функциональность конструктору, просто определив новые методы. Например, предположим, что вы хотите добавить метод конструктора для Foo объектов, который принимает только один аргумент и использует данное значение для обоих полей bar и baz. Это просто:

julia> Foo(x) = Foo(x,x)
Foo

julia> Foo(1)
Foo(1, 1)

Вы также могли бы добавить метод конструктора Foo без аргументов, который предоставляет значения по умолчанию для обоих полей bar и baz.

julia> Foo() = Foo(0)
Foo

julia> Foo()
Foo(0, 0)

Здесь метод конструктора без аргументов вызывает метод конструктора с одним аргументом, который, в свою очередь, вызывает автоматически предоставленный метод конструктора с двумя аргументами. По причинам, которые станут ясными очень скоро, дополнительные методы конструкторов, объявленные как обычные методы таким образом, называются внешними методами конструкторов. Внешние методы конструкторов могут только создавать новый экземпляр, вызывая другой метод конструктора, например, автоматически предоставленные значения по умолчанию.

Внутренние методы конструкторов

В то время как внешние методы конструкторов успешно решают проблему предоставления дополнительных удобных методов для создания объектов, они не решают двух других задач, упомянутых во введении к этой главе: обеспечение соблюдения инвариантов и возможность создания самоссылочных объектов. Для этих задач требуются внутренние методы конструкторов. Внутренний метод конструктора очень похож на внешний метод конструктора, с двумя отличиями:

  1. Он объявляется внутри блока объявления типа, а не вне его, как обычные методы.
  2. Он имеет доступ к специальной локально существующей функции, называемой new, которая создаёт объекты типа блока.

Например, предположим, что нужно объявить тип, содержащий пару действительных чисел, с ограничением, что первое число не больше второго. Его можно объявить так:

julia> struct OrderedPair
           x::Real
           y::Real
           OrderedPair(x,y) = x > y ? error("out of order") : new(x,y)
       end

Теперь объекты типа OrderedPair могут быть созданы только так, чтобы x <= y:

julia> OrderedPair(1, 2)
OrderedPair(1, 2)

julia> OrderedPair(2,1)
ERROR: out of order
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] OrderedPair(::Int64, ::Int64) at ./none:4
 [3] top-level scope

Если тип был объявлен mutable, вы могли бы напрямую изменить значения полей, чтобы нарушить это инвариант, но несанкционированное вмешательство в внутренние данные объекта считается плохой формой. Вы (или кто-то ещё) также можете предоставить дополнительные внешние методы конструкторов в любой момент позже, но после объявления типа невозможно добавить больше внутренних методов конструкторов. Поскольку внешние методы конструкторов могут создавать объекты только путём вызова других методов конструкторов, в конечном счёте, для создания объекта должен быть вызван какой-то внутренний конструктор. Это гарантирует, что все объекты объявленного типа должны быть созданы вызовом одного из внутренних методов конструкторов, предоставленных с типом, тем самым обеспечивая некоторую степень обеспечения инвариантов типа.

Если любой внутренний метод конструктора определён, метод конструктора по умолчанию не предоставляется: предполагается, что вы сами предоставили все необходимые внутренние конструкторы. Конструктор по умолчанию эквивалентен написанию собственного внутреннего метода конструктора, который принимает все поля объекта в качестве параметров (ограниченные соответствующим типом, если соответствующее поле имеет тип), и передаёт их в new, возвращая полученный объект:

julia> struct Foo
           bar
           baz
           Foo(bar,baz) = new(bar,baz)
       end

Это объявление имеет тот же эффект, что и предыдущее определение типа Foo без явного внутреннего метода конструктора. Следующие два типа эквивалентны — один с конструктором по умолчанию, другой с явным конструктором:

julia> struct T1
           x::Int64
       end

julia> struct T2
           x::Int64
           T2(x) = new(x)
       end

julia> T1(1)
T1(1)

julia> T2(1)
T2(1)

julia> T1(1.0)
T1(1)

julia> T2(1.0)
T2(1)

Принято предоставлять как можно меньше внутренних методов конструкторов: только те, которые явно принимают все аргументы и обеспечивают необходимую проверку ошибок и преобразование. Дополнительные удобные методы конструкторов, предоставляющие значения по умолчанию или вспомогательные преобразования, должны предоставляться как внешние конструкторы, которые вызывают внутренние конструкторы, чтобы сделать всю тяжёлую работу. Это разделение обычно довольно естественно.

Неполная инициализация

Последняя проблема, которая до сих пор не решена, — создание самоссылочных объектов или, более широко, рекурсивных структур данных. Поскольку фундаментальная трудность может быть не сразу очевидной, давайте кратко объясним её. Рассмотрим следующее рекурсивное объявление типа:

julia> mutable struct SelfReferential
           obj::SelfReferential
       end

Этот тип может показаться достаточно безобидным, пока не подумаешь о том, как создать его экземпляр. Если a является экземпляром SelfReferential, то второй экземпляр можно создать с помощью вызова:

julia> b = SelfReferential(a)

Но как создать первый экземпляр, когда ни один экземпляр не существует, чтобы предоставить его как допустимое значение для его поля obj? Единственное решение — разрешить создание неполностью инициализированного экземпляра SelfReferential с неопределённым полем obj, и использовать этот неполный экземпляр как допустимое значение для поля obj другого экземпляра, например, самого себя.

Для разрешения создания неполностью инициализированных объектов Julia разрешает вызов функции new с меньшим количеством полей, чем у типа, возвращая объект с неопределёнными полями, не инициализированными. Затем внутренний метод конструктора может использовать неполный объект, завершая его инициализацию перед возвратом. Например, вот ещё одна попытка определить тип SelfReferential с внутренним конструктором без аргументов, возвращающим экземпляры с полями obj, ссылающимися на себя:

julia> mutable struct SelfReferential
           obj::SelfReferential
           SelfReferential() = (x = new(); x.obj = x)
       end

Мы можем проверить, что этот конструктор работает и строит объекты, которые, на самом деле, самоссылочные:

julia> x = SelfReferential();

julia> x === x
true

julia> x === x.obj
true

julia> x === x.obj.obj
true

Хотя обычно рекомендуется возвращать полностью инициализированный объект из внутреннего конструктора, могут возвращаться и неполностью инициализированные объекты:

julia> mutable struct Incomplete
           xx
           Incomplete() = new()
       end

julia> z = Incomplete();

Хотя вы можете создавать объекты с неинициализированными полями, любой доступ к неинициализированной ссылке является непосредственной ошибкой:

julia> z.xx
ERROR: UndefRefError: access to undefined reference

Это позволяет избежать необходимости постоянной проверки на значения null. Однако не все поля объекта являются ссылками. Julia рассматривает некоторые типы как «простые данные», то есть все их данные содержатся в себе и не ссылаются на другие объекты. Простые типы данных состоят из примитивных типов (например, Int) и неизменяемых структур других простых типов данных. Начальное содержимое простого типа данных не определено:

julia> struct HasPlain
           n::Int
           HasPlain() = new()
       end

julia> HasPlain()
HasPlain(438103441441)

Массивы простых типов данных демонстрируют такое же поведение.

Вы можете передавать неполные объекты другим функциям из внутренних конструкторов, чтобы делегировать их завершение:

julia> mutable struct Lazy
           xx
           Lazy(v) = complete_me(new(), v)
       end

Как и в случае с неполными объектами, возвращаемыми конструкторами, если complete_me или любой из его вызывающих функций попытается получить доступ к полю xx объекта Lazy до его инициализации, будет немедленно выброшено исключение.

Параметрические конструкторы

Параметрические типы добавляют несколько нюансов в историю конструкторов. Вспомните из Параметрических типов, что по умолчанию экземпляры параметрических композитных типов могут быть созданы либо с явно указанными параметрами типа, либо с параметрами типа, подразумеваемыми типами аргументов, переданными конструктору. Вот несколько примеров:

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

julia> Point(1,2) ## implicit T ##
Point{Int64}(1, 2)

julia> Point(1.0,2.5) ## implicit T ##
Point{Float64}(1.0, 2.5)

julia> Point(1,2.5) ## implicit T ##
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
Closest candidates are:
  Point(::T<:Real, ::T<:Real) where T<:Real at none:2

julia> Point{Int64}(1, 2) ## explicit T ##
Point{Int64}(1, 2)

julia> Point{Int64}(1.0,2.5) ## explicit T ##
ERROR: InexactError: Int64(Int64, 2.5)
Stacktrace:
[...]

julia> Point{Float64}(1.0, 2.5) ## explicit T ##
Point{Float64}(1.0, 2.5)

julia> Point{Float64}(1,2) ## explicit T ##
Point{Float64}(1.0, 2.0)

Как видите, для вызовов конструкторов с явными параметрами типа аргументы преобразуются в подразумеваемые типы полей: Point{Int64}(1,2) работает, но Point{Int64}(1.0,2.5) вызывает InexactError при преобразовании 2.5 в Int64. Когда тип подразумевается аргументами вызова конструктора, как в Point(1,2), то типы аргументов должны совпадать — в противном случае тип T не может быть определён — но любая пара действительных аргументов с одинаковым типом может быть передана в универсальный конструктор Point.

Здесь происходит то, что Point, Point{Float64} и Point{Int64} — всё разные функции-конструкторы. Фактически, Point{T} — это отдельная функция-конструктор для каждого типа T. Без явно указанных внутренних конструкторов объявление композитного типа Point{T<:Real} автоматически предоставляет внутренний конструктор Point{T} для каждого возможного типа T<:Real, который ведет себя так же, как и непараметрические внутренние конструкторы по умолчанию. Он также предоставляет один общий внешний конструктор Point для пар действительных аргументов, которые должны быть одного типа. Это автоматическое предоставление конструкторов эквивалентно следующему явному объявлению:

julia> struct Point{T<:Real}
           x::T
           y::T
           Point{T}(x,y) where {T<:Real} = new(x,y)
       end

julia> Point(x::T, y::T) where {T<:Real} = Point{T}(x,y);

Обратите внимание, что каждое определение напоминает форму вызова конструктора, который оно обрабатывает. Вызов Point{Int64}(1,2) вызовет определение Point{T}(x,y) внутри блока struct. С другой стороны, объявление внешнего конструктора определяет метод для общего конструктора Point, который применяется только к парам значений одного и того же вещественного типа. Это объявление позволяет вызовам конструкторов без явных параметров типа, таких как Point(1,2) и Point(1.0,2.5), работать. Поскольку объявление метода ограничивает аргументы одним типом, вызовы, подобные Point(1,2.5), с аргументами разных типов, приводят к ошибкам «нет метода».

Предположим, мы хотим заставить вызов конструктора Point(1,2.5) работать, «преобразуя» целое значение 1 в значение с плавающей точкой 1.0. Самый простой способ достичь этого — определить дополнительный внешний метод конструктора:

julia> Point(x::Int64, y::Float64) = Point(convert(Float64,x),y);

Этот метод использует функцию convert, чтобы явно преобразовать x в Float64, а затем делегирует построение общему конструктору для случая, когда оба аргумента являются Float64. С этим определением метода то, что ранее было ошибкой MethodError, теперь успешно создает точку типа Point{Float64}:

julia> Point(1,2.5)
Point{Float64}(1.0, 2.5)

julia> typeof(ans)
Point{Float64}

Однако другие похожие вызовы по-прежнему не работают:

julia> Point(1.5,2)
ERROR: MethodError: no method matching Point(::Float64, ::Int64)
Closest candidates are:
  Point(::T<:Real, !Matched::T<:Real) where T<:Real at none:1

Для более общего способа сделать все такие вызовы осмысленными, см. Преобразование и повышение. Рискуя раскрыть интригу, мы можем раскрыть здесь, что для того, чтобы все вызовы общего конструктора Point работали так, как ожидается, достаточно следующего определения внешнего метода:

julia> Point(x::Real, y::Real) = Point(promote(x,y)...);

Функция promote преобразует все свои аргументы в общий тип — в данном случае Float64. С этим определением метода конструктор Point повышает свои аргументы так же, как арифметические операторы, такие как +, и работает со всеми видами вещественных чисел:

julia> Point(1.5,2)
Point{Float64}(1.5, 2.0)

julia> Point(1,1//2)
Point{Rational{Int64}}(1//1, 1//2)

julia> Point(1.0,1//2)
Point{Float64}(1.0, 0.5)

Таким образом, хотя неявно заданные параметры типа, предоставляемые по умолчанию в Julia, довольно строгие, вполне возможно сделать их поведение более гибким, но осмысленным довольно легко. Более того, поскольку конструкторы могут использовать всю мощь системы типов, методы и многочисленный диспетчер, определение сложного поведения обычно довольно просто.

Случайное исследование: Рациональные числа

Возможно, лучший способ связать все эти части вместе — представить реальный пример параметрического составного типа и его методов конструктора. С этой целью мы реализуем наш собственный тип рациональных чисел OurRational, аналогичный встроенному типу Julia Rational, определенному в rational.jl:

julia> struct OurRational{T<:Integer} <: Real
           num::T
           den::T
           function OurRational{T}(num::T, den::T) where T<:Integer
               if num == 0 && den == 0
                    error("invalid rational: 0//0")
               end
               g = gcd(den, num)
               num = div(num, g)
               den = div(den, g)
               new(num, den)
           end
       end

julia> OurRational(n::T, d::T) where {T<:Integer} = OurRational{T}(n,d)
OurRational

julia> OurRational(n::Integer, d::Integer) = OurRational(promote(n,d)...)
OurRational

julia> OurRational(n::Integer) = OurRational(n,one(n))
OurRational

julia> ⊘(n::Integer, d::Integer) = OurRational(n,d)
⊘ (generic function with 1 method)

julia> ⊘(x::OurRational, y::Integer) = x.num ⊘ (x.den*y)
⊘ (generic function with 2 methods)

julia> ⊘(x::Integer, y::OurRational) = (x*y.den) ⊘ y.num
⊘ (generic function with 3 methods)

julia> ⊘(x::Complex, y::Real) = complex(real(x) ⊘ y, imag(x) ⊘ y)
⊘ (generic function with 4 methods)

julia> ⊘(x::Real, y::Complex) = (x*y') ⊘ real(y*y')
⊘ (generic function with 5 methods)

julia> function ⊘(x::Complex, y::Complex)
           xy = x*y'
           yy = real(y*y')
           complex(real(xy) ⊘ yy, imag(xy) ⊘ yy)
       end
⊘ (generic function with 6 methods)

Первая строка — struct OurRational{T<:Integer} <: Real — объявляет, что OurRational принимает один параметр типа целого числа и сам является вещественным типом. Объявления полей num::T и den::T указывают, что данные, хранящиеся в объекте OurRational{T} — это пара целых чисел типа T, одна из которых представляет собой числитель рационального значения, а другая — знаменатель.

Теперь становится интересно. OurRational имеет один внутренний метод конструктора, который проверяет, что ни num, ни den не равны нулю, и гарантирует, что каждый рациональный объект создается в «самой низкой форме» с неотрицательным знаменателем. Это достигается путем деления заданных значений числителя и знаменателя на их наибольший общий делитель, вычисленный с помощью функции gcd. Поскольку gcd возвращает наибольший общий делитель своих аргументов со знаком, соответствующим первому аргументу (den здесь), после этого деления новое значение den гарантированно будет неотрицательным. Поскольку это единственный внутренний конструктор для OurRational, мы можем быть уверены, что объекты OurRational всегда создаются в этой нормализованной форме.

OurRational также предоставляет несколько внешних методов конструктора для удобства. Первый — это «стандартный» общий конструктор, который выводит параметр типа T из типа числителя и знаменателя, когда они имеют один и тот же тип. Второй применяется, когда заданные значения числителя и знаменателя имеют разные типы: он повышает их до общего типа и затем делегирует построение внешнему конструктору для аргументов совпадающего типа. Третий внешний конструктор преобразует целые значения в рациональные, предоставив значение 1 в качестве знаменателя.

После определений внешних конструкторов мы определили ряд методов для оператора ⊘, который предоставляет синтаксис для записи рациональных чисел (например, 1 ⊘ 2). Тип Julia Rational использует оператор // для этой цели. До этих определений, ⊘ является полностью неопределенным оператором с только синтаксисом и без смысла. После этого он ведет себя так, как описано в Рациональные числа — его все поведение определено в этих нескольких строках. Первое и самое основное определение просто заставляет a ⊘ b создавать OurRational путем применения конструктора OurRational к a и b , когда они целые числа. Когда один из операндов ⊘ уже является рациональным числом, мы создаем новое рациональное число для полученного отношения немного по-другому; это поведение фактически идентично делению рационального числа на целое число. Наконец, применение ⊘ к комплексным целочисленным значениям создает экземпляр Complex{OurRational} — комплексное число, действительная и мнимая части которого являются рациональными числами:

julia> z = (1 + 2im) ⊘ (1 - 2im);

julia> typeof(z)
Complex{OurRational{Int64}}

julia> typeof(z) <: Complex{OurRational}
false

Таким образом, хотя оператор ⊘ обычно возвращает экземпляр OurRational, если любой из его аргументов является комплексными целыми числами, он вернет экземпляр Complex{OurRational} вместо этого. Заинтересованный читатель должен рассмотреть возможность прочтения остальной части rational.jl: она короткая, самодостаточная и реализует весь базовый тип Julia.

Только внешние конструкторы

Как мы видели, типичный параметрический тип имеет внутренние конструкторы, которые вызываются, когда параметры типа известны; например, они применяются к Point{Int} , но не к Point. Можно добавить внешние конструкторы, которые автоматически определяют параметры типа, например, построение Point{Int} из вызова Point(1,2). Внешние конструкторы вызывают внутренние конструкторы для выполнения основной работы по созданию экземпляра. Однако в некоторых случаях предпочтительнее не предоставлять внутренние конструкторы, чтобы конкретные параметры типа нельзя было запросить вручную.

Например, предположим, мы определяем тип, который хранит вектор вместе с точным представлением его суммы:

julia> struct SummedArray{T<:Number,S<:Number}
           data::Vector{T}
           sum::S
       end

julia> SummedArray(Int32[1; 2; 3], Int32(6))
SummedArray{Int32,Int32}(Int32[1, 2, 3], 6)

Проблема в том, что мы хотим, чтобы S был типом большего размера, чем T, чтобы мы могли суммировать много элементов с меньшей потерей информации. Например, когда T равен Int32, мы хотим, чтобы S был Int64. Поэтому мы хотим избежать интерфейса, который позволяет пользователю создавать экземпляры типа SummedArray{Int32,Int32}. Один из способов сделать это — предоставить конструктор только для SummedArray, но внутри блока определения struct для подавления генерации конструкторов по умолчанию:

julia> struct SummedArray{T<:Number,S<:Number}
           data::Vector{T}
           sum::S
           function SummedArray(a::Vector{T}) where T
               S = widen(T)
               new{T,S}(a, sum(S, a))
           end
       end

julia> SummedArray(Int32[1; 2; 3], Int32(6))
ERROR: MethodError: no method matching SummedArray(::Array{Int32,1}, ::Int32)
Closest candidates are:
  SummedArray(::Array{T,1}) where T at none:5

Этот конструктор будет вызван синтаксисом SummedArray(a). Синтаксис new{T,S} позволяет указать параметры для конструируемого типа, т.е. этот вызов вернет SummedArray{T,S}. new{T,S} может использоваться в любом определении конструктора, но для удобства параметры new{} автоматически выводятся из конструируемого типа, когда это возможно.

© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v0.7.0/manual/constructors/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API