Constructors
البناؤون [1] هم دوال تقوم بإنشاء كائنات جديدة - تحديدًا، حالات من Composite Types. في جوليا، تعمل كائنات النوع أيضًا كدوال بنائية: فهي تنشئ حالات جديدة من نفسها عند تطبيقها على مجموعة من الوسائط كدالة. تم ذكر هذا بالفعل بإيجاز عندما تم تقديم الأنواع المركبة. على سبيل المثال:
julia> struct Foo
bar
baz
end
julia> foo = Foo(1, 2)
Foo(1, 2)
julia> foo.bar
1
julia> foo.baz
2
بالنسبة للعديد من الأنواع، فإن تشكيل كائنات جديدة عن طريق ربط قيم حقولها معًا هو كل ما هو مطلوب على الإطلاق لإنشاء مثيلات. ومع ذلك، في بعض الحالات، تكون هناك حاجة إلى مزيد من الوظائف عند إنشاء كائنات مركبة. في بعض الأحيان، يجب فرض الثوابت، إما عن طريق التحقق من المعاملات أو عن طريق تحويلها. Recursive data structures، خاصة تلك التي قد تكون ذاتية الإشارة، غالبًا ما لا يمكن إنشاؤها بشكل نظيف دون أن يتم إنشاؤها أولاً في حالة غير مكتملة ثم تعديلها برمجيًا لتصبح كاملة، كخطوة منفصلة عن إنشاء الكائن. في بعض الأحيان، يكون من الملائم فقط أن تكون قادرًا على إنشاء كائنات مع عدد أقل أو أنواع مختلفة من المعاملات مقارنةً بالحقول التي تمتلكها. يتعامل نظام جوليا لإنشاء الكائنات مع جميع هذه الحالات وأكثر.
Outer Constructor Methods
الباني يشبه أي دالة أخرى في جوليا من حيث أن سلوكه العام يتم تحديده من خلال السلوك المشترك لطرقه. وبناءً عليه، يمكنك إضافة وظائف إلى الباني ببساطة عن طريق تعريف طرق جديدة. على سبيل المثال، لنفترض أنك تريد إضافة طريقة باني لكائنات 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)
هنا، تستدعي طريقة المُنشئ بدون وسائط طريقة المُنشئ ذات الوسيطة الواحدة، والتي بدورها تستدعي طريقة المُنشئ ذات الوسيطتين التي يتم توفيرها تلقائيًا. لأسباب ستصبح واضحة قريبًا، تُسمى طرق المُنشئ الإضافية المعلنة كطرق عادية مثل هذه طرق المُنشئ الخارجية. يمكن لطرق المُنشئ الخارجية أن تنشئ فقط مثيلًا جديدًا عن طريق استدعاء طريقة مُنشئ أخرى، مثل تلك التي يتم توفيرها افتراضيًا.
Inner Constructor Methods
بينما تنجح طرق البناء الخارجية في معالجة مشكلة توفير طرق ملائمة إضافية لبناء الكائنات، إلا أنها تفشل في معالجة حالتين الاستخدام الأخريين المذكورتين في مقدمة هذا الفصل: فرض الثوابت، والسماح ببناء كائنات ذاتية الإشارة. لهذه المشاكل، يحتاج المرء إلى طرق البناء الداخلية. طريقة البناء الداخلية تشبه طريقة البناء الخارجية، باستثناء اختلافين:
- يتم الإعلان عنه داخل كتلة إعلان النوع، بدلاً من خارجه مثل الطرق العادية.
- لديها وصول إلى دالة محلية خاصة تُدعى
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)
من الممارسات الجيدة تقديم أقل عدد ممكن من طرق البناء الداخلية: فقط تلك التي تأخذ جميع المعاملات بشكل صريح وتفرض التحقق من الأخطاء الأساسي والتحويل. يجب توفير طرق البناء المساعدة الإضافية، التي تزود القيم الافتراضية أو التحويلات المساعدة، كطرق بناء خارجية تستدعي الطرق الداخلية للقيام بالعمل الشاق. هذا الفصل يكون عادةً طبيعيًا جدًا.
Incomplete Initialization
المشكلة النهائية التي لم يتم تناولها بعد هي بناء الكائنات ذاتية الإشارة، أو بشكل أكثر عمومية، الهياكل البيانية التكرارية. نظرًا لأن الصعوبة الأساسية قد لا تكون واضحة على الفور، دعنا نشرحها باختصار. اعتبر إعلان النوع التكراري التالي:
julia> mutable struct SelfReferential
obj::SelfReferential
end
قد يبدو هذا النوع غير ضار بما فيه الكفاية، حتى يتم النظر في كيفية إنشاء مثيل له. إذا كان a
مثيلًا لـ SelfReferential
، فيمكن إنشاء مثيل ثانٍ من خلال الاستدعاء:
julia> b = SelfReferential(a)
لكن كيف يمكن للمرء إنشاء الحالة الأولى عندما لا توجد حالة لتوفير قيمة صالحة لحقل obj
الخاص بها؟ الحل الوحيد هو السماح بإنشاء حالة غير مكتملة من SelfReferential
مع حقل obj
غير المعين، واستخدام تلك الحالة غير المكتملة كقيمة صالحة لحقل obj
لحالة أخرى، مثل، على سبيل المثال، نفسها.
للسماح بإنشاء كائنات غير مُهيأة بالكامل، تسمح جوليا باستدعاء دالة 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
data
Incomplete() = new()
end
julia> z = Incomplete();
بينما يُسمح لك بإنشاء كائنات تحتوي على حقول غير مُهيأة، فإن أي وصول إلى مرجع غير مُهيأ يُعتبر خطأً فوريًا:
julia> z.data
ERROR: UndefRefError: access to undefined reference
هذا يتجنب الحاجة إلى التحقق المستمر من قيم null
. ومع ذلك، ليست جميع حقول الكائنات مراجع. تعتبر جوليا بعض الأنواع "بيانات بسيطة"، مما يعني أن جميع بياناتها مكتفية ذاتيًا ولا تشير إلى كائنات أخرى. تتكون أنواع البيانات البسيطة من الأنواع الأولية (مثل Int
) والهياكل غير القابلة للتغيير من أنواع بيانات بسيطة أخرى (انظر أيضًا: isbits
، isbitstype
). المحتويات الأولية لنوع البيانات البسيطة غير محددة:
julia> struct HasPlain
n::Int
HasPlain() = new()
end
julia> HasPlain()
HasPlain(438103441441)
تظهر مصفوفات أنواع البيانات البسيطة نفس السلوك.
يمكنك تمرير كائنات غير مكتملة إلى دوال أخرى من المنشئين الداخليين لتفويض إكمالها:
julia> mutable struct Lazy
data
Lazy(v) = complete_me(new(), v)
end
كما هو الحال مع الكائنات غير المكتملة التي يتم إرجاعها من المنشئين، إذا حاول complete_me
أو أي من الدوال المستدعاة الوصول إلى حقل data
من كائن Lazy
قبل أن يتم تهيئته، سيتم طرح خطأ على الفور.
Parametric Constructors
تضيف الأنواع البارامترية بعض التعقيدات لقصة المُنشئ. تذكر من Parametric Types أنه، بشكل افتراضي، يمكن إنشاء مثيلات من الأنواع المركبة البارامترية إما مع معلمات نوع مُعطاة بشكل صريح أو مع معلمات نوع مستنتجة من أنواع الوسائط المقدمة إلى المُنشئ. إليك بعض الأمثلة:
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)
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, ::T) 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(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
ثم تفوض البناء إلى المُنشئ العام للحالة التي تكون فيها كلا الحجتين 4d61726b646f776e2e436f64652822222c2022466c6f617436342229_40726566
. مع تعريف هذه الطريقة، ما كان سابقًا MethodError
الآن ينشئ بنجاح نقطة من النوع Point{Float64}
:
julia> p = Point(1,2.5)
Point{Float64}(1.0, 2.5)
julia> typeof(p)
Point{Float64}
ومع ذلك، لا تزال المكالمات المماثلة الأخرى لا تعمل:
julia> Point(1.5,2)
ERROR: MethodError: no method matching Point(::Float64, ::Int64)
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<:Real
@ Main none:1
Point(!Matched::Int64, !Matched::Float64)
@ Main none:1
Stacktrace:
[...]
لطريقة أكثر عمومية لجعل جميع هذه المكالمات تعمل بشكل منطقي، انظر Conversion and Promotion. على خطر إفساد التشويق، يمكننا الكشف هنا أن كل ما يتطلبه الأمر هو تعريف الطريقة الخارجية التالية لجعل جميع المكالمات إلى المُنشئ العام 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)
لذا، بينما تكون مُنشئات معلمات النوع الضمنية المقدمة افتراضيًا في جوليا صارمة إلى حد ما، من الممكن جعلها تتصرف بطريقة أكثر مرونة ولكنها معقولة بسهولة. علاوة على ذلك، نظرًا لأن المُنشئات يمكن أن تستفيد من كل قوة نظام النوع، والأساليب، والتوزيع المتعدد، فإن تعريف سلوك معقد عادة ما يكون بسيطًا جدًا.
Case Study: Rational
ربما تكون أفضل طريقة لربط كل هذه الأجزاء معًا هي تقديم مثال من العالم الحقيقي لنوع مركب بارامتري وطرق بنائه. لهذا الغرض، نقوم بتنفيذ نوع عدد كسري خاص بنا OurRational
، مشابه لنوع 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
num = flipsign(num, den)
den = flipsign(den, den)
g = gcd(num, den)
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
دائمًا يعيد رقمًا غير سالب، بغض النظر عن إشارة وسائطه). نظرًا لأن هذه هي الطريقة الوحيدة للبناء الداخلي لـ OurRational
، يمكننا أن نكون متأكدين من أن كائنات OurRational
يتم إنشاؤها دائمًا في هذا الشكل الموحد.
OurRational
يوفر أيضًا عدة طرق بناء خارجية لراحة المستخدم. الأول هو "البناء العام" القياسي الذي يستنتج نوع المعامل T
من نوع البسط والمقام عندما يكون لهما نفس النوع. الثاني ينطبق عندما تكون قيم البسط والمقام المعطاة من أنواع مختلفة: حيث يرفعها إلى نوع مشترك ثم يفوض البناء إلى البناء الخارجي للمعاملات من النوع المطابق. البناء الخارجي الثالث يحول القيم الصحيحة إلى كسور من خلال تزويد قيمة 1
كمقام.
بعد تعريفات المُنشئ الخارجي، قمنا بتعريف عدد من الطرق لعملية ⊘
، التي توفر صيغة لكتابة الأعداد النسبية (على سبيل المثال، 1 ⊘ 2
). يستخدم نوع Rational
في جوليا العملية //
لهذا الغرض. قبل هذه التعريفات، كانت ⊘
عملية غير معرفة تمامًا مع وجود صيغة فقط دون معنى. بعد ذلك، تتصرف كما هو موصوف في Rational Numbers – سلوكها بالكامل مُعرف في هذه السطور القليلة. لاحظ أن الاستخدام في الوسط لـ ⊘
يعمل لأن جوليا لديها مجموعة من الرموز المعترف بها كعمليات وسطية. التعريف الأول والأساسي يجعل 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}
true
لذا، على الرغم من أن عامل التشغيل ⊘
عادةً ما يُرجع مثيلًا من OurRational
، إذا كان أحد معاييره عددًا صحيحًا مركبًا، فسيرجع مثيلًا من Complex{<:OurRational}
بدلاً من ذلك. يجب على القارئ المهتم أن يفكر في تصفح بقية rational.jl
: إنه قصير، ومحتوى ذاتي، وينفذ نوع جوليا أساسي كامل.
Outer-only constructors
كما رأينا، يحتوي النوع البارامتري النموذجي على منشئات داخلية تُستدعى عندما تكون معلمات النوع معروفة؛ على سبيل المثال، تنطبق على 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(::Vector{Int32}, ::Int32)
The type `SummedArray` exists, but no method is defined for this combination of argument types when trying to construct it.
Closest candidates are:
SummedArray(::Vector{T}) where T
@ Main none:4
Stacktrace:
[...]
سيتم استدعاء هذا المُنشئ بواسطة الصيغة SummedArray(a)
. تتيح الصيغة new{T,S}
تحديد المعلمات لنوع الكائن الذي سيتم إنشاؤه، أي أن هذه المكالمة ستعيد SummedArray{T,S}
. يمكن استخدام new{T,S}
في أي تعريف مُنشئ، ولكن لسهولة الاستخدام، يتم اشتقاق المعلمات لـ new{}
تلقائيًا من النوع الذي يتم إنشاؤه عند الإمكان.
Constructors are just callable objects
يمكن أن يكون كائن من أي نوع made callable عن طريق تعريف طريقة. وهذا يشمل الأنواع، أي كائنات من نوع Type
؛ ويمكن، في الواقع، اعتبار المنشئات ككائنات نوع قابلة للاستدعاء. على سبيل المثال، هناك العديد من الطرق المعرفة على Bool
وأنواعها العليا المختلفة:
julia> methods(Bool)
# 10 methods for type constructor:
[1] Bool(x::BigFloat)
@ Base.MPFR mpfr.jl:393
[2] Bool(x::Float16)
@ Base float.jl:338
[3] Bool(x::Rational)
@ Base rational.jl:138
[4] Bool(x::Real)
@ Base float.jl:233
[5] (dt::Type{<:Integer})(ip::Sockets.IPAddr)
@ Sockets ~/tmp/jl/jl/julia-nightly-assert/share/julia/stdlib/v1.11/Sockets/src/IPAddr.jl:11
[6] (::Type{T})(x::Enum{T2}) where {T<:Integer, T2<:Integer}
@ Base.Enums Enums.jl:19
[7] (::Type{T})(z::Complex) where T<:Real
@ Base complex.jl:44
[8] (::Type{T})(x::Base.TwicePrecision) where T<:Number
@ Base twiceprecision.jl:265
[9] (::Type{T})(x::T) where T<:Number
@ boot.jl:894
[10] (::Type{T})(x::AbstractChar) where T<:Union{AbstractChar, Number}
@ char.jl:50
الصيغة المعتادة للبناء تعادل تمامًا صيغة الكائن الشبيهة بالدالة، لذا فإن محاولة تعريف طريقة باستخدام كل من الصيغتين ستؤدي إلى استبدال الطريقة الأولى بالطريقة التالية:
julia> struct S
f::Int
end
julia> S() = S(7)
S
julia> (::Type{S})() = S(8) # overwrites the previous constructor method
julia> S()
S(8)
- 1Nomenclature: while the term "constructor" generally refers to the entire function which constructs objects of a type, it is common to abuse terminology slightly and refer to specific constructor methods as "constructors". In such situations, it is generally clear from the context that the term is used to mean "constructor method" rather than "constructor function", especially as it is often used in the sense of singling out a particular method of the constructor from all of the others.