Scope of Variables
نطاق المتغير هو المنطقة من الشيفرة التي يمكن الوصول فيها إلى المتغير. يساعد تحديد نطاق المتغيرات في تجنب تعارض أسماء المتغيرات. المفهوم بديهي: يمكن لوظيفتين أن تحتويان على معطيات تسمى x
دون أن تشير x
إلى نفس الشيء. وبالمثل، هناك العديد من الحالات الأخرى حيث يمكن لكتل الشيفرة المختلفة استخدام نفس الاسم دون الإشارة إلى نفس الشيء. القواعد التي تحدد متى يشير نفس اسم المتغير إلى نفس الشيء أو لا تشير إليه تُسمى قواعد النطاق؛ هذه القسم يوضحها بالتفصيل.
تقدم بعض البنى في اللغة كتل النطاق، وهي مناطق من الشيفرة التي يمكن أن تكون نطاقًا لمجموعة معينة من المتغيرات. لا يمكن أن يكون نطاق المتغير مجموعة عشوائية من أسطر المصدر؛ بدلاً من ذلك، سيتماشى دائمًا مع واحدة من هذه الكتل. هناك نوعان رئيسيان من النطاقات في جوليا، النطاق العالمي و النطاق المحلي. يمكن أن يكون الأخير متداخلًا. هناك أيضًا تمييز في جوليا بين البنى التي تقدم "نطاقًا صارمًا" وتلك التي تقدم فقط "نطاقًا ناعمًا"، مما يؤثر على ما إذا كان shadowing يُسمح بمتغير عالمي بنفس الاسم أم لا.
Scope constructs
الإنشاءات التي تقدم كتل النطاق هي:
Construct | Scope type | Allowed within |
---|---|---|
module , baremodule | global | global |
struct | local (soft) | global |
for , while , try | local (soft) | global, local |
macro | local (hard) | global |
functions, do blocks, let blocks, comprehensions, generators | local (hard) | global, local |
من الجدير بالذكر أن ما يفتقر إليه هذا الجدول هو begin blocks و if blocks التي لا تقدم مجالات جديدة. تتبع الأنواع الثلاثة من المجالات قواعد مختلفة إلى حد ما والتي سيتم شرحها أدناه.
تستخدم جوليا lexical scoping، مما يعني أن نطاق الدالة لا يرث من نطاق المتصل بها، بل من النطاق الذي تم تعريف الدالة فيه. على سبيل المثال، في الكود التالي، تشير x
داخل foo
إلى x
في النطاق العالمي لوحدتها Bar
:
julia> module Bar
x = 1
foo() = x
end;
وليس هناك x
في النطاق الذي يتم فيه استخدام foo
:
julia> import .Bar
julia> x = -1;
julia> Bar.foo()
1
لذا فإن نطاق المعجم يعني أن ما تشير إليه متغير في قطعة معينة من الشيفرة يمكن استنتاجه من الشيفرة التي تظهر فيها فقط ولا يعتمد على كيفية تنفيذ البرنامج. يمكن لنطاق متداخل داخل نطاق آخر "رؤية" المتغيرات في جميع النطاقات الخارجية التي يحتوي عليها. من ناحية أخرى، لا يمكن للنطاقات الخارجية رؤية المتغيرات في النطاقات الداخلية.
Global Scope
يقدم كل وحدة نطاقًا عالميًا جديدًا، منفصلًا عن النطاق العالمي لجميع الوحدات الأخرى - لا يوجد نطاق عالمي شامل. يمكن للوحدات تقديم متغيرات من وحدات أخرى إلى نطاقها من خلال عبارات using or import أو من خلال الوصول المؤهل باستخدام تدوين النقطة، أي أن كل وحدة هي ما يسمى مساحة اسم بالإضافة إلى كونها بنية بيانات من الدرجة الأولى تربط الأسماء بالقيم.
إذا كان التعبير على المستوى الأعلى يحتوي على إعلان متغير باستخدام الكلمة الرئيسية local
، فإن هذا المتغير غير متاح خارج ذلك التعبير. المتغير داخل التعبير لا يؤثر على المتغيرات العالمية بنفس الاسم. مثال على ذلك هو إعلان local x
في كتلة begin
أو if
على المستوى الأعلى:
julia> x = 1
begin
local x = 0
@show x
end
@show x;
x = 0
x = 1
لاحظ أن الموجه التفاعلي (المعروف أيضًا باسم REPL) في النطاق العالمي للوحدة Main
.
Local Scope
يتم تقديم نطاق محلي جديد بواسطة معظم كتل الشيفرة (انظر أعلاه table للحصول على قائمة كاملة). إذا كانت مثل هذه الكتلة متداخلة نحويًا داخل نطاق محلي آخر، فإن النطاق الذي تنشئه يكون متداخلًا داخل جميع النطاقات المحلية التي تظهر ضمنها، والتي تكون جميعها متداخلة في النهاية داخل النطاق العالمي للوحدة التي يتم تقييم الشيفرة فيها. المتغيرات في النطاقات الخارجية مرئية من أي نطاق تحتويه - مما يعني أنه يمكن قراءتها وكتابتها في النطاقات الداخلية - ما لم يكن هناك متغير محلي بنفس الاسم "يظلل" المتغير الخارجي بنفس الاسم. هذا صحيح حتى إذا تم إعلان النطاق الخارجي بعد (من حيث النص أدناه) كتلة داخلية. عندما نقول إن متغيرًا "يوجد" في نطاق معين، فهذا يعني أن متغيرًا بهذا الاسم موجود في أي من النطاقات التي يتداخل فيها النطاق الحالي، بما في ذلك النطاق الحالي.
تتطلب بعض لغات البرمجة الإعلان عن المتغيرات الجديدة بشكل صريح قبل استخدامها. يعمل الإعلان الصريح في جوليا أيضًا: في أي نطاق محلي، كتابة local x
تعلن عن متغير محلي جديد في ذلك النطاق، بغض النظر عما إذا كان هناك بالفعل متغير باسم x
في نطاق خارجي أم لا. ومع ذلك، فإن إعلان كل متغير جديد بهذه الطريقة يعتبر مفرطًا في الطول ومملًا، لذا تعتبر جوليا، مثل العديد من اللغات الأخرى، أن التعيين لاسم متغير غير موجود بالفعل يعلن ضمنيًا عن ذلك المتغير. إذا كان النطاق الحالي عالميًا، فإن المتغير الجديد يكون عالميًا؛ إذا كان النطاق الحالي محليًا، فإن المتغير الجديد يكون محليًا لأعمق نطاق محلي وسيكون مرئيًا داخل ذلك النطاق ولكن ليس خارجه. إذا قمت بالتعيين لمتغير محلي موجود، فإنه دائمًا يحدث تحديثًا لذلك المتغير المحلي الموجود: يمكنك فقط إخفاء متغير محلي عن طريق الإعلان صراحةً عن متغير محلي جديد في نطاق متداخل باستخدام كلمة local
. بشكل خاص، ينطبق هذا على المتغيرات المعينة في الدوال الداخلية، مما قد يفاجئ المستخدمين القادمين من بايثون حيث يخلق التعيين في دالة داخلية متغيرًا محليًا جديدًا ما لم يتم الإعلان عن المتغير صراحةً ليكون غير محلي.
في الغالب، هذا بديهي إلى حد كبير، ولكن كما هو الحال مع العديد من الأشياء التي تتصرف بشكل بديهي، فإن التفاصيل أكثر دقة مما قد يتخيله المرء بسذاجة.
عندما يحدث x = <value>
في نطاق محلي، تطبق جوليا القواعد التالية لتحديد ما تعنيه العبارة بناءً على مكان حدوث تعبير الإسناد وما يشير إليه x
بالفعل في ذلك الموقع:
المتغير المحلي الموجود: إذا كان
x
متغيرًا محليًا موجودًا بالفعل، فإن المتغير المحلي الموجودx
يتم تعيينه؛نطاق صارم: إذا كان
x
ليس بالفعل متغيرًا محليًا وحدثت عملية تعيين داخل أي بناء نطاق صارم (أي داخل كتلةlet
، أو جسم دالة أو ماكرو، أو فهم، أو مولد)، يتم إنشاء متغير محلي جديد باسمx
في نطاق التعيين؛نطاق ناعم: إذا كان
x
ليس متغيرًا محليًا بالفعل وجميع هياكل النطاق التي تحتوي على التعيين هي نطاقات ناعمة (حلقات، كتلtry
/catch
، أو كتلstruct
)، فإن السلوك يعتمد على ما إذا كان المتغير العالميx
معرفًا:إذا كانت المتغير العالمي
x
غير معرف، يتم إنشاء متغير محلي جديد باسمx
في نطاق التعيين؛إذا كانت المتغير العالمي
x
مُعرفًا، فإن التعيين يُعتبر غامضًا:- في سياقات غير تفاعلية (ملفات، eval)، يتم طباعة تحذير غموض ويتم إنشاء محلي جديد؛
- في السياقات التفاعلية (REPL، دفاتر الملاحظات)، يتم تعيين المتغير العالمي
x
.
يمكنك ملاحظة أنه في السياقات غير التفاعلية، فإن سلوك النطاق الصارم والناعم متطابق باستثناء أنه يتم طباعة تحذير عندما يتداخل متغير محلي ضمني (أي غير مُعلن باستخدام local x
) مع متغير عالمي. في السياقات التفاعلية، تتبع القواعد خوارزمية أكثر تعقيدًا من أجل الراحة. يتم تناول هذا بالتفصيل في الأمثلة التي تلي.
الآن بعد أن عرفت القواعد، دعنا نلقي نظرة على بعض الأمثلة. يُفترض أن يتم تقييم كل مثال في جلسة REPL جديدة بحيث تكون المتغيرات العالمية الوحيدة في كل جزء من الشيفرة هي تلك التي تم تعيينها في تلك الكتلة من الشيفرة.
سنبدأ بموقف واضح ومحدد - تعيين داخل نطاق صارم، في هذه الحالة جسم دالة، عندما لا توجد متغيرات محلية بهذا الاسم بالفعل:
julia> function greet()
x = "hello" # new local
println(x)
end
greet (generic function with 1 method)
julia> greet()
hello
julia> x # global
ERROR: UndefVarError: `x` not defined in `Main`
داخل دالة greet
، تتسبب العبارة x = "hello"
في أن تكون x
متغيرًا محليًا جديدًا في نطاق الدالة. هناك حقيقتان ذات صلة: تحدث العبارة في النطاق المحلي ولا يوجد متغير محلي موجود باسم x
. نظرًا لأن x
محلي، فلا يهم ما إذا كان هناك متغير عالمي باسم x
أم لا. هنا على سبيل المثال نعرف x = 123
قبل تعريف واستدعاء greet
:
julia> x = 123 # global
123
julia> function greet()
x = "hello" # new local
println(x)
end
greet (generic function with 1 method)
julia> greet()
hello
julia> x # global
123
نظرًا لأن x
في greet
محلي، فإن القيمة (أو عدم وجودها) لـ x
العالمية لا تتأثر باستدعاء greet
. قاعدة النطاق الصارمة لا تهتم بما إذا كان هناك اسم عالمي يسمى x
أم لا: التعيين لـ x
في نطاق صارم هو محلي (ما لم يتم إعلان x
كعالمي).
الحالة الواضحة التالية التي سننظر فيها هي عندما يكون هناك بالفعل متغير محلي باسم x
، وفي هذه الحالة x = <value>
دائمًا ما يعين إلى هذا المتغير المحلي الموجود x
. هذا صحيح سواء حدث التعيين في نفس النطاق المحلي، أو في نطاق محلي داخلي في نفس جسم الدالة، أو في جسم دالة متداخلة داخل دالة أخرى، والمعروفة أيضًا باسم closure.
سنستخدم دالة sum_to
، التي تحسب مجموع الأعداد الصحيحة من واحد حتى n
، كمثال:
function sum_to(n)
s = 0 # new local
for i = 1:n
s = s + i # assign existing local
end
return s # same local
end
كما في المثال السابق، فإن التعيين الأول لـ s
في أعلى دالة sum_to
يجعل s
متغيرًا محليًا جديدًا في جسم الدالة. حلقة for
لها نطاق محلي داخلي خاص بها ضمن نطاق الدالة. في النقطة التي يحدث فيها s = s + i
، يكون s
بالفعل متغيرًا محليًا، لذا فإن التعيين يحدث تحديثًا لـ s
الموجود بدلاً من إنشاء متغير محلي جديد. يمكننا اختبار ذلك عن طريق استدعاء sum_to
في REPL:
julia> function sum_to(n)
s = 0 # new local
for i = 1:n
s = s + i # assign existing local
end
return s # same local
end
sum_to (generic function with 1 method)
julia> sum_to(10)
55
julia> s # global
ERROR: UndefVarError: `s` not defined in `Main`
نظرًا لأن s
محلي إلى الدالة sum_to
، فإن استدعاء الدالة ليس له تأثير على المتغير العالمي s
. يمكننا أيضًا أن نرى أن التحديث s = s + i
في حلقة for
يجب أن يكون قد قام بتحديث نفس s
الذي تم إنشاؤه بواسطة التهيئة s = 0
نظرًا لأننا نحصل على المجموع الصحيح 55 للأعداد الصحيحة من 1 إلى 10.
دعونا نتعمق في حقيقة أن جسم حلقة for
له نطاقه الخاص للحظة من خلال كتابة نسخة أكثر تفصيلاً قليلاً والتي سنطلق عليها sum_to_def
، حيث نقوم بحفظ المجموع s + i
في متغير t
قبل تحديث s
:
julia> function sum_to_def(n)
s = 0 # new local
for i = 1:n
t = s + i # new local `t`
s = t # assign existing local `s`
end
return s, @isdefined(t)
end
sum_to_def (generic function with 1 method)
julia> sum_to_def(10)
(55, false)
تُعيد هذه النسخة s
كما في السابق لكنها تستخدم أيضًا ماكرو @isdefined
لإرجاع قيمة منطقية تشير إلى ما إذا كان هناك متغير محلي باسم t
معرف في نطاق المتغيرات المحلية الخارجي. كما ترى، لا يوجد t
معرف خارج جسم حلقة for
. وذلك بسبب قاعدة النطاق الصارم مرة أخرى: نظرًا لأن التعيين لـ t
يحدث داخل دالة، والتي تُدخل نطاقًا صارمًا، فإن التعيين يتسبب في أن يصبح t
متغيرًا محليًا جديدًا في النطاق المحلي حيث يظهر، أي داخل جسم الحلقة. حتى لو كان هناك متغير عالمي باسم t
، فلن يُحدث ذلك فرقًا - قاعدة النطاق الصارم لا تتأثر بأي شيء في النطاق العالمي.
لاحظ أن النطاق المحلي لجسم حلقة for لا يختلف عن النطاق المحلي لدالة داخلية. هذا يعني أنه يمكننا إعادة كتابة هذا المثال بحيث يتم تنفيذ جسم الحلقة كاستدعاء لدالة مساعدة داخلية وتتصرف بنفس الطريقة:
julia> function sum_to_def_closure(n)
function loop_body(i)
t = s + i # new local `t`
s = t # assign same local `s` as below
end
s = 0 # new local
for i = 1:n
loop_body(i)
end
return s, @isdefined(t)
end
sum_to_def_closure (generic function with 1 method)
julia> sum_to_def_closure(10)
(55, false)
هذا المثال يوضح بعض النقاط الرئيسية:
- نطاقات الدوال الداخلية تشبه أي نطاق محلي متداخل آخر. بشكل خاص، إذا كانت المتغيرات محلية بالفعل خارج الدالة الداخلية وقمت بتعيين قيمة لها في الدالة الداخلية، يتم تحديث المتغير المحلي الخارجي.
- لا يهم إذا كان تعريف المتغير المحلي الخارجي يحدث أدناه حيث يتم تحديثه، القاعدة تظل كما هي. يتم تحليل نطاق المتغير المحلي المحيط بالكامل وتحديد المتغيرات المحلية الخاصة به قبل حل معاني المتغيرات المحلية الداخلية.
هذا التصميم يعني أنه يمكنك عمومًا نقل الكود داخل أو خارج دالة داخلية دون تغيير معناه، مما يسهل عددًا من العبارات الشائعة في اللغة باستخدام الإغلاقات (انظر do blocks).
دعونا ننتقل إلى بعض الحالات الأكثر غموضًا التي تغطيها قاعدة النطاق الناعم. سنستكشف ذلك من خلال استخراج أجسام دوال greet
و sum_to_def
إلى سياقات نطاق ناعم. أولاً، دعونا نضع جسم greet
في حلقة for
—وهي ناعمة، بدلاً من صلبة—ونقيمها في REPL:
julia> for i = 1:3
x = "hello" # new local
println(x)
end
hello
hello
hello
julia> x
ERROR: UndefVarError: `x` not defined in `Main`
نظرًا لأن المتغير العالمي x
غير معرف عند تقييم حلقة for
، فإن الشرط الأول لقواعد النطاق اللين ينطبق ويتم إنشاء x
كمتغير محلي لحلقة for
وبالتالي يبقى المتغير العالمي x
غير معرف بعد تنفيذ الحلقة. بعد ذلك، دعنا نعتبر جسم الدالة sum_to_def
المستخرج إلى النطاق العالمي، مع تثبيت معاملها عند n = 10
s = 0
for i = 1:10
t = s + i
s = t
end
s
@isdefined(t)
ما الذي تفعله هذه الشيفرة؟ تلميح: إنها سؤال خدعة. الإجابة هي "يعتمد الأمر." إذا تم إدخال هذه الشيفرة بشكل تفاعلي، فإنها تتصرف بنفس الطريقة التي تتصرف بها في جسم الدالة. ولكن إذا ظهرت الشيفرة في ملف، فإنها تطبع تحذير غموض وترمي خطأ متغير غير معرف. دعنا نرى كيف تعمل في REPL أولاً:
julia> s = 0 # global
0
julia> for i = 1:10
t = s + i # new local `t`
s = t # assign global `s`
end
julia> s # global
55
julia> @isdefined(t) # global
false
ي approximates وجود في جسم دالة من خلال تحديد ما إذا كانت الإسنادات داخل الحلقة تعين إلى متغير عالمي أو تنشئ متغير محلي جديد بناءً على ما إذا كان متغير عالمي بهذا الاسم معرفًا أم لا. إذا كان هناك متغير عالمي بالاسم، فإن الإسناد يقوم بتحديثه. إذا لم يكن هناك متغير عالمي، فإن الإسناد ينشئ متغير محلي جديد. في هذا المثال نرى كلا الحالتين في العمل:
- لا يوجد اسم عالمي
t
، لذا فإنt = s + i
ينشئt
جديدة محلية لحلقةfor
؛ - هناك متغير عالمي باسم
s
، لذا فإنs = t
تعين له.
الحقائق الثانية هي لماذا تغيير تنفيذ الحلقة القيمة العالمية لـ s
والحقائق الأولى هي لماذا t
لا تزال غير معرفة بعد تنفيذ الحلقة. الآن، دعنا نحاول تقييم هذا الرمز نفسه كما لو كان في ملف بدلاً من ذلك:
julia> code = """
s = 0 # global
for i = 1:10
t = s + i # new local `t`
s = t # new local `s` with warning
end
s, # global
@isdefined(t) # global
""";
julia> include_string(Main, code)
┌ Warning: Assignment to `s` in soft scope is ambiguous because a global variable by the same name exists: `s` will be treated as a new local. Disambiguate by using `local s` to suppress this warning or `global s` to assign to the existing global variable.
└ @ string:4
ERROR: LoadError: UndefVarError: `s` not defined in local scope
هنا نستخدم include_string
، لتقييم code
كما لو كان محتويات ملف. يمكننا أيضًا حفظ code
في ملف ثم استدعاء include
على ذلك الملف - ستكون النتيجة هي نفسها. كما ترى، هذا يتصرف بشكل مختلف تمامًا عن تقييم نفس الكود في REPL. دعنا نفصل ما يحدث هنا:
- تم تعريف المتغير العالمي
s
بالقيمة0
قبل تقييم الحلقة - تحدث عملية الإسناد
s = t
في نطاق ناعم - حلقةfor
خارج أي جسم دالة أو بناء نطاق صلب آخر. - لذلك ينطبق البند الثاني من قاعدة النطاق الناعم، والتعيين غير واضح لذا يتم إصدار تحذير.
- تستمر التنفيذ، مما يجعل
s
محليًا لجسم حلقةfor
- نظرًا لأن
s
محلي لدورةfor
، فإنه غير معرف عندما يتم تقييمt = s + i
، مما يتسبب في حدوث خطأ. - يتوقف التقييم هناك، ولكن إذا وصل إلى
s
و@isdefined(t)
، فسيرجع0
وfalse
.
هذا يُظهر بعض الجوانب المهمة للنطاق: في النطاق، يمكن أن يكون لكل متغير معنى واحد فقط، ويتم تحديد هذا المعنى بغض النظر عن ترتيب التعبيرات. وجود التعبير s = t
في الحلقة يجعل s
محليًا للحلقة، مما يعني أنه محلي أيضًا عندما يظهر على الجانب الأيمن من t = s + i
، على الرغم من أن هذا التعبير يظهر أولاً ويتم تقييمه أولاً. قد يتخيل المرء أن s
في السطر الأول من الحلقة يمكن أن يكون عالميًا بينما s
في السطر الثاني من الحلقة محلي، لكن هذا غير ممكن لأن السطرين في نفس كتلة النطاق وكل متغير يمكن أن يعني شيئًا واحدًا فقط في نطاق معين.
On Soft Scope
لقد غطينا الآن جميع قواعد النطاق المحلي، ولكن قبل إنهاء هذا القسم، ربما يجب أن تُقال بضع كلمات حول سبب التعامل مع حالة النطاق الناعم الغامضة بشكل مختلف في السياقات التفاعلية وغير التفاعلية. هناك سؤالان واضحان يمكن طرحهما:
- لماذا لا تعمل مثل REPL في كل مكان؟
- لماذا لا تعمل مثل الملفات في كل مكان؟ وربما نتخطى التحذير؟
في جوليا ≤ 0.6، كانت جميع النطاقات العالمية تعمل مثل REPL الحالي: عندما يحدث x = <value>
في حلقة (أو try
/catch
، أو جسم struct
) ولكن خارج جسم دالة (أو كتلة let
أو فهم)، تم اتخاذ القرار بناءً على ما إذا كان هناك اسم عالمي x
معرف أم لا، سواء كان يجب أن يكون x
محليًا للحلقة. هذه السلوك له ميزة كونه بديهي ومريح لأنه يقارب السلوك داخل جسم الدالة عن كثب قدر الإمكان. على وجه الخصوص، يجعل من السهل نقل الكود ذهابًا وإيابًا بين جسم الدالة وREPL عند محاولة تصحيح سلوك دالة. ومع ذلك، له بعض العيوب. أولاً، إنه سلوك معقد للغاية: كان العديد من الأشخاص على مر السنين مرتبكين بشأن هذا السلوك واشتكى من أنه معقد وصعب الشرح والفهم. نقطة عادلة. ثانيًا، ومن المحتمل أن يكون أسوأ، هو أنه سيء لبرمجة "على نطاق واسع". عندما ترى قطعة صغيرة من الكود في مكان واحد مثل هذا، من الواضح تمامًا ما يحدث:
s = 0
for i = 1:10
s += i
end
من الواضح أن النية هي تعديل المتغير العالمي الموجود s
. ماذا يمكن أن يعني غير ذلك؟ ومع ذلك، ليس كل كود في العالم الحقيقي قصيرًا أو واضحًا جدًا. لقد وجدنا أن الكود مثل ما يلي يحدث غالبًا في البرية:
x = 123
# much later
# maybe in a different file
for i = 1:10
x = "hello"
println(x)
end
# much later
# maybe in yet another file
# or maybe back in the first one where `x = 123`
y = x + 234
من غير الواضح تمامًا ما يجب أن يحدث هنا. نظرًا لأن x + "hello"
هو خطأ في الطريقة، يبدو من المحتمل أن النية هي أن يكون x
محليًا إلى حلقة for
. لكن القيم في وقت التشغيل وما يحدث من طرق لا يمكن استخدامها لتحديد نطاقات المتغيرات. مع سلوك جوليا ≤ 0.6، من المقلق بشكل خاص أن شخصًا ما قد كتب حلقة for
أولاً، وعملت بشكل جيد، ولكن لاحقًا عندما يضيف شخص آخر متغيرًا عالميًا جديدًا بعيدًا—ربما في ملف مختلف—يتغير معنى الكود فجأة إما ليكسر بشكل صاخب أو، والأسوأ من ذلك، يفعل الشيء الخطأ بصمت. هذا النوع من "spooky action at a distance" هو شيء يجب أن تمنعه تصميمات لغات البرمجة الجيدة.
لذا في جوليا 1.0، قمنا بتبسيط القواعد المتعلقة بالنطاق: في أي نطاق محلي، فإن تعيين اسم لم يكن بالفعل متغيرًا محليًا أنشأ متغيرًا محليًا جديدًا. هذا أزال مفهوم النطاق الناعم تمامًا كما أزال إمكانية العمل الغامض. لقد اكتشفنا وأصلحنا عددًا كبيرًا من الأخطاء بسبب إزالة النطاق الناعم، مما يبرر الخيار للتخلص منه. وكان هناك الكثير من الفرح! حسنًا، لا، ليس حقًا. لأن بعض الناس كانوا غاضبين لأنهم الآن كان عليهم كتابة:
s = 0
for i = 1:10
global s += i
end
هل ترى تلك التعليمة global
هناك؟ بشعة. من الواضح أن هذا الوضع لا يمكن تحمله. لكن بجدية، هناك مشكلتان رئيسيتان تتعلقان بمتطلبات global
لهذا النوع من الشيفرة على المستوى العلوي:
- لم يعد من الملائم نسخ ولصق الكود من داخل جسم الدالة إلى REPL لتصحيحه—عليك إضافة تعليقات
global
ثم إزالتها مرة أخرى للعودة؛ - سيكتب المبتدئون هذا النوع من الشيفرات بدون
global
وليس لديهم أي فكرة عن سبب عدم عمل شيفرتهم - الخطأ الذي يحصلون عليه هو أنs
غير معرف، وهو ما لا يبدو أنه ينير أي شخص يحدث أن يرتكب هذا الخطأ.
اعتبارًا من Julia 1.5، يعمل هذا الكود بدون توضيح global
في السياقات التفاعلية مثل REPL أو دفاتر Jupyter (تمامًا مثل Julia 0.6) وفي الملفات والسياقات غير التفاعلية الأخرى، يطبع هذا التحذير المباشر جدًا:
تعيين
s
في النطاق الناعم غير واضح لأن هناك متغير عالمي بنفس الاسم موجود: سيتم اعتبارs
كمتغير محلي جديد. قم بتوضيح ذلك باستخدامlocal s
لقمع هذه التحذيرات أوglobal s
للتعيين على المتغير العالمي الموجود.
هذا يعالج كلا المسألتين مع الحفاظ على فوائد "البرمجة على نطاق واسع" لسلوك 1.0: المتغيرات العالمية ليس لها تأثير غامض على معنى الكود الذي قد يكون بعيدًا؛ في REPL، يعمل النسخ واللصق في تصحيح الأخطاء، ولا يواجه المبتدئون أي مشاكل؛ في أي وقت ينسى فيه شخص ما إشارة global
أو يتسبب عن غير قصد في تظليل متغير عالمي موجود بمتغير محلي في نطاق ناعم، وهو ما سيكون محيرًا على أي حال، يحصلون على تحذير واضح وجميل.
خاصية مهمة في هذا التصميم هي أن أي كود يتم تنفيذه في ملف دون تحذير سيتصرف بنفس الطريقة في REPL جديد. ومن الجهة الأخرى، إذا أخذت جلسة REPL وحفظتها في ملف، وإذا تصرفت بشكل مختلف عما كانت عليه في REPL، فستحصل على تحذير.
Let Blocks
تقوم عبارات let
بإنشاء كتلة نطاق صارم جديدة (انظر أعلاه) وتقدم ارتباطات متغيرة جديدة في كل مرة يتم تشغيلها. لا يلزم أن يتم تعيين المتغير على الفور:
julia> var1 = let x
for i in 1:5
(i == 4) && (x = i; break)
end
x
end
4
بينما قد تعيد التعيينات تعيين قيمة جديدة لموقع قيمة موجود، فإن let
دائمًا ما ينشئ موقعًا جديدًا. هذه الفروق عادةً ليست مهمة، ولا يمكن اكتشافها إلا في حالة المتغيرات التي تعيش لفترة أطول من نطاقها عبر الإغلاقات. صيغة let
تقبل سلسلة من التعيينات وأسماء المتغيرات مفصولة بفواصل:
julia> x, y, z = -1, -1, -1;
julia> let x = 1, z
println("x: $x, y: $y") # x is local variable, y the global
println("z: $z") # errors as z has not been assigned yet but is local
end
x: 1, y: -1
ERROR: UndefVarError: `z` not defined in local scope
تُقيَّم المهام بالترتيب، حيث يتم تقييم كل جانب أيمن في النطاق قبل إدخال المتغير الجديد على الجانب الأيسر. لذلك، من المنطقي كتابة شيء مثل let x = x
حيث أن المتغيرين x
متميزان ولديهما تخزين منفصل. إليك مثال حيث تكون سلوك let
مطلوبًا:
julia> Fs = Vector{Any}(undef, 2); i = 1;
julia> while i <= 2
Fs[i] = ()->i
global i += 1
end
julia> Fs[1]()
3
julia> Fs[2]()
3
هنا نقوم بإنشاء وتخزين إغلاقين يعيدان المتغير i
. ومع ذلك، فإنه دائمًا نفس المتغير i
، لذا فإن الإغلاقين يتصرفان بشكل متطابق. يمكننا استخدام let
لإنشاء ارتباط جديد لـ i
:
julia> Fs = Vector{Any}(undef, 2); i = 1;
julia> while i <= 2
let i = i
Fs[i] = ()->i
end
global i += 1
end
julia> Fs[1]()
1
julia> Fs[2]()
2
نظرًا لأن بناء begin
لا يقدم نطاقًا جديدًا، يمكن أن يكون من المفيد استخدام let
بدون معلمات فقط لتقديم كتلة نطاق جديدة دون إنشاء أي روابط جديدة على الفور:
julia> let
local x = 1
let
local x = 2
end
x
end
1
نظرًا لأن let
يقدم كتلة نطاق جديدة، فإن المتغير المحلي الداخلي x
هو متغير مختلف عن المتغير المحلي الخارجي x
. هذا المثال المحدد يعادل:
julia> let x = 1
let x = 2
end
x
end
1
Loops and Comprehensions
In loops and comprehensions, new variables introduced in their body scopes are freshly allocated for each loop iteration, as if the loop body were surrounded by a let
block, as demonstrated by this example:
julia> Fs = Vector{Any}(undef, 2);
julia> for j = 1:2
Fs[j] = ()->j
end
julia> Fs[1]()
1
julia> Fs[2]()
2
متغير حلقة for
أو متغير تكرار الفهم هو دائمًا متغير جديد:
julia> function f()
i = 0
for i = 1:3
# empty
end
return i
end;
julia> f()
0
ومع ذلك، من المفيد أحيانًا إعادة استخدام متغير محلي موجود كمتغير تكرار. يمكن القيام بذلك بسهولة عن طريق إضافة الكلمة الرئيسية outer
:
julia> function f()
i = 0
for outer i = 1:3
# empty
end
return i
end;
julia> f()
3
Constants
استخدام شائع للمتغيرات هو إعطاء أسماء لقيم محددة وثابتة. يتم تعيين هذه المتغيرات مرة واحدة فقط. يمكن نقل هذه النية إلى المترجم باستخدام الكلمة الرئيسية const
:
julia> const e = 2.71828182845904523536;
julia> const pi = 3.14159265358979323846;
يمكن إعلان متغيرات متعددة في عبارة const
واحدة:
julia> const a, b = 1, 2
(1, 2)
يجب استخدام إعلان const
فقط في النطاق العالمي على المتغيرات العالمية. من الصعب على المترجم تحسين الشيفرة التي تتضمن متغيرات عالمية، حيث قد تتغير قيمها (أو حتى أنواعها) في أي وقت تقريبًا. إذا كان المتغير العالمي لن يتغير، فإن إضافة إعلان const
تحل هذه المشكلة في الأداء.
الثوابت المحلية مختلفة تمامًا. يمكن للمترجم أن يحدد تلقائيًا متى تكون المتغيرات المحلية ثابتة، لذا فإن إعلانات الثوابت المحلية ليست ضرورية، وفي الواقع، فهي غير مدعومة حاليًا.
التعيينات الخاصة على المستوى الأعلى، مثل تلك التي يتم تنفيذها بواسطة كلمات function
و struct
، تكون ثابتة بشكل افتراضي.
لاحظ أن const
تؤثر فقط على ربط المتغير؛ قد يكون المتغير مربوطًا بكائن قابل للتغيير (مثل مصفوفة)، ويمكن تعديل ذلك الكائن. بالإضافة إلى ذلك، عندما يحاول المرء تعيين قيمة لمتغير تم الإعلان عنه كثابت، فإن السيناريوهات التالية ممكنة:
- إذا كانت القيمة الجديدة من نوع مختلف عن نوع الثابت، فسيتم إلقاء خطأ:
julia> const x = 1.0
1.0
julia> x = 1
ERROR: invalid redefinition of constant x
- إذا كانت القيمة الجديدة من نفس نوع الثابت، فسيتم طباعة تحذير:
julia> const y = 1.0
1.0
julia> y = 2.0
WARNING: redefinition of constant y. This may fail, cause incorrect answers, or produce other errors.
2.0
- إذا لم تؤدِ عملية الإسناد إلى تغيير قيمة المتغير، فلن يتم إعطاء أي رسالة:
julia> const z = 100
100
julia> z = 100
100
تنطبق القاعدة الأخيرة على الكائنات غير القابلة للتغيير حتى لو تغير ربط المتغير، على سبيل المثال:
julia> const s1 = "1"
"1"
julia> s2 = "1"
"1"
julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
Ptr{UInt8} @0x00000000132c9638
Ptr{UInt8} @0x0000000013dd3d18
julia> s1 = s2
"1"
julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
Ptr{UInt8} @0x0000000013dd3d18
Ptr{UInt8} @0x0000000013dd3d18
ومع ذلك، بالنسبة للكائنات القابلة للتغيير، يتم طباعة التحذير كما هو متوقع:
julia> const a = [1]
1-element Vector{Int64}:
1
julia> a = [1]
WARNING: redefinition of constant a. This may fail, cause incorrect answers, or produce other errors.
1-element Vector{Int64}:
1
لاحظ أنه على الرغم من أنه قد يكون ممكنًا في بعض الأحيان، إلا أن تغيير قيمة متغير const
يُنصح بشدة بعدم القيام به، وهو مخصص فقط للراحة أثناء الاستخدام التفاعلي. يمكن أن يتسبب تغيير الثوابت في مشاكل مختلفة أو سلوكيات غير متوقعة. على سبيل المثال، إذا كانت طريقة ما تشير إلى ثابت وتم تجميعها بالفعل قبل تغيير الثابت، فقد تستمر في استخدام القيمة القديمة:
julia> const x = 1
1
julia> f() = x
f (generic function with 1 method)
julia> f()
1
julia> x = 2
WARNING: redefinition of constant x. This may fail, cause incorrect answers, or produce other errors.
2
julia> f()
1
Typed Globals
تمت إضافة الدعم للمتغيرات العالمية المخصصة في جوليا 1.8
مماثل للإعلان عن الثوابت، يمكن أيضًا إعلان الروابط العالمية لتكون دائمًا من نوع ثابت. يمكن القيام بذلك إما دون تعيين قيمة فعلية باستخدام الصيغة global x::T
أو عند التعيين كـ x::T = 123
.
julia> x::Float64 = 2.718
2.718
julia> f() = x
f (generic function with 1 method)
julia> Base.return_types(f)
1-element Vector{Any}:
Float64
لأي تعيين إلى متغير عالمي، ستقوم جوليا أولاً بمحاولة تحويله إلى النوع المناسب باستخدام convert
:
julia> global y::Int
julia> y = 1.0
1.0
julia> y
1
julia> y = 3.14
ERROR: InexactError: Int64(3.14)
Stacktrace:
[...]
لا تحتاج النوع إلى أن يكون ملموسًا، ولكن التعليقات التوضيحية مع الأنواع المجردة عادةً ما يكون لها فائدة أداء قليلة.
بمجرد أن يتم تعيين متغير عالمي أو تم تعيين نوعه، لا يُسمح بتغيير نوع الربط:
julia> x = 1
1
julia> global x::Int
ERROR: cannot set type for global x. It already has a value or is already set to a different type.
Stacktrace:
[...]