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

الحجج والمعلمات
المعلمة هي ما نقبله. وصفًا لنوع المعلمة ، وضعنا قيودًا على مجموعة الأنواع التي يمكن نقلها إلينا. بعض الأمثلة:
// function log( id : string | number ) {} // class Logger { constructor( readonly id : Natural ) {} } // class Node< Id extends Number > { id : Id }
الحجة هي ما ننقله. في وقت النقل ، تحتوي الوسيطة دائمًا على نوع معين. ومع ذلك ، في التحليل الثابت ، قد لا يكون هناك نوع معين معروف ، ولهذا السبب يعمل المترجم مرة أخرى مع قيود الكتابة. بعض الأمثلة:
log( 123 ) // new Logger( promptStringOrNumber( 'Enter id' ) ) // new Node( 'root' ) // ,
فرعية
يمكن أن تشكل أنواع التسلسل الهرمي. النوع الفرعي هو حالة خاصة من النوع الفائق . يمكن تكوين نوع فرعي من خلال تضييق مجموعة القيم الممكنة للنوع الفائق. على سبيل المثال ، النوع Natural هو نوع فرعي من عدد صحيح وإيجابي. وجميع الثلاثة أنواع فرعية من ريال مدريد في نفس الوقت. والنوع الرئيسي هو نوع فرعي لكل ما سبق. في الوقت نفسه ، يتداخل النوعان الإيجابي والأعداد الصحيحة ، لكن لا أحد منهما يقيد الآخر.

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

سلم
لمزيد من السرد ، نحن بحاجة إلى تسلسل هرمي صغير من الحيوانات وتسلسل هرمي مماثل من الخلايا.
abstract class Animal {} abstract class Pet extends Animal {} class Cat extends Pet {} class Dog extends Pet {} class Fox extends Animal {} class AnimalCage { content : Animal } class PetCage extends AnimalCage { content : Pet } class CatCage extends PetCage { content : Cat } class DogCage extends PetCage { content : Dog } class FoxCage extends AnimalCage { content : Fox }
كل شيء أدناه هو تضييق من النوع أعلاه. يمكن أن يحتوي القفص الذي يحتوي على حيوان أليف على حيوانات منزلية فقط ، ولكن لا يحتوي على حيوانات برية. يمكن أن يحتوي القفص مع كلب على كلاب فقط.

التغاير
أبسط والأكثر قابلية للفهم هو تقييد النوع الفائق أو التغاير. في المثال التالي ، تكون معلمة الدالة متماثلة للنوع المحدد لها. بمعنى ، يمكن أن تقبل الدالة كل من هذا النوع نفسه وأي نوع فرعي منه ، ولكن لا يمكن أن تقبل الأنواع الفائقة أو الأنواع الأخرى.
function touchPet( cage : PetCage ) : void { log( `touch ${cage.content}` ) } touchPet( new AnimalCage ) // forbid touchPet( new PetCage ) // allow touchPet( new CatCage ) // allow touchPet( new DogCage ) // allow touchPet( new FoxCage ) // forbid

نظرًا لأننا لا نغير أي شيء في القفص ، يمكننا نقل الوظائف بأمان إلى القفص مع القطة ، لأنها ليست أكثر من حالة خاصة من القفص مع حيوان أليف.
contravariant
يصعب فهم تقييد النوع الفرعي أو المخالفة. في المثال التالي ، تكون معلمة الدالة مخالفة للنوع المحدد لها. بمعنى ، يمكن أن تقبل الدالة هذا النوع نفسه وأي نوع من أنواعه الفائقة ، لكن لا يمكن قبول الأنواع الفرعية أو الأنواع الأخرى.
function pushPet( cage : PetCage ) : void { const Pet = random() > .5 ? Cat : Dog cage.content = new Pet } pushPet( new AnimalCage ) // allow pushPet( new PetCage ) // allow pushPet( new CatCage ) // forbid pushPet( new DogCage ) // forbid pushPet( new FoxCage ) // forbid

لا يمكننا تمرير القفص مع القطة ، لأن الوظيفة يمكن أن تضع الكلب هناك ، وهو أمر غير مسموح به. ولكن يمكن نقل القفص مع أي حيوان بأمان ، حيث يمكن وضع كل من القطة والكلب هناك.
ثبات
يمكن أن يكون الحد من النوع الفرعي والنوع الفائق في نفس الوقت. مثل هذه الحالة تسمى الثبات. في المثال التالي ، تكون معلمة الدالة غير ثابتة للنوع المحدد لها. وهذا يعني أن الوظيفة لا تقبل إلا النوع المحدد وليس أكثر.
function replacePet( cage : PetCage ) : void { touchPet( cage ) pushPet( cage ) } replacePet( new AnimalCage ) // forbid replacePet( new PetCage ) // allow replacePet( new CatCage ) // forbid replacePet( new DogCage ) // forbid replacePet( new FoxCage ) // forbid

ترث وظيفة replacePet
قيود تلك الوظائف التي تستخدمها داخليًا: لقد استغرقت القيود المفروضة على النوع من pushPet
، والتقييد على النوع الفرعي بواسطة pushPet
. إذا قدمنا لها قفصًا مع أي حيوان ، فلن تكون قادرة على نقله إلى وظيفة touchPet ، التي لا تعرف كيفية التعامل مع الثعالب (حيوان أليف سوف يعض إصبعه). وإذا قمنا بنقل القفص مع القطة ، فلن تعمل على استدعاء pushPet
.
bivariant
لا يسع المرء إلا أن يذكر الغياب الغريب للقيود - الثنائي. في المثال التالي ، يمكن أن تقبل أي دالة أي نوع فرعي أو نوع فرعي.
function enshurePet( cage : PetCage ) : void { if( cage.content instanceof Pet ) return pushPet( cage ) } replacePet( new AnimalCage ) // allow replacePet( new PetCage ) // allow replacePet( new CatCage ) // allow replacePet( new DogCage ) // allow replacePet( new FoxCage ) // forbid

في ذلك يمكنك نقل القفص مع الحيوان. ثم ستتحقق من وجود حيوان أليف في القفص ، وإلا ستضعه داخل حيوان أليف عشوائي. ويمكنك نقل ، على سبيل المثال ، قفص مع قطة ، ثم لن تفعل أي شيء.
التعميمات
يعتقد البعض أن التباين يرتبط بطريقة ما بالتعميمات. غالبًا لأنه يتم تفسير التباين باستخدام حاويات عامة كمثال. ومع ذلك ، في القصة بأكملها ، ما زلنا لم نحصل على تعميم واحد - إنها فئات محددة تمامًا:
class AnimalCage { content : Animal } class PetCage extends AnimalCage { content : Pet } class CatCage extends PetCage { content : Cat } class DogCage extends PetCage { content : Dog } class FoxCage extends AnimalCage { content : Fox }
تم ذلك لإظهار أن مشاكل التباين ليست مرتبطة بالتعميمات. هناك حاجة إلى التعميمات فقط لتقليل لصق النسخ. على سبيل المثال ، يمكن إعادة كتابة الكود أعلاه من خلال تعميم بسيط:
class Cage<Animal> { content : Animal }
والآن يمكنك إنشاء مثيلات لأي خلايا:
const animalCage = new Cage<Animal>() const petCage = new Cage<Pet>() const catCage = new Cage<Cat>() const dogCage = new Cage<Dog>() const foxCage = new Cage<Fox>()
إعلان القيود
يرجى ملاحظة أن تواقيع جميع الوظائف الأربع المدرجة سابقًا هي نفسها تمامًا:
( cage : PetCage )=> void
بمعنى أن مثل هذا الوصف للمعلمات المقبولة للوظيفة ليس له اكتمال - لا يمكن القول أنه يمكن نقله إلى الوظيفة. حسنًا ، ما لم يكن من الواضح أنه لا يستحق بالتأكيد تمرير القفص مع الثعلب فيه.
لذلك ، توجد في اللغات الحديثة وسيلة للإشارة صراحةً إلى قيود النوع التي تحتوي عليها المعلمة. على سبيل المثال ، معدّلات الدخول والخروج في C #:
interface ICageIn<in T> { T content { set; } } // contravariant generic parameter interface ICageOut<out T> { T content { get; } } // covariant generic parameter interface ICageInOut<T> { T content { get; set; } } // invariant generic parameter
لسوء الحظ ، في C # لكل متغير من المعدلات ، من الضروري أن تبدأ على واجهة منفصلة. بالإضافة إلى ذلك ، كما أفهمها ، فإن التباين في لغة C # لا يمكن تمييزه عمومًا.
معلمات الإخراج
لا يمكن أن تقبل الوظائف فقط ، ولكن أيضًا تُرجع القيم. بشكل عام ، قد لا تكون قيمة الإرجاع واحدة. على سبيل المثال ، خذ وظيفة أخذ قفص مع حيوان أليف وإعادة اثنين من الحيوانات الأليفة.
function getPets( input : PetCage ) : [ Pet , Pet ] { return [ input.content , new Cat ] }
هذه الوظيفة تعادل وظيفة تأخذ ، بالإضافة إلى معلمة إدخال واحدة ، نواتج أخريين.
function getPets( input : PetCage , output1 : PetCage , output2 : PetCage ) : void { output1.content = input.content output2.content = new Cat }
يخصص الكود الخارجي ذاكرة إضافية على المكدس بحيث تضع الوظيفة كل ما تريد العودة إليه. وعند الانتهاء ، سيتمكن رمز الاتصال بالفعل من استخدام هذه الحاويات لأغراضهم الخاصة.

من معادلة هاتين الوظيفتين ، يترتب على ذلك أن القيم التي يتم إرجاعها بواسطة الدالة ، على عكس المعلمات ، تتعارض دائمًا مع نوع الإخراج المحدد. للحصول على وظيفة يمكن أن يكتب لهم ، ولكن لا يمكن القراءة منها.
أساليب الكائن
أساليب الكائن هي وظائف تأخذ مؤشرًا إضافيًا إلى كائن كمعلمة ضمنية. أي أن الدالتين التاليتين متكافئين.
class PetCage { pushPet() : void { const Pet = random() > .5 ? Cat : Dog this.content = new Pet } }
function pushPet( this : PetCage ) : void { const Pet = random() > .5 ? Cat : Dog this.content = new Pet }
ومع ذلك ، من المهم ملاحظة أن الطريقة ، بخلاف الوظيفة العادية ، هي أيضًا عضو في الفصل ، وهو امتداد للنوع. هذا يؤدي إلى حقيقة أن هناك قيود نمط إضافي إضافية تظهر للوظائف التي تستدعي هذه الطريقة:
function fillPetCage( cage : PetCage ) { cage.pushPet() }

لا يمكننا تمرير مثل هذا pushPet
حيث لم يتم تعريف طريقة pushPet
بعد. هذا مشابه لحالة الثبات حيث يوجد قيود من أسفل ومن أعلى. ومع ذلك ، قد يكون موقع طريقة pushPet
أعلى في التسلسل الهرمي. وهذا هو المكان الذي سيتم فيه تقييد الكتابة الفوقية.
Barbara Lisk مبدأ الاستبدال (LSP)
يعتقد الكثير من الناس أن نسبة النوع الفرعي إلى النوع الفرعي لا يتم تحديدها على أساس الطرق المذكورة سابقًا لتضييق وتوسيع النوع ، ولكن بدلاً من ذلك إمكانية استبدال النوع الفرعي في أي مكان يتم فيه استخدام النوع الفائق. يبدو أن سبب هذا الخطأ هو بالتحديد في LSP. ومع ذلك ، دعونا نقرأ تعريف هذا المبدأ بعناية ، مع الانتباه إلى ما هو أساسي وما هو ثانوي:
يجب أن تكون الوظائف التي تستخدم النوع الأساسي قادرة على استخدام أنواع فرعية من النوع الأساسي دون معرفة ذلك ودون انتهاك صحة البرنامج.
بالنسبة للكائنات غير القابلة للتغيير (بما في ذلك تلك التي لا تشير إلى قابلة للتغيير) ، يتم تنفيذ هذا المبدأ تلقائيًا ، لأنه لا يوجد مكان لأخذ قيود النوع الفرعي منه.
مع التحويلات ، يصبح الأمر أكثر صعوبة ، لأن الحالتين التاليتين تستبعدان بشكل متبادل مبدأ LSP:
- تحتوي الفئة
A
على فئة فرعية من B
، حيث يكون الحقل B::foo
عبارة عن نوع فرعي من A::foo
. - تحتوي الفئة
A
على طريقة يمكنها تغيير الحقل A::foo
.
وفقًا لذلك ، هناك ثلاث طرق فقط متبقية:
- منع الكائنات من وراثة من تضييق أنواع الحقول الخاصة بهم. ولكن بعد ذلك يمكنك دفع فيل إلى قفص القط.
- لا تسترشد بـ LSP ، ولكن بتغير كل معلمة لكل وظيفة على حدة. ولكن عليك التفكير كثيرًا وشرح للمترجم أين توجد قيود الكتابة.
- بصق على كل شيء وانتقل إلى
الدير البرمجة الوظيفية ، حيث تكون جميع الكائنات غير قابلة للتغيير ، مما يعني أن قبول المعلمات لها هو متغير للنوع المعلن.
نسخة مطبوعة على الآلة الكاتبة
في البرنامج النصي الزمني ، يكون المنطق بسيطًا: تُعتبر جميع معلمات الدالة متغايرة (وهذا غير صحيح) ، وتعتبر قيم الإرجاع مخالفة (وهذا صحيح). سبق أن أوضح أن معلمات دالة يمكن أن يكون لها أي اختلاف على الإطلاق ، اعتمادًا على ما تقوم به هذه الوظيفة مع هذه المعلمات. لذلك ، هذه هي الحوادث التالية:
abstract class Animal { is! : 'cat' | 'dog' | 'fox' } abstract class Pet extends Animal { is! : 'cat' | 'dog' } class Cat extends Pet { is! : 'cat' } class Dog extends Pet { is! : 'dog' } class Fox extends Animal { is! : 'fox' } class Cage<Animal> { content! : Animal } function pushPet( cage : Cage<Pet> ) : void { const Pet = Math.random() > .5 ? Cat : Dog cage.content = new Pet } pushPet( new Cage<Animal>() ) // forbid to push Pet to Animal Cage :-( pushPet( new Cage<Cat>() ) // allow to push Dog to Cat Cage :-(
لحل هذه المشكلة ، يجب عليك مساعدة المحول البرمجي مع رمز غير بديهي إلى حد ما:
function pushPet< PetCage extends Cage<Animal> >( cage: Cage<Pet> extends PetCage ? PetCage : never ): void { const Pet = Math.random() > .5 ? Cat : Dog cage.content = new Pet } pushPet( new Cage<Animal>() ) // allow :-) pushPet( new Cage<Pet>() ) // allow :-) pushPet( new Cage<Cat>() ) // forbid :-) pushPet( new Cage<Dog>() ) // forbid :-) pushPet( new Cage<Fox>() ) // forbid :-)
جرب على الانترنت
FlowJS
FlowJS لديه نظام نوع أكثر تقدما. على وجه الخصوص ، في وصف النوع ، من الممكن الإشارة إلى تباين المعلمات المعممة ولحقول الكائن. في مثال الخلية لدينا ، يبدو مثل هذا:
class Animal {} class Pet extends Animal {} class Cat extends Pet {} class Dog extends Pet {} class Fox extends Animal {} class Cage< Animal > { content : Animal } function touchPet( cage : { +content : Pet } ) : void { console.log( `touch ${typeof cage.content}` ) } function pushPet( cage: { -content: Pet } ): void { const Pet = Number((0: any)) > .5 ? Cat : Dog cage.content = new Pet } function replacePet( cage : { content : Pet } ) : void { touchPet( cage ) pushPet( cage ) } touchPet( new Cage<Animal> ) // forbid :-) touchPet( new Cage<Pet> ) // allow :-) touchPet( new Cage<Cat> ) // allow :-) touchPet( new Cage<Dog> ) // allow :-) touchPet( new Cage<Fox> ) // forbid :-) pushPet( new Cage<Animal> ) // allow :-) pushPet( new Cage<Pet> ) // allow :-) pushPet( new Cage<Cat> ) // forbid :-) pushPet( new Cage<Dog> ) // forbid :-) pushPet( new Cage<Fox> ) // forbid :-) replacePet( new Cage<Animal> ) // forbid :-) replacePet( new Cage<Pet> ) // allow :-) replacePet( new Cage<Cat> ) // forbid :-) replacePet( new Cage<Dog> ) // forbid :-) replacePet( new Cage<Fox>) // forbid :-)
جرب على الانترنت
التباين هنا لا يمكن تمييزه. لسوء الحظ ، لم أتمكن من إيجاد طريقة لتعيين التباين بشكل أكثر ملاءمة دون وصف أنواع الحقول جميعها بشكل صريح. على سبيل المثال ، شيء مثل هذا:
function pushPet( cage: Contra< Cage<Pet> , 'content' > ): void { const Pet = Number((0: any)) > .5 ? Cat : Dog cage.content = new Pet }
C حاد
تم تصميم C # في الأصل دون أي فهم للاختلاف. ومع ذلك ، في وقت لاحق ، وأضيفت معدلات المعلمة ، مما سمح للمترجم بالتحقق من أنواع الوسائط التي تم تمريرها بشكل صحيح. لسوء الحظ ، استخدام هذه المعدلات مرة أخرى ليست مريحة للغاية.
using System; abstract class Animal {} abstract class Pet : Animal {} class Cat : Pet {} class Dog : Pet {} class Fox : Animal {} interface ICageIn<in T> { T content { set; } } interface ICageOut<out T> { T content { get; } } interface ICageInOut<T> { T content { get; set; } } class Cage<T> : ICageIn<T>, ICageOut<T>, ICageInOut<T> { public T content { get; set; } } public class Program { static void touchPet( ICageOut<Pet> cage ) { Console.WriteLine( cage.content ); } static void pushPet( ICageIn<Pet> cage ) { cage.content = new Dog(); } static void replacePet( ICageInOut<Pet> cage ) { touchPet( cage as ICageOut<Pet> ); pushPet( cage as ICageIn<Pet> ); } void enshurePet( Cage<Pet> cage ) { if( cage.content is Pet ) return; pushPet( cage as ICageIn<Pet> ); } public static void Main() { var animalCage = new Cage<Animal>(); var petCage = new Cage<Pet>(); var catCage = new Cage<Cat>(); var dogCage = new Cage<Dog>(); var foxCage = new Cage<Fox>(); touchPet( animalCage ); // forbid :-) touchPet( petCage ); // allow :-) touchPet( catCage ); // allow :-) touchPet( dogCage ); // allow :-) touchPet( foxCage ); // forbid :-) pushPet( animalCage ); // allow :-) pushPet( petCage ); // allow :-) pushPet( catCage ); // forbid :-) pushPet( dogCage ); // forbid :-) pushPet( foxCage ); // forbid :-) replacePet( animalCage ); // forbid :-) replacePet( petCage ); // allow :-) replacePet( catCage ); // forbid :-) replacePet( dogCage ); // forbid :-) replacePet( foxCage ); // forbid :-) } }
جرب على الانترنت
جافا
بالنسبة إلى Java ، تمت إضافة القدرة على تبديل الاختلافات في وقت متأخر تمامًا وفقط للمعلمات المعممة ، التي ظهرت مؤخرًا نسبيًا. إذا لم يتم تعميم المعلمة ، ثم مشكلة.
abstract class Animal {} abstract class Pet extends Animal {} class Cat extends Pet {} class Dog extends Pet {} class Fox extends Animal {} class Cage<T> { public T content; } public class Main { static void touchPet( Cage<? extends Pet> cage ) { System.out.println( cage.content ); } static void pushPet( Cage<? super Pet> cage ) { cage.content = new Dog(); } static void replacePet(Cage<Pet> cage ) { touchPet( cage ); pushPet( cage ); } void enshurePet( Cage<Pet> cage ) { if( cage.content instanceof Pet ) return; pushPet( cage ); } public static void main(String[] args) { Cage<Animal> animalCage = new Cage<Animal>(); Cage<Pet> petCage = new Cage<Pet>(); Cage<Cat> catCage = new Cage<Cat>(); Cage<Dog> dogCage = new Cage<Dog>(); Cage<Fox> foxCage = new Cage<Fox>(); touchPet( animalCage );
جرب على الانترنت
C ++
يمكن لـ C ++ ، بفضل نظام القوالب القوي ، التعبير عن أشكال مختلفة ، لكن بالطبع هناك الكثير من التعليمات البرمجية.
#include <iostream> #include <typeinfo> #include <type_traits> class Animal {}; class Pet: public Animal {}; class Cat: public Pet {}; class Dog: public Pet {}; class Fox: public Animal {}; template<class T> class Cage { public: T *content; }; template<class T, class = std::enable_if_t<std::is_base_of<Pet, T>::value>> void touchPet(const Cage<T> &cage) { std::cout << typeid(T).name(); } template<class T, class = std::enable_if_t<std::is_base_of<T, Pet>::value>> void pushPet(Cage<T> &cage) { cage.content = new Dog(); } void replacePet(Cage<Pet> &cage) { touchPet(cage); pushPet(cage); } int main(void) { Cage<Animal> animalCage {new Fox()}; Cage<Pet> petCage {new Cat()}; Cage<Cat> catCage {new Cat()}; Cage<Dog> dogCage {new Dog()}; Cage<Fox> foxCage {new Fox()}; touchPet( animalCage ); // forbid :-) touchPet( petCage ); // allow :-) touchPet( catCage ); // allow :-) touchPet( dogCage ); // allow :-) touchPet( foxCage ); // forbid :-) pushPet( animalCage ); // allow :-) pushPet( petCage ); // allow :-) pushPet( catCage ); // forbid :-) pushPet( dogCage ); // forbid :-) pushPet( foxCage ); // forbid :-) replacePet( animalCage ); // forbid :-) replacePet( petCage ); // allow :-) replacePet( catCage ); // forbid :-) replacePet( dogCage ); // forbid :-) replacePet( foxCage ); // forbid :-) return 0; }
جرب على الانترنت
D
لا يملك D أي وسيلة عاقلة للإشارة بوضوح إلى التباين ، لكنه يعرف كيفية استنتاج الأنواع بناءً على استخدامها.
import std.stdio, std.random; abstract class Animal {} abstract class Pet : Animal { string name; } class Cat : Pet {} class Dog : Pet {} class Fox : Animal {} class Cage(T) { T content; } void touchPet( PetCage )( PetCage cage ) { writeln( cage.content.name ); } void pushPet( PetCage )( PetCage cage ) { cage.content = ( uniform(0,2) > 0 ) ? new Dog() : new Cat(); } void replacePet( PetCage )( PetCage cage ) { touchPet( cage ); pushPet( cage); } void main() { Cage!Animal animalCage; Cage!Pet petCage; Cage!Cat catCage; Cage!Dog dogCage; Cage!Fox foxCage; animalCage.touchPet();
جرب على الانترنت
خاتمة
هذا كل شيء الآن. آمل أن تكون المادة المقدمة قد ساعدتك على فهم القيود المفروضة على الأنواع بشكل أفضل ، وكيفية تنفيذها بلغات مختلفة. أفضل في مكان ما ، في مكان ما أسوأ ، في مكان ما بأي حال من الأحوال ، ولكن على العموم - ما إلى ذلك. ربما تقوم أنت بتطوير اللغة التي سيتم بها تنفيذ كل هذا بطريقة مريحة وآمنة. في غضون ذلك ، انضم إلى دردشة البرقية الخاصة بنا ، حيث نناقش أحيانًا المفاهيم النظرية للغات البرمجة .