Spec-Zone.ru › Julia 0.6

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

Конструкторы [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] OrderedPair(::Int64, ::Int64) at ./none:4

Если тип был объявлен 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, !Matched::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()
Stacktrace:
 [1] convert(::Type{Int64}, ::Float64) at ./float.jl:679
 [2] Point{Int64}(::Float64, ::Float64) at ./none:2

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) внутри блока type. Внешнее объявление конструктора, с другой стороны, определяет метод для общего конструктора 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, довольно строгие, можно довольно легко сделать их поведение более гибким, но осмысленным. Более того, поскольку конструкторы могут использовать всю мощь системы типов, методы и множественную диспетчеризацию, определение сложного поведения обычно довольно просто.

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

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

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 в качестве знаменателя.

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

julia> ans = (1 + 2im)//(1 - 2im);

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

julia> ans <: Complex{OurRational}
false

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

Конструкторы и преобразование

Конструкторы T(args...) в Julia реализуются как другие вызываемые объекты: к их типам добавляются методы. Тип типа является Type, поэтому все методы конструкторов хранятся в таблице методов для типа Type.

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

Однако в некоторых случаях вы можете рассмотреть добавление методов к Base.convert вместо определения конструктора, потому что Julia обращается к вызову convert(), если не найдено соответствующего конструктора. Например, если конструктор T(args...) = ... не существует, вызывается Base.convert(::Type{T}, args...) = ....

convert широко используется в Julia всякий раз, когда один тип нужно преобразовать в другой (например, при присваивании, ccall и т.д.), и его следует определять (или успешно вызывать) только в случае безналогового преобразования. Например, convert(Int, 3.0) производит 3, но convert(Int, 3.2) вызывает исключение InexactError.

Если вы хотите определить конструктор для безналогового преобразования из одного типа в другой, вы, вероятно, должны определить метод convert вместо этого.

С другой стороны, если ваш конструктор не представляет безналоговое преобразование или вообще не представляет «преобразование», лучше оставить его как конструктор, а не метод convert. Например, конструктор Array{Int}() создает нульмерное Array типа Int, но фактически не является «преобразованием» из Int в Array.

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

Как мы видели, типичный параметрический тип имеет внутренние конструкторы, которые вызываются, когда параметры типа известны; например, они применяются к 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, но внутри блока определения type, чтобы подавить генерацию конструкторов по умолчанию:

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–2016 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/release-0.6/manual/constructors/

Spec-Zone.ru

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