كل ما تحتاج لمعرفته حسب المرجع مقابل القيمة

عندما يتعلق الأمر بهندسة البرمجيات ، فهناك بعض المفاهيم الخاطئة والمصطلحات التي يساء استخدامها. بالرجوع مقابل القيمة هي بالتأكيد واحدة منهم.

أتذكر مرة أخرى في اليوم الذي قرأت فيه الموضوع ويبدو أن كل مصدر ذهبت إليه يتناقض مع المصدر السابق. استغرق الأمر بعض الوقت للحصول على فهم قوي منه. لم يكن لدي أي خيار لأنه موضوع أساسي إذا كنت مهندس برامج.

لقد واجهت خللًا سيئًا قبل بضعة أسابيع وقررت أن أكتب مقالًا حتى يتمكن الآخرون من قضاء وقت أسهل في اكتشاف هذا الأمر برمته.

أنا رمز في روبي على أساس يومي. أستخدم JavaScript أيضًا في أغلب الأحيان ، لذلك اخترت هاتين اللغتين لهذا العرض التقديمي.

لفهم جميع المفاهيم على الرغم من أننا سوف نستخدم بعض الأمثلة Go و Perl كذلك.

لفهم الموضوع بالكامل ، عليك فهم 3 أشياء مختلفة:

  • كيف يتم تطبيق بنيات البيانات الأساسية في اللغة (الكائنات ، الأنواع البدائية ، قابلية التحويل ،).
  • كيف متغير الاحالة / نسخ / إعادة التعيين / المقارنة العمل
  • كيف يتم تمرير المتغيرات إلى الوظائف

أنواع البيانات الأساسية

في روبي لا توجد أنواع بدائية وكل شيء كائن بما في ذلك الأعداد الصحيحة والمنطقية.

ونعم هناك TrueClass في روبي.

true.is_a؟ (TrueClass) => true
3.is_a؟ (Integer) => صحيح
true.is_a؟ (Object) => true
3.is_a؟ (Object) => true
TrueClass.is_a؟ (Object) => true
Integer.is_a؟ (Object) => true

يمكن أن تكون هذه الكائنات قابلة للتغيير أو غير قابلة للتغيير.

يعني غير قابل للتغيير أنه لا توجد طريقة يمكنك تغيير الكائن بمجرد إنشائه. هناك مثيل واحد فقط لقيمة معينة مع object_id واحد ويبقى كما هو بغض النظر عن ما تفعله.

تبعًا للإعدادات الافتراضية في روبي ، فإن أنواع الكائنات الثابتة هي: منطقية ، رقمية ، لا شيء ، ورمز.

في التصوير بالرنين المغناطيسي ، يشبه object_id للكائن القيمة VALUE التي تمثل الكائن على المستوى C. بالنسبة لمعظم أنواع الكائنات ، فإن VALUE هي مؤشر إلى موقع في الذاكرة حيث يتم تخزين بيانات الكائن الفعلية.

من الآن فصاعدًا ، سوف نستخدم object_id وعنوان الذاكرة بالتبادل.

لنقم بتشغيل بعض رموز روبي في التصوير بالرنين المغناطيسي للحصول على رمز ثابت وسلسلة قابلة للتغيير:

: symbol.object_id => 808668
: symbol.object_id => 808668
'string'.object_id => 70137215233780
'string'.object_id => 70137215215120

كما ترى بينما يحتفظ إصدار الرمز بالكائن نفسه بنفس القيمة ، فإن قيم السلسلة تنتمي إلى عناوين ذاكرة مختلفة.

على عكس روبي ، يحتوي JavaScript على أنواع بدائية.

هم - منطقية ، خالية ، غير محددة ، سلسلة ، ورقم.

تندرج بقية أنواع البيانات تحت مظلة الكائنات (Array و Function و Object). لا يوجد شيء خيالي هنا فهي طريقة أكثر وضوحًا من روبي.

[] مثيل صفيف => صحيح
[] مثيل كائن => صحيح
3 مثيل كائن => خطأ

الاحالة المتغيرة والنسخ وإعادة التكليف والمقارنة

في روبي كل متغير هو مجرد إشارة إلى كائن (لأن كل شيء كائن).

a = 'string'
ب = أ
# إذا قمت بإعادة التعيين بنفس القيمة
a = 'string'
يضع b => 'string'
يضع == b => القيم الحقيقية # هي نفسها
يضع a.object_id == b.object_id => false # memory adr-s. اختلف
# إذا قمت بإعادة تعيين قيمة أخرى
a = 'سلسلة جديدة'
يضع => "سلسلة جديدة"
يضع b => 'string'
يضع == b => قيم خاطئة مختلفة
يضع a.object_id == b.object_id => false # memory adr-s. تختلف أيضا

عندما تقوم بتعيين متغير ، فهذا مرجع إلى كائن وليس الكائن نفسه. عندما تقوم بنسخ كائن b = سيشير كلا المتغيرين إلى نفس العنوان.

يسمى هذا السلوك نسخة حسب القيمة المرجعية.

بالمعنى الدقيق للكلمة في روبي وجافا سكريبت يتم نسخ كل شيء من حيث القيمة.

عندما يتعلق الأمر بالكائنات ، فإن القيم هي عناوين الذاكرة لتلك الكائنات. بفضل هذا يمكننا تعديل القيم الموجودة في عناوين الذاكرة هذه. مرة أخرى ، يسمى هذا بنسخة حسب القيمة المرجعية ولكن معظم الناس يشيرون إلى هذا على أنه نسخة حسب المرجع.

ستكون نسخة بالرجوع إليها بعد إعادة تعيين "سلسلة جديدة" ، يشير b إلى نفس العنوان ويكون له نفس قيمة "السلسلة الجديدة".

عندما تعلن b = a ، يشير a و b إلى نفس عنوان الذاكرةبعد إعادة تعيين (a = 'string') ، يشير a و b إلى عناوين ذاكرة مختلفة

الشيء نفسه مع نوع ثابت مثل Integer:

أ = 1
ب = أ
أ = 1
يضع ب => 1
يضع = = b => صحيح # مقارنة من حيث القيمة
يضع a.object_id == b.object_id => صحيح # مقارنة بواسطة الذاكرة adr.

عندما تقوم بإعادة تعيين نفس العدد الصحيح ، فإن عنوان الذاكرة يبقى كما هو حيث أن عدد صحيح معين يكون له نفس الكائن.

كما ترى عند مقارنة أي كائن بآخر ، تتم مقارنته بالقيمة. إذا كنت تريد التحقق مما إذا كانت هي نفس الكائن ، فعليك استخدام object_id.

لنرى نسخة JavaScript:

var a = 'string'؛
var b = a؛
a = 'string'؛ يتم إعادة تعيين # a إلى نفس القيمة
console.log (أ)؛ => "سلسلة"
console.log (ب)؛ => "سلسلة"
console.log (a === b) ؛ => صحيح // مقارنة بالقيمة
فار = [] ؛
var b = a؛
console.log (a === b) ؛ => صحيح
a = [] ؛
console.log (أ)؛ => []
console.log (ب)؛ => []
console.log (a === b) ؛ => خطأ // مقارنة عن طريق عنوان الذاكرة

باستثناء المقارنة - يستخدم JavaScript حسب القيمة للأنواع البدائية والمرجع للكائنات. يبدو أن السلوك هو نفسه كما في روبي.

كذلك ليس تماما.

لن يتم مشاركة القيم الأولية في JavaScript بين متغيرات متعددة. حتى لو قمت بتعيين المتغيرات تساوي بعضها البعض. يضمن كل متغير يمثل قيمة بدائية أن ينتمي إلى موقع ذاكرة فريد.

هذا يعني أن أيا من المتغيرات سوف تشير أبدا إلى نفس عنوان الذاكرة. من المهم أيضًا تخزين القيمة نفسها في موقع الذاكرة الفعلية.

في مثالنا عندما نعلن b = a ، يشير b إلى عنوان ذاكرة مختلف بنفس قيمة "السلسلة" على الفور. لذلك لا تحتاج إلى إعادة تعيين علامة للإشارة إلى عنوان ذاكرة مختلف.

وهذا ما يسمى نسخ من القيمة لأنه لا يوجد لديك الوصول إلى عنوان الذاكرة فقط إلى القيمة.

عندما تعلن a = b ، يتم تعيينها حسب القيمة لذلك يشير a و b إلى عناوين ذاكرة مختلفة

دعونا نرى مثالاً أفضل حيث كل هذا يهم.

في روبي إذا قمنا بتعديل القيمة الموجودة في عنوان الذاكرة ، فكل المراجع التي تشير إلى العنوان ستكون لها نفس القيمة المحدثة:

a = 'x'
ب = أ
a.concat ( 'ص')
يضع => 'س س'
يضع b => 'xy'
b.concat ( 'ض')
يضع => 'xyz'
يضع b => 'xyz'
a = 'z'
يضع => 'ض'
يضع b => 'xyz'
a [0] = 'y'
يضع => 'ص'
يضع b => 'xyz'

قد تعتقد في جافا سكريبت فقط أن قيمة a ستتغير لكن لا. لا يمكنك حتى تغيير القيمة الأصلية لأنه ليس لديك إمكانية الوصول المباشر إلى عنوان الذاكرة.

يمكنك أن تقول أنك قمت بتعيين "x" على ولكن تم تعيينها حسب القيمة بحيث يحتفظ عنوان الذاكرة بالقيمة "x" ، لكن لا يمكنك تغييرها لأنه ليس لديك أي إشارة إليها.

var a = 'x' ؛
var b = a؛
a.concat ( 'ص')؛
console.log (أ)؛ => 'س'
console.log (ب)؛ => 'س'
a [0] = 'z' ؛
console.log (أ)؛ => 'س' ؛

سلوك كائنات جافا سكريبت وتطبيقها هي نفسها كائنات روبي القابلة للتغيير. كل نسخة تكون القيمة المرجعية.

يتم نسخ أنواع جافا سكريبت البدائية حسب القيمة. السلوك هو نفسه مثل كائنات روبي غير القابلة للتغيير التي يتم نسخها حسب القيمة المرجعية.

هاه؟

مرة أخرى ، عندما تنسخ شيئًا ما بالقيمة فهذا يعني أنه لا يمكنك تغيير (تغيير) القيمة الأصلية نظرًا لعدم وجود إشارة إلى عنوان الذاكرة. من منظور رمز الكتابة ، هذا هو نفس الشيء مثل امتلاك كيانات ثابتة لا يمكنك تحورها.

إذا قارنت Ruby و JavaScript ، فإن نوع البيانات الوحيد الذي "يتصرف" بشكل مختلف افتراضيًا هو String (لهذا السبب استخدمنا String في الأمثلة أعلاه).

في روبي هو كائن قابل للتغيير ويتم نسخه / تمريره بالقيمة المرجعية بينما في JavaScript يعد نوعًا بدائيًا ويتم نسخه / تمريره حسب القيمة.

عندما تريد استنساخ (وليس نسخ) كائن ما ، يجب عليك فعله صراحةً باللغتين حتى تتمكن من التأكد من عدم تعديل الكائن الأصلي:

a = {'name': 'Kate'}
ب = أ
ب ['الاسم'] = 'آنا'
يضع => {: name => "Kate"}
var a = {'name': 'Kate'}؛
var b = {... a}؛ // مع بناء جملة ES6 الجديد
b ['name'] = 'Anna'؛
console.log (أ)؛ => {name: "Kate"}

من الأهمية بمكان أن تتذكر هذا وإلا فستواجه بعض الأخطاء السيئة عند استدعاء الرمز الخاص بك أكثر من مرة. مثال جيد سيكون دالة متكررة حيث يمكنك استخدام الكائن كوسيطة.

آخرها هو React (إطار جافا سكريبت الأمامي) حيث يجب عليك دائمًا تمرير كائن جديد لتحديث الحالة حيث تعمل المقارنة على أساس معرف الكائن.

هذا أسرع لأنه ليس عليك الانتقال من سطر إلى سطر لمعرفة ما إذا كان قد تم تغييره.

كيف يتم تمرير المتغيرات إلى الوظائف

يعمل تمرير المتغيرات إلى الوظائف بنفس طريقة النسخ لأنواع البيانات نفسها في معظم اللغات.

في JavaScript يتم نسخ الأنواع البدائية وتمريرها حسب القيمة ويتم نسخ الكائنات وتمريرها حسب القيمة المرجعية.

أعتقد أن هذا هو السبب في أن الناس يتحدثون فقط عن المرور بالقيمة أو المرور بالرجوع ولا يبدو أنهم ذكروا النسخ. أعتقد أنها تفترض نسخ يعمل بنفس الطريقة.

a = 'b'
إخراج def (سلسلة) # مرت حسب القيمة المرجعية
  تم إعادة تعيين السلسلة = 'c' # لذلك لا توجد إشارة إلى الأصل
  يضع الخيط
النهاية
الإخراج (أ) => 'c'
يضع => 'ب'
إخراج def2 (سلسلة) # مرت حسب القيمة المرجعية
  string.concat ('c') # نقوم بتغيير القيمة الموجودة في العنوان
  يضع الخيط
النهاية
الإخراج (أ) => 'bc'
يضع => 'bc'

الآن في JavaScript:

var a = 'b'؛
وظيفة الإخراج (سلسلة) {// مرت بالقيمة
  سلسلة = 'ج' ؛ / إعادة التعيين إلى قيمة أخرى
  console.log (سلسلة)؛
}
الإخراج (أ). => 'ج'
console.log (أ)؛ => 'ب'
دالة الإخراج 2 (السلسلة) {// مرت بالقيمة
  string.concat ( 'ج')؛ // لا يمكننا تعديله دون الرجوع
  console.log (سلسلة)؛
}
output2 (أ)؛ => 'ب'
console.log (أ)؛ => 'ب'

إذا قمت بتمرير كائن (وليس نوعًا بدائيًا كما فعلنا) في JavaScript إلى الوظيفة ، فإنه يعمل بنفس الطريقة مثل مثال Ruby.

لغات اخرى

لقد رأينا بالفعل كيف تعمل عملية النسخ / المرور حسب القيمة ونسخ / تمرير حسب القيمة المرجعية سنرى الآن ما هو تمرير بالرجوع عنه ونكتشف أيضًا كيف يمكننا تغيير الكائنات إذا مررنا بالقيمة.

بينما بحثت عن اللغات المرجعية ، لم أجد الكثير منها وانتهى بي الأمر باختيار بيرل. دعونا نرى كيف يعمل النسخ في بيرل:

بلدي $ س = 'سلسلة' ؛
$ y = $ x ؛
$ x = 'سلسلة جديدة' ؛
طباعة "$ x" ؛ => "سلسلة جديدة"
طباعة "$ y" ؛ => "سلسلة"
my $ a = {data => "string"}؛
بلدي $ b = $ a ؛
$ a -> {data} = "سلسلة جديدة" ؛
طباعة "$ a -> {data} \ n"؛ => "سلسلة جديدة"
طباعة "$ b -> {data} \ n"؛ => "سلسلة جديدة"

حسنًا ، يبدو أن هذا هو نفسه كما في روبي. لم أجد أي دليل ، لكنني أقول أن بيرل يتم نسخه بالقيمة المرجعية لسلسلة.

الآن ، دعونا نتحقق مما يعني بالرجوع:

بلدي $ س = 'سلسلة' ؛
طباعة "$ x" ؛ => "سلسلة"
دون فو {
  $ _ [0] = 'سلسلة جديدة' ؛
  print "$ _ [0]"؛ => "سلسلة جديدة"
}
فو ($ العاشر)؛
طباعة "$ x" ؛ => "سلسلة جديدة"

نظرًا لأن Perl يتم تمريره بالرجوع إليه إذا قمت بإعادة التعيين داخل الوظيفة ، فستغير القيمة الأصلية لعنوان الذاكرة أيضًا.

لتمرير لغة القيمة ، اخترت Go لأنني أعتزم تعميق معرفتي في Go في المستقبل المنظور:

الحزمة الرئيسية
استيراد "fmt"
func changeAddress (a * int) {
  fmt.Println (أ)
  * a = 0 // ضبط قيمة عنوان الذاكرة على 0
}
func changeValue (a int) {
  fmt.Println (أ)
  a = 0 // نقوم بتغيير القيمة داخل الوظيفة
  fmt.Println (أ)
}
func main () {
  a: = 5
  fmt.Println (أ)
  fmt.Println (وأ)
  يتم تغيير changeValue (a) // a بالقيمة
  fmt.Println (أ)
  changeAddress (& a) // يتم تمرير عنوان الذاكرة الخاص بالقيمة
  fmt.Println (أ)
}
عند ترجمة وتشغيل الشفرة ، ستحصل على ما يلي:
0xc42000e328
5
5
0
5
0xc42000e328
0

إذا كنت تريد تغيير قيمة عنوان الذاكرة ، فعليك استخدام مؤشرات وتمرير عناوين الذاكرة حسب القيمة. يحتفظ المؤشر بعنوان ذاكرة القيمة.

ينشئ & عامل التشغيل مؤشرًا لمعامله ويشير العامل * إلى القيمة الأساسية للمؤشر. هذا يعني في الأساس أنك تمرر عنوان ذاكرة بقيمة مع & وقمت بتعيين قيمة عنوان ذاكرة مع *.

خاتمة

كيفية تقييم اللغة:

  1. فهم أنواع البيانات الأساسية في اللغة. قراءة بعض المواصفات واللعب معهم. وعادة ما يتلخص في الأنواع والأشياء البدائية. ثم تحقق مما إذا كانت هذه الكائنات قابلة للتغيير أو غير قابلة للتغيير. تستخدم بعض اللغات أساليب نسخ / تمرير مختلفة لأنواع مختلفة من البيانات.
  2. الخطوة التالية هي المهمة المتغيرة والنسخ وإعادة التعيين والمقارنة. هذا هو الجزء الأكثر أهمية على ما أعتقد. بمجرد الحصول على هذا ، ستتمكن من معرفة ما يحدث. يساعد كثيرا إذا قمت بفحص عناوين الذاكرة عند اللعب.
  3. تمرير المتغيرات إلى الوظائف عادة ما يكون غير خاص. وعادة ما يعمل بنفس الطريقة مثل النسخ في معظم اللغات. بمجرد أن تعرف كيف يتم نسخ المتغيرات وإعادة تعيينها ، فإنك تعرف بالفعل كيف يتم نقلها إلى الوظائف.

اللغات التي استخدمناها هنا:

  • اذهب: تم ​​نسخها وتمريرها حسب القيمة
  • JavaScript: يتم نسخ / تمرير الأنواع البدائية حسب القيمة ، ويتم نسخ الكائنات / تمريرها حسب القيمة المرجعية
  • روبي: تم نسخها وتمريرها حسب القيمة المرجعية + كائنات قابلة للتغيير / غير قابلة للتغيير
  • Perl: تم نسخها بالقيمة المرجعية وتمريرها بالرجوع إليها

عندما يقول الناس أنها مرت بالإشارة ، فإنها عادة ما تعني أنها مرت بالقيمة المرجعية تمرير القيمة المرجعية يعني أن المتغيرات يتم تمريرها حسب القيمة ولكن تلك القيم هي إشارات إلى الكائنات.

كما رأيت ، يستخدم Ruby القيمة المرجعية فقط بينما يستخدم JavaScript إستراتيجية مختلطة. ومع ذلك ، فإن السلوك هو نفسه بالنسبة لجميع أنواع البيانات تقريبًا نظرًا للتطبيق المختلف لهياكل البيانات.

يتم نسخ معظم اللغات الرئيسية وإصدارها بالقيمة أو نسخها وتمريرها بالقيمة المرجعية. لآخر مرة: عادةً ما تسمى القيمة المرجعية بالمرور بالرجوع إليها.

بشكل عام ، يعتبر المرور حسب القيمة أكثر أمانًا لأنك لن تواجه مشكلات نظرًا لأنه لا يمكنك تغيير القيمة الأصلية عن طريق الخطأ. كما أنه أبطأ في الكتابة لأنه يجب عليك استخدام المؤشرات إذا كنت تريد تغيير الكائنات.

إنها نفس الفكرة مثل الكتابة الثابتة مقابل الكتابة الديناميكية - سرعة التطوير على حساب الأمان. كما تفكر في تمرير القيمة عادة ما تكون ميزة من لغات المستوى الأدنى مثل C أو Java أو Go.

عادةً ما يتم استخدام تمرير حسب القيمة المرجعية أو المرجعية من قبل لغات المستوى الأعلى مثل JavaScript و Ruby و Python.

عندما تكتشف لغة جديدة ، تمر العملية كما فعلنا هنا وستفهم كيف تعمل.

هذا ليس موضوعًا سهلاً ولست متأكدًا من أن كل شيء صحيح ما كتبته هنا. إذا كنت تعتقد أنني ارتكبت بعض الأخطاء في هذه المقالة ، فالرجاء إخبارنا في التعليقات.