Modules
تساعد الوحدات في جوليا على تنظيم الشيفرة في وحدات متماسكة. يتم تحديدها نحويًا داخل module NameOfModule ... end
، ولها الميزات التالية:
- تعتبر الوحدات مساحات أسماء منفصلة، حيث تقدم كل منها نطاقًا عالميًا جديدًا. هذا مفيد، لأنه يسمح باستخدام نفس الاسم لوظائف مختلفة أو متغيرات عالمية دون تعارض، طالما أنها في وحدات منفصلة.
- تحتوي الوحدات على مرافق لإدارة المساحات الاسمية بالتفصيل: كل وحدة تحدد مجموعة من الأسماء التي تقوم بتصديرها وتحددها على أنها عامة، ويمكنها استيراد أسماء من وحدات أخرى باستخدام
using
وimport
(سنشرح هذه أدناه). - يمكن تجميع الوحدات مسبقًا لتحميل أسرع، وقد تحتوي على كود لتهيئة وقت التشغيل.
عادةً، في حزم جوليا الأكبر، سترى كود الوحدة منظمًا في ملفات، مثل
module SomeModule
# export, public, using, import statements are usually here; we discuss these below
include("file1.jl")
include("file2.jl")
end
الملفات وأسماء الملفات غالبًا ما تكون غير مرتبطة بالوحدات؛ الوحدات مرتبطة فقط بتعبيرات الوحدة. يمكن أن يحتوي كل وحدة على ملفات متعددة، ويمكن أن تحتوي كل ملف على وحدات متعددة. include
يتصرف كما لو أن محتويات ملف المصدر تم تقييمها في النطاق العالمي للوحدة المضمنة. في هذا الفصل، نستخدم أمثلة قصيرة ومبسطة، لذا لن نستخدم include
.
الأسلوب الموصى به هو عدم إزاحة جسم الوحدة، حيث أن ذلك قد يؤدي عادةً إلى إزاحة الملفات بالكامل. كما أنه من الشائع استخدام UpperCamelCase
لأسماء الوحدات (تمامًا مثل الأنواع)، واستخدام الشكل الجمع إذا كان ذلك مناسبًا، خاصةً إذا كانت الوحدة تحتوي على معرف يحمل اسمًا مشابهًا، لتجنب تعارض الأسماء. على سبيل المثال،
module FastThings
struct FastThing
...
end
end
Namespace management
إدارة المساحات الاسمية تشير إلى المرافق التي يقدمها اللغة لجعل الأسماء في وحدة متاحة في وحدات أخرى. نناقش المفاهيم والوظائف ذات الصلة أدناه بالتفصيل.
Qualified names
ت belong to a module, called the parent module, which can be found interactively with parentmodule
، على سبيل المثال
julia> parentmodule(UnitRange)
Base
يمكن أيضًا الإشارة إلى هذه الأسماء خارج وحدتها الأصلية عن طريق تمييزها بوحدتها، مثل Base.UnitRange
. يُطلق على هذا اسم اسم مؤهل. قد تكون الوحدة الأصلية متاحة باستخدام سلسلة من الوحدات الفرعية مثل Base.Math.sin
، حيث تُسمى Base.Math
مسار الوحدة. بسبب الغموض النحوي، يتطلب تأهيل اسم يحتوي فقط على رموز، مثل عامل، إدراج نقطتين، مثل Base.:+
. عدد قليل من العوامل يتطلب أيضًا أقواسًا، مثل Base.:(==)
.
إذا كان الاسم مؤهلاً، فإنه يكون دائماً قابلاً للوصول، وفي حالة الدالة، يمكن أيضاً إضافة طرق إليها باستخدام الاسم المؤهل كاسم للدالة.
داخل الوحدة، يمكن "حجز" اسم متغير دون تعيينه عن طريق إعلانه كـ global x
. هذا يمنع تعارض الأسماء للمتغيرات العالمية التي تم تهيئتها بعد وقت التحميل. الصيغة M.x = y
لا تعمل لتعيين متغير عالمي في وحدة أخرى؛ فالتعيين العالمي يكون دائمًا محليًا للوحدة.
Export lists
يمكن إضافة الأسماء (التي تشير إلى الدوال، الأنواع، المتغيرات العالمية، والثوابت) إلى قائمة التصدير لوحدة باستخدام export
: هذه هي الرموز التي يتم استيرادها عند استخدام الوحدة. عادةً، تكون في أعلى أو بالقرب من تعريف الوحدة حتى يتمكن قراء الشيفرة المصدرية من العثور عليها بسهولة، كما في
julia> module NiceStuff
export nice, DOG
struct Dog end # singleton type, not exported
const DOG = Dog() # named instance, exported
nice(x) = "nice $x" # function, exported
end;
لكن هذا مجرد اقتراح أسلوب — يمكن أن يحتوي الوحدة على عدة عبارات export
في مواقع عشوائية.
من الشائع تصدير الأسماء التي تشكل جزءًا من واجهة برمجة التطبيقات (API). في الكود أعلاه، تشير قائمة التصدير إلى أن المستخدمين يجب أن يستخدموا nice
و DOG
. ومع ذلك، نظرًا لأن الأسماء المؤهلة تجعل المعرفات متاحة دائمًا، فإن هذه مجرد خيار لتنظيم واجهات برمجة التطبيقات: على عكس اللغات الأخرى، لا تحتوي جوليا على تسهيلات لإخفاء تفاصيل الوحدة حقًا.
أيضًا، بعض الوحدات لا تصدر أسماء على الإطلاق. يتم ذلك عادةً إذا كانت تستخدم كلمات شائعة، مثل derivative
، في واجهة برمجتها، مما قد يتسبب في تعارض مع قوائم التصدير لوحدات أخرى. سنرى كيف ندير تعارض الأسماء أدناه.
لتحديد اسم كعام دون تصديره إلى مساحة أسماء الأشخاص الذين يستخدمون using NiceStuff
، يمكن استخدام public
بدلاً من export
. هذا يحدد الاسماء العامة كجزء من واجهة برمجة التطبيقات العامة، ولكنه لا يؤثر على أي مساحة أسماء. الكلمة الرئيسية public
متاحة فقط في جوليا 1.11 وما فوق. للحفاظ على التوافق مع جوليا 1.10 وما دون، استخدم الماكرو @compat
من حزمة Compat.
Standalone using
and import
للاستخدام التفاعلي، فإن الطريقة الأكثر شيوعًا لتحميل وحدة هي using ModuleName
. هذا loads الكود المرتبط بـ ModuleName
، ويجلب
- اسم الوحدة
- وعناصر قائمة التصدير إلى مساحة الاسم العالمية المحيطة.
تقنيًا، تعني العبارة using ModuleName
أن وحدة تسمى ModuleName
ستكون متاحة لحل الأسماء حسب الحاجة. عندما يتم مواجهة متغير عالمي ليس له تعريف في الوحدة الحالية، سيبحث النظام عنه بين المتغيرات المصدرة بواسطة ModuleName
ويستخدمه إذا تم العثور عليه هناك. هذا يعني أن جميع استخدامات ذلك المتغير العالمي داخل الوحدة الحالية ستتوافق مع تعريف ذلك المتغير في ModuleName
.
لتحميل وحدة من حزمة، يمكن استخدام العبارة using ModuleName
. لتحميل وحدة من وحدة معرفة محليًا، يجب إضافة نقطة قبل اسم الوحدة مثل using .ModuleName
.
للمتابعة مع مثالنا،
julia> using .NiceStuff
سيتم تحميل الكود أعلاه، مما يجعل NiceStuff
(اسم الوحدة)، و DOG
و nice
متاحة. Dog
ليست على قائمة التصدير، ولكن يمكن الوصول إليها إذا تم تأهيل الاسم مع مسار الوحدة (الذي هنا هو فقط اسم الوحدة) كـ NiceStuff.Dog
.
من المهم أن using ModuleName
هو الشكل الوحيد الذي تهم فيه قوائم التصدير على الإطلاق.
في المقابل،
julia> import .NiceStuff
يجلب فقط اسم الوحدة إلى النطاق. سيحتاج المستخدمون إلى استخدام NiceStuff.DOG
و NiceStuff.Dog
و NiceStuff.nice
للوصول إلى محتوياتها. عادةً ما يتم استخدام import ModuleName
في السياقات عندما يريد المستخدم الحفاظ على نظافة مساحة الأسماء. كما سنرى في القسم التالي، فإن import .NiceStuff
يعادل using .NiceStuff: NiceStuff
.
يمكنك دمج عدة عبارات using
و import
من نفس النوع في تعبير مفصول بفواصل، على سبيل المثال
julia> using LinearAlgebra, Random
using
and import
with specific identifiers, and adding methods
عندما يتبع using ModuleName:
أو import ModuleName:
قائمة مفصولة بفواصل من الأسماء، يتم تحميل الوحدة، ولكن فقط تلك الأسماء المحددة يتم جلبها إلى مساحة الأسماء بواسطة العبارة. على سبيل المثال،
julia> using .NiceStuff: nice, DOG
سوف نستورد الأسماء nice
و DOG
.
من المهم أن اسم الوحدة NiceStuff
لن يكون في مساحة الأسماء. إذا كنت تريد جعله متاحًا، يجب عليك ذكره صراحة، كما هو الحال في
julia> using .NiceStuff: nice, DOG, NiceStuff
عندما تقوم حزمتان أو أكثر بتصدير اسم ولا يشير هذا الاسم إلى نفس الشيء في كل من الحزم، ويتم تحميل الحزم عبر using
دون قائمة صريحة من الأسماء، فإنه من الخطأ الإشارة إلى ذلك الاسم دون تأهيل. لذلك يُوصى بأن يقوم الكود الذي يُقصد به أن يكون متوافقًا مع الإصدارات المستقبلية من تبعياته ومن جوليا، مثل الكود في الحزم المصدرة، بإدراج الأسماء التي يستخدمها من كل حزمة محملة، مثل using Foo: Foo, f
بدلاً من using Foo
.
تحتوي جوليا على شكلين لنفس الشيء ظاهريًا لأن import ModuleName: f
يسمح بإضافة طرق إلى f
بدون مسار وحدة. بمعنى آخر، المثال التالي سيعطي خطأ:
julia> using .NiceStuff: nice
julia> struct Cat end
julia> nice(::Cat) = "nice 😸"
ERROR: invalid method definition in Main: function NiceStuff.nice must be explicitly imported to be extended
Stacktrace:
[1] top-level scope
@ none:0
[2] top-level scope
@ none:1
يمنع هذا الخطأ إضافة طرق عن غير قصد إلى الدوال في وحدات أخرى كنت تنوي استخدامها فقط.
هناك طريقتان للتعامل مع هذا. يمكنك دائمًا تأهيل أسماء الدوال بمسار الوحدة:
julia> using .NiceStuff
julia> struct Cat end
julia> NiceStuff.nice(::Cat) = "nice 😸"
بدلاً من ذلك، يمكنك import
اسم الدالة المحددة:
julia> import .NiceStuff: nice
julia> struct Cat end
julia> nice(::Cat) = "nice 😸"
nice (generic function with 2 methods)
أي واحدة تختارها هي مسألة أسلوب. الشكل الأول يوضح أنك تضيف طريقة إلى دالة في وحدة أخرى (تذكر أن الاستيرادات وتعريف الطريقة قد تكون في ملفات منفصلة)، بينما الشكل الثاني أقصر، وهو مريح بشكل خاص إذا كنت تعرف طرقًا متعددة.
بمجرد أن يتم جعل المتغير مرئيًا عبر using
أو import
، لا يمكن للوحدة إنشاء متغير خاص بها بنفس الاسم. المتغيرات المستوردة هي للقراءة فقط؛ فإن تعيين قيمة لمتغير عالمي يؤثر دائمًا على متغير مملوك للوحدة الحالية، أو يثير خطأ.
Renaming with as
يمكن إعادة تسمية معرف تم إدخاله في النطاق بواسطة import
أو using
باستخدام الكلمة الرئيسية as
. هذا مفيد لتجاوز تعارضات الأسماء وكذلك لتقصير الأسماء. على سبيل المثال، يقوم Base
بتصدير اسم الدالة read
، ولكن حزمة CSV.jl توفر أيضًا CSV.read
. إذا كنا سنستدعي قراءة CSV عدة مرات، سيكون من الملائم إسقاط المؤهل CSV.
. ولكن بعد ذلك يصبح من الغامض ما إذا كنا نشير إلى Base.read
أو CSV.read
:
julia> read;
julia> import CSV: read
WARNING: ignoring conflicting import of CSV.read into Main
إعادة التسمية توفر حلاً:
julia> import CSV: read as rd
يمكن أيضًا إعادة تسمية الحزم المستوردة:
import BenchmarkTools as BT
as
يعمل مع using
فقط عندما يتم جلب معرف واحد إلى النطاق. على سبيل المثال، using CSV: read as rd
يعمل، لكن using CSV as C
لا يعمل، لأنه يعمل على جميع الأسماء المصدرة في CSV
.
Mixing multiple using
and import
statements
عند استخدام عدة عبارات using
أو import
من أي من الأشكال المذكورة أعلاه، يتم دمج تأثيرها بالترتيب الذي تظهر به. على سبيل المثال،
julia> using .NiceStuff # exported names and the module name
julia> import .NiceStuff: nice # allows adding methods to unqualified functions
سيجلب جميع الأسماء المصدرة من NiceStuff
واسم الوحدة نفسها إلى النطاق، وسيسمح أيضًا بإضافة طرق إلى nice
دون الحاجة إلى تمييزها باسم الوحدة.
Handling name conflicts
اعتبر الحالة التي تصدر فيها حزمتان (أو أكثر) نفس الاسم، كما في
julia> module A
export f
f() = 1
end
A
julia> module B
export f
f() = 2
end
B
العبارة using .A, .B
تعمل، ولكن عندما تحاول استدعاء f
، تحصل على خطأ مع تلميح
julia> using .A, .B
julia> f
ERROR: UndefVarError: `f` not defined in `Main`
Hint: It looks like two or more modules export different bindings with this name, resulting in ambiguity. Try explicitly importing it from a particular module, or qualifying the name with the module it should come from.
هنا، لا تستطيع جوليا أن تقرر أي f
تشير إليه، لذا عليك أن تتخذ قرارًا. الحلول التالية هي الأكثر استخدامًا:
ببساطة تابع باستخدام الأسماء المؤهلة مثل
A.f
وB.f
. هذا يجعل السياق واضحًا للقارئ في كودك، خاصة إذا كانf
يتزامن بالصدفة ولكن له معاني مختلفة في حزم مختلفة. على سبيل المثال، يستخدم مصطلحdegree
بطرق مختلفة في الرياضيات، والعلوم الطبيعية، وفي الحياة اليومية، ويجب الحفاظ على هذه المعاني منفصلة.استخدم الكلمة الرئيسية
as
أعلاه لإعادة تسمية واحد أو كلا المعرفين، على سبيل المثال```jldoctest module_manual julia> using .A: f as f
julia> using .B: f as g
```
سوف يجعل
B.f
متاحًا كـg
. هنا، نفترض أنك لم تستخدمusing A
من قبل، مما كان سيجلبf
إلى مساحة الأسماء.عندما تتشارك الأسماء المعنية معنى، من الشائع أن يقوم أحد الوحدات باستيرادها من وحدة أخرى، أو أن يكون لديها حزمة "أساسية" خفيفة الوزن تهدف فقط إلى تعريف واجهة مثل هذه، والتي يمكن استخدامها من قبل حزم أخرى. من المعتاد أن تنتهي أسماء هذه الحزم بـ
...Base
(وهو ما لا علاقة له بوحدةBase
في جوليا).
Default top-level definitions and bare modules
تحتوي الوحدات تلقائيًا على using Core
، using Base
، وتعريفات الدوال eval
و include
، التي تقوم بتقييم التعبيرات/الملفات ضمن النطاق العالمي لتلك الوحدة.
إذا لم تكن هذه التعريفات الافتراضية مرغوبة، يمكن تعريف الوحدات باستخدام الكلمة الرئيسية baremodule
بدلاً من ذلك (ملاحظة: لا يزال يتم استيراد Core
). من حيث baremodule
، تبدو الوحدة القياسية module
هكذا:
baremodule Mod
using Base
eval(x) = Core.eval(Mod, x)
include(p) = Base.include(Mod, p)
...
end
إذا لم يكن Core
مرغوبًا حتى، يمكن تعريف وحدة تستورد لا شيء ولا تعرف أي أسماء على الإطلاق باستخدام Module(:YourNameHere, false, false)
ويمكن تقييم الشيفرة فيها باستخدام @eval
أو Core.eval
:
julia> arithmetic = Module(:arithmetic, false, false)
Main.arithmetic
julia> @eval arithmetic add(x, y) = $(+)(x, y)
add (generic function with 1 method)
julia> arithmetic.add(12, 13)
25
Standard modules
هناك ثلاثة وحدات قياسية مهمة:
Core
يحتوي على جميع الوظائف "المضمنة" في اللغة.Base
يحتوي على وظائف أساسية مفيدة في معظم الحالات.Main
هو الوحدة العليا والوحدة الحالية، عندما يتم بدء تشغيل جوليا.
بشكل افتراضي، تأتي جوليا مع بعض وحدات المكتبة القياسية. هذه الوحدات تعمل مثل حزم جوليا العادية باستثناء أنك لا تحتاج إلى تثبيتها بشكل صريح. على سبيل المثال، إذا كنت ترغب في إجراء بعض اختبارات الوحدة، يمكنك تحميل مكتبة Test
القياسية كما يلي:
using Test
Submodules and relative paths
يمكن أن تحتوي الوحدات على وحدات فرعية، مع استخدام نفس بناء الجملة module ... end
. يمكن استخدامها لتقديم مساحات أسماء منفصلة، مما يمكن أن يكون مفيدًا لتنظيم قواعد الشفرات المعقدة. لاحظ أن كل module
يقدم خاصيته الخاصة scope، لذا فإن الوحدات الفرعية لا "ترث" الأسماء تلقائيًا من الوالدين.
يوصى بأن تشير الوحدات الفرعية إلى وحدات أخرى داخل الوحدة الأم المحيطة (بما في ذلك الأخيرة) باستخدام مؤهلات الوحدة النسبية في عبارات using
و import
. يبدأ مؤهل الوحدة النسبي بنقطة (.
)، والتي تتوافق مع الوحدة الحالية، وكل نقطة متتالية .
تقود إلى الوالد للوحدة الحالية. يجب أن يتبع ذلك الوحدات إذا لزم الأمر، وفي النهاية الاسم الفعلي للوصول إليه، جميعها مفصولة بنقاط .
.
اعتبر المثال التالي، حيث تعرف الوحدة الفرعية SubA
دالة، والتي يتم توسيعها بعد ذلك في وحدتها "الأخت":
julia> module ParentModule
module SubA
export add_D # exported interface
const D = 3
add_D(x) = x + D
end
using .SubA # brings `add_D` into the namespace
export add_D # export it from ParentModule too
module SubB
import ..SubA: add_D # relative path for a “sibling” module
struct Infinity end
add_D(x::Infinity) = x
end
end;
يمكنك رؤية الشيفرة في الحزم، والتي، في حالة مشابهة، تستخدم
julia> import .ParentModule.SubA: add_D
ومع ذلك، يعمل هذا من خلال code loading، وبالتالي فإنه يعمل فقط إذا كان ParentModule
في حزمة. من الأفضل استخدام المسارات النسبية.
لاحظ أن ترتيب التعريفات مهم أيضًا إذا كنت تقوم بتقييم القيم. اعتبر
module TestPackage
export x, y
x = 0
module Sub
using ..TestPackage
z = y # ERROR: UndefVarError: `y` not defined in `Main`
end
y = 1
end
حيث أن Sub
تحاول استخدام TestPackage.y
قبل أن يتم تعريفه، لذا فإنه لا يحتوي على قيمة.
لأسباب مماثلة، لا يمكنك استخدام ترتيب دوري:
module A
module B
using ..C # ERROR: UndefVarError: `C` not defined in `Main.A`
end
module C
using ..B
end
end
Module initialization and precompilation
يمكن أن تستغرق الوحدات الكبيرة عدة ثوانٍ لتحميلها لأن تنفيذ جميع التعليمات في وحدة ما غالبًا ما يتطلب تجميع كمية كبيرة من الشيفرة. تقوم جوليا بإنشاء ذاكرات مؤقتة مسبقة التجميع للوحدة لتقليل هذا الوقت.
تُنشأ وتُستخدم ملفات الوحدات المجمعة (المعروفة أحيانًا باسم "ملفات التخزين المؤقت") تلقائيًا عند تحميل وحدة باستخدام import
أو using
. إذا لم تكن ملفات التخزين المؤقت موجودة بعد، فسيتم تجميع الوحدة وحفظها لإعادة استخدامها في المستقبل. يمكنك أيضًا استدعاء Base.compilecache(Base.identify_package("modulename"))
يدويًا لإنشاء هذه الملفات دون تحميل الوحدة. ستُخزن ملفات التخزين المؤقت الناتجة في المجلد الفرعي compiled
من DEPOT_PATH[1]
. إذا لم يتغير أي شيء في نظامك، فسيتم استخدام مثل هذه الملفات عند تحميل الوحدة باستخدام import
أو using
.
تخزن ملفات ذاكرة التخزين المؤقت السابقة تعريفات الوحدات والأنواع والأساليب والثوابت. قد تخزن أيضًا تخصصات الأساليب والشفرة المولدة لها، ولكن هذا يتطلب عادةً من المطور إضافة توجيهات صريحة precompile
أو تنفيذ أحمال عمل تجبر على الترجمة أثناء بناء الحزمة.
ومع ذلك، إذا قمت بتحديث تبعيات الوحدة أو تغيير شفرتها المصدرية، يتم إعادة تجميع الوحدة تلقائيًا عند using
أو import
. التبعيات هي الوحدات التي تستوردها، بناء جوليا، الملفات التي تتضمنها، أو التبعيات الصريحة المعلنة بواسطة include_dependency(path)
في ملف(ملفات) الوحدة.
بالنسبة للاعتماديات الملفات المحملة بواسطة include
، يتم تحديد التغيير من خلال فحص ما إذا كان حجم الملف (fsize
) أو المحتوى (المكثف في تجزئة) غير متغير. بالنسبة للاعتماديات الملفات المحملة بواسطة include_dependency
، يتم تحديد التغيير من خلال فحص ما إذا كان وقت التعديل (mtime
) غير متغير، أو يساوي وقت التعديل المقطوع إلى أقرب ثانية (لتلبية أنظمة لا يمكنها نسخ mtime بدقة أقل من الثانية). كما يأخذ في الاعتبار ما إذا كان المسار إلى الملف الذي اختاره منطق البحث في require
يتطابق مع المسار الذي أنشأ ملف ما قبل التجميع. كما يأخذ في الاعتبار مجموعة الاعتماديات التي تم تحميلها بالفعل في العملية الحالية ولن يعيد تجميع تلك الوحدات، حتى لو تغيرت ملفاتها أو اختفت، لتجنب إنشاء عدم توافق بين النظام الجاري وذاكرة التخزين المؤقت للتجميع المسبق. أخيرًا، يأخذ في الاعتبار التغييرات في أي compile-time preferences.
إذا كنت تعلم أن الوحدة ليست آمنة للتجميع المسبق (على سبيل المثال، لأحد الأسباب الموضحة أدناه)، يجب عليك وضع __precompile__(false)
في ملف الوحدة (عادة ما يتم وضعه في الأعلى). سيؤدي ذلك إلى جعل Base.compilecache
يرمي خطأ، وسيتسبب في تحميل using
/ import
لها مباشرة في العملية الحالية وتجاوز التجميع المسبق والتخزين المؤقت. وهذا يمنع أيضًا استيراد الوحدة من قبل أي وحدة تم تجميعها مسبقًا.
قد تحتاج إلى أن تكون على دراية ببعض السلوكيات المتأصلة في إنشاء المكتبات المشتركة التزايدية والتي قد تتطلب العناية عند كتابة وحدتك. على سبيل المثال، لا يتم الحفاظ على الحالة الخارجية. لاستيعاب ذلك، يجب فصل أي خطوات تهيئة يجب أن تحدث في وقت التشغيل عن الخطوات التي يمكن أن تحدث في وقت الترجمة. لهذا الغرض، يسمح لك جوليا بتعريف دالة __init__()
في وحدتك التي تنفذ أي خطوات تهيئة يجب أن تحدث في وقت التشغيل. لن يتم استدعاء هذه الدالة أثناء الترجمة (--output-*
). بشكل فعال، يمكنك أن تفترض أنها ستُشغل مرة واحدة فقط في عمر الكود. يمكنك، بالطبع، استدعاؤها يدويًا إذا لزم الأمر، لكن الافتراضي هو افتراض أن هذه الدالة تتعامل مع حساب الحالة للجهاز المحلي، والذي لا يحتاج إلى أن يتم – أو حتى يجب ألا يتم – التقاطه في الصورة المترجمة. سيتم استدعاؤها بعد تحميل الوحدة في عملية، بما في ذلك إذا كانت تُحمّل في ترجمة تزايدية (--output-incremental=yes
)، ولكن ليس إذا كانت تُحمّل في عملية ترجمة كاملة.
بشكل خاص، إذا قمت بتعريف دالة function __init__()
في وحدة، فإن جوليا ستقوم باستدعاء __init__()
على الفور بعد تحميل الوحدة (مثل import
، using
، أو require
) في وقت التشغيل للمرة الأولى (أي، يتم استدعاء __init__
مرة واحدة فقط، وفقط بعد تنفيذ جميع العبارات في الوحدة). نظرًا لأنه يتم استدعاؤه بعد استيراد الوحدة بالكامل، فإن أي وحدات فرعية أو وحدات مستوردة أخرى يتم استدعاء دوال __init__
الخاصة بها قبل __init__
للوحدة المحيطة.
استخدامان نموذجيان لـ __init__
هما استدعاء دوال التهيئة في وقت التشغيل لمكتبات C الخارجية وتهيئة الثوابت العالمية التي تتضمن مؤشرات تم إرجاعها بواسطة المكتبات الخارجية. على سبيل المثال، افترض أننا نستدعي مكتبة C libfoo
التي تتطلب منا استدعاء دالة التهيئة foo_init()
في وقت التشغيل. افترض أيضًا أننا نريد تعريف ثابت عالمي foo_data_ptr
الذي يحتفظ بقيمة الإرجاع لدالة void *foo_data()
المعرفة بواسطة libfoo
– يجب تهيئة هذا الثابت في وقت التشغيل (وليس في وقت الترجمة) لأن عنوان المؤشر سيتغير من تشغيل إلى آخر. يمكنك تحقيق ذلك من خلال تعريف دالة __init__
التالية في وحدتك:
const foo_data_ptr = Ref{Ptr{Cvoid}}(0)
function __init__()
ccall((:foo_init, :libfoo), Cvoid, ())
foo_data_ptr[] = ccall((:foo_data, :libfoo), Ptr{Cvoid}, ())
nothing
end
لاحظ أنه من الممكن تمامًا تعريف متغير عالمي داخل دالة مثل __init__
؛ هذه واحدة من مزايا استخدام لغة ديناميكية. ولكن من خلال جعله ثابتًا في النطاق العالمي، يمكننا التأكد من أن النوع معروف للمترجم والسماح له بتوليد كود محسن بشكل أفضل. من الواضح أن أي متغيرات عالمية أخرى في وحدتك تعتمد على foo_data_ptr
يجب أيضًا أن يتم تهيئتها في __init__
.
الثوابت التي تتعلق بمعظم كائنات جوليا التي لم يتم إنتاجها بواسطة ccall
لا تحتاج إلى أن توضع في __init__
: يمكن تجميع تعريفاتها مسبقًا وتحميلها من صورة الوحدة المخزنة. يشمل ذلك الكائنات المعقدة المخصصة في الذاكرة مثل المصفوفات. ومع ذلك، يجب استدعاء أي روتين يعيد قيمة مؤشر خام في وقت التشغيل لكي تعمل التجميع المسبق (Ptr
ستتحول إلى مؤشرات فارغة ما لم تكن مخفية داخل كائن isbits
). يشمل ذلك قيم الإرجاع من دوال جوليا @cfunction
و pointer
.
أنواع القواميس والمجموعات، أو بشكل عام أي شيء يعتمد على ناتج دالة hash(key)
، هي حالة أكثر تعقيدًا. في الحالة الشائعة حيث تكون المفاتيح أرقامًا، أو سلاسل نصية، أو رموزًا، أو نطاقات، أو Expr
، أو تركيبات من هذه الأنواع (عبر المصفوفات، أو الأزواج، أو المجموعات، أو الأزواج، إلخ) فهي آمنة للتجميع المسبق. ومع ذلك، بالنسبة لبعض أنواع المفاتيح الأخرى، مثل Function
أو DataType
وأنواع المستخدم المعرفة بشكل عام حيث لم تقم بتعريف دالة hash
، تعتمد دالة hash
الاحتياطية على عنوان الذاكرة للكائن (عبر objectid
الخاص به) وبالتالي قد تتغير من تشغيل إلى آخر. إذا كان لديك أحد هذه الأنواع من المفاتيح، أو إذا لم تكن متأكدًا، لتكون في أمان يمكنك تهيئة هذه القاموس من داخل دالة __init__
الخاصة بك. بدلاً من ذلك، يمكنك استخدام نوع القاموس IdDict
، الذي يتم التعامل معه بشكل خاص من قبل التجميع المسبق بحيث يكون آمنًا للتهيئة في وقت التجميع.
عند استخدام ما قبل التجميع، من المهم الحفاظ على فهم واضح للتمييز بين مرحلة التجميع ومرحلة التنفيذ. في هذا الوضع، سيكون من الواضح غالبًا أن جوليا هي مترجم يسمح بتنفيذ كود جوليا عشوائي، وليس مفسرًا مستقلًا يقوم أيضًا بإنشاء كود مجمع.
تشمل السيناريوهات المحتملة الأخرى للفشل المعروفة:
عدادات عالمية (على سبيل المثال، لمحاولة تحديد الكائنات بشكل فريد). اعتبر مقتطف الشيفرة التالي:
julia mutable struct UniquedById myid::Int let counter = 0 UniquedById() = new(counter += 1) end end
بينما كانت نية هذا الكود هي إعطاء كل مثيل معرفًا فريدًا، يتم تسجيل قيمة العداد في نهاية التجميع. ستبدأ جميع الاستخدامات اللاحقة لهذه الوحدة المجمعة بشكل تدريجي من نفس قيمة العداد تلك.
لاحظ أن
objectid
(الذي يعمل عن طريق تجزئة مؤشر الذاكرة) لديه مشاكل مشابهة (انظر الملاحظات حول استخدامDict
أدناه).بديل واحد هو استخدام ماكرو لالتقاط
@__MODULE__
وتخزينه بمفرده مع قيمةcounter
الحالية، ومع ذلك، قد يكون من الأفضل إعادة تصميم الكود لعدم الاعتماد على هذه الحالة العالمية.تحتاج المجموعات الترابطية (مثل
Dict
وSet
) إلى إعادة التجزئة في__init__
. (في المستقبل، قد يتم توفير آلية لتسجيل دالة تهيئة.)اعتمادًا على الآثار الجانبية في وقت الترجمة التي تستمر خلال وقت التحميل. تشمل الأمثلة: تعديل المصفوفات أو المتغيرات الأخرى في وحدات جوليا الأخرى؛ الحفاظ على مقبضات للملفات أو الأجهزة المفتوحة؛ تخزين مؤشرات لموارد النظام الأخرى (بما في ذلك الذاكرة)؛
إنشاء "نسخ" غير مقصودة من الحالة العالمية من وحدة أخرى، من خلال الإشارة إليها مباشرة بدلاً من عبر مسار البحث الخاص بها. على سبيل المثال، (في النطاق العالمي):
```julia #mystdout = Base.stdout #= will not work correctly, since this will copy Base.stdout into this module =#
instead use accessor functions:
getstdout() = Base.stdout #= best option =#
or move the assignment into the runtime:
init() = global mystdout = Base.stdout #= also works =# ```
تم فرض العديد من القيود الإضافية على العمليات التي يمكن القيام بها أثناء تجميع الكود مسبقًا لمساعدة المستخدم في تجنب حالات السلوك الخاطئ الأخرى:
- استدعاء
eval
للتسبب في تأثير جانبي في وحدة أخرى. سيؤدي هذا أيضًا إلى إصدار تحذير عند تعيين علامة ما قبل التجميع التزايدي. global const
بيانات من النطاق المحلي بعد بدء__init__()
(انظر المشكلة #12010 لخطط إضافة خطأ لذلك)- استبدال وحدة هو خطأ في وقت التشغيل أثناء القيام بعملية تجميع مسبق تدريجي.
بعض النقاط الأخرى التي يجب أن تكون على دراية بها:
- لا يتم إعادة تحميل الكود / إبطال ذاكرة التخزين المؤقت بعد إجراء تغييرات على ملفات المصدر نفسها، (بما في ذلك بواسطة
Pkg.update
)، ولا يتم إجراء أي تنظيف بعدPkg.rm
- يتم تجاهل سلوك مشاركة الذاكرة لمصفوفة معاد تشكيلها بواسطة ما قبل التجميع (كل عرض يحصل على نسخته الخاصة)
- Expecting the filesystem to be unchanged between compile-time and runtime e.g.
@__FILE__
/source_path()
to find resources at runtime, or the BinDeps@checked_lib
macro. Sometimes this is unavoidable. However, when possible, it can be good practice to copy resources into the module at compile-time so they won't need to be found at runtime. - أجسام
WeakRef
والمُنهِيات لا تُعالج حاليًا بشكل صحيح بواسطة المُسجِّل (سيتم إصلاح ذلك في إصدار قادم). - من الأفضل عادةً تجنب التقاط الإشارات إلى حالات كائنات البيانات الوصفية الداخلية مثل
Method
وMethodInstance
وMethodTable
وTypeMapLevel
وTypeMapEntry
وحقول تلك الكائنات، حيث يمكن أن يؤدي ذلك إلى إرباك المُسلسل وقد لا يؤدي إلى النتيجة التي ترغب فيها. ليس من الضروري أن يكون هذا خطأ، ولكن عليك ببساطة أن تكون مستعدًا لأن النظام سيحاول نسخ بعض هذه الكائنات وإنشاء حالة فريدة واحدة من غيرها.
أحيانًا يكون من المفيد أثناء تطوير الوحدة إيقاف التشغيل التدريجي للتجميع المسبق. يتيح لك علم سطر الأوامر --compiled-modules={yes|no|existing}
تبديل التجميع المسبق للوحدات تشغيلًا وإيقافًا. عندما يتم بدء تشغيل جوليا مع --compiled-modules=no
، يتم تجاهل الوحدات المسلسلة في ذاكرة التخزين المؤقت للتجميع عند تحميل الوحدات واعتماديات الوحدات. في بعض الحالات، قد ترغب في تحميل وحدات مسبقة التجميع موجودة، ولكن دون إنشاء وحدات جديدة. يمكن القيام بذلك عن طريق بدء جوليا مع --compiled-modules=existing
. يتوفر تحكم أكثر دقة مع --pkgimages={yes|no|existing}
، والذي يؤثر فقط على تخزين الشيفرة الأصلية أثناء التجميع المسبق. لا يزال يمكن استدعاء Base.compilecache
يدويًا. يتم تمرير حالة علم سطر الأوامر هذا إلى Pkg.build
لتعطيل تشغيل التجميع المسبق التلقائي عند تثبيت أو تحديث أو بناء الحزم بشكل صريح.
يمكنك أيضًا تصحيح بعض أخطاء ما قبل التجميع باستخدام متغيرات البيئة. قد يساعد تعيين JULIA_VERBOSE_LINKING=true
في حل مشكلات الربط لمكتبات المشاركة من التعليمات البرمجية الأصلية المجمعة. راجع قسم وثائق المطور في دليل جوليا، حيث ستجد مزيدًا من التفاصيل في القسم الذي يوثق البنية الداخلية لجوليا تحت "صور الحزمة".