
في مايو 2017 ، أعلنت Google أن Kotlin أصبحت لغة التطوير الرسمية لنظام Android. ثم سمع شخص ما اسم هذه اللغة لأول مرة ، وكتب عليها شخص ما لفترة طويلة ، ولكن منذ تلك اللحظة أصبح من الواضح أن كل شخص قريب من تطوير Android ملزم الآن بالتعرف عليه. وأعقب ذلك ردود حماسية "أخيرًا!" وسخط رهيب "لماذا نحتاج إلى لغة جديدة؟" ما الذي لم ترضه Java؟ " الخ. الخ.
لقد مر الوقت الكافي منذ ذلك الحين ، وعلى الرغم من أن الجدل حول ما إذا كان Kotlin جيدًا أو سيئًا لم يهدأ بعد ، إلا أنه تم كتابة المزيد والمزيد من التعليمات البرمجية لنظام Android عليه. وحتى المطورين المحافظين يتحولون إليه أيضًا. بالإضافة إلى ذلك ، على الشبكة ، يمكنك التعثر في المعلومات التي تفيد بزيادة سرعة التطوير بعد إتقان هذه اللغة بنسبة 30٪ مقارنة بـ Java.
اليوم ، تمكنت Kotlin بالفعل من التعافي من العديد من أمراض الطفولة ، متضخمة مع الكثير من الأسئلة والإجابات على Stack Overflow. بالعين المجردة ، أصبحت مزاياها ونقاط ضعفها مرئية.
وفي هذه الموجة ، حدثت لي الفكرة لتحليل بالتفصيل العناصر الفردية للغة شابة ولكن شائعة. انتبه إلى النقاط المعقدة وقارنها بجافا من أجل الوضوح والفهم الأفضل. لفهم السؤال أعمق قليلا من هذا يمكن القيام به من خلال قراءة الوثائق. إذا كانت هذه المقالة تثير الاهتمام ، فمن المرجح أنها ستضع الأساس لسلسلة كاملة من المقالات. في غضون ذلك ، سأبدأ بأشياء أساسية إلى حد ما ، ومع ذلك ، تخفي الكثير من المزالق. دعونا نتحدث عن المنشئين والمبدعين في Kotlin.
كما هو الحال في Java ، في Kotlin ، يحدث إنشاء كائنات جديدة - كيانات من نوع معين - عن طريق استدعاء مُنشئ الفئة. يمكنك أيضًا تمرير الحجج إلى المُنشئ ، ويمكن أن يكون هناك العديد من المُنشئين. إذا نظرت إلى هذه العملية من الخارج ، فإن الاختلاف الوحيد من Java هو نقص الكلمة الرئيسية الجديدة عند استدعاء المُنشئ. الآن ألق نظرة أعمق وشاهد ما يحدث داخل الفصل.
يمكن أن يكون للفئة مُنشئات أساسية وثانوية
يتم تعريف المنشئ باستخدام الكلمة الأساسية للمنشئ. إذا لم يكن لدى المنشئ الأساسي معدِّلات وصول وتعليقات توضيحية ، فيمكن حذف الكلمة الرئيسية.
قد لا يكون لدى الفئة منشئي إعلان صراحة. في هذه الحالة ، بعد إعلان الطبقة لا توجد منشآت ، ننتقل على الفور إلى جسم الطبقة. إذا قمنا برسم تشابه مع Java ، فإن هذا يعادل عدم وجود إعلان صريح عن المنشئين ، ونتيجة لذلك سيتم إنشاء المنشئ الافتراضي (بدون معلمات) تلقائيًا في مرحلة التجميع. يبدو كما هو متوقع:
class MyClassA
هذا يعادل الإدخال التالي:
class MyClassA constructor()
ولكن إذا كتبت بهذه الطريقة ، فسيُطلب منك بأدب إزالة المنشئ الأساسي بدون معلمات.
المنشئ الأساسي هو الذي يتم استدعاؤه دائمًا عند إنشاء كائن في حالة وجوده. بينما نأخذ هذا في الاعتبار ، وسوف نحلله بمزيد من التفصيل لاحقًا ، عندما ننتقل إلى المنشئين الثانويين. وفقًا لذلك ، نتذكر أنه إذا لم يكن هناك منشئون على الإطلاق ، في الواقع هناك واحد (أساسي) ، لكننا لا نراه.
على سبيل المثال ، إذا أردنا ألا يحصل المنشئ الأساسي بدون معلمات على وصول عام ، فمع التعديل
private
سنحتاج إلى الإعلان عنه صراحةً باستخدام الكلمة الأساسية للمنشئ.
السمة الرئيسية للمنشئ الأساسي هي أنه لا يحتوي على جسم ، أي لا يمكن أن يحتوي على رمز قابل للتنفيذ. إنه ببساطة يأخذ المعلمات في نفسه ويمررها عميقًا في الفصل لاستخدامها في المستقبل. على مستوى النحو ، يبدو كما يلي:
class MyClassA constructor(param1: String, param2: Int, param3: Boolean)
يمكن استخدام المعلمات التي تم تمريرها بهذه الطريقة لعمليات التهيئة المختلفة ، ولكن ليس أكثر. في شكلها الخالص ، لا يمكننا استخدام هذه الحجج في كود العمل للفئة. ومع ذلك ، يمكننا تهيئة حقول الصف هنا. يبدو هذا:
class MyClassA constructor(val param1: String, var param2: Int, param3: Boolean)
هنا ، يمكن استخدام
param1
و
param2
في التعليمات البرمجية كحقول للفئة ، وهو ما يعادل ما يلي:
class MyClassA constructor(p1: String, p2: Int, param3: Boolean)
حسنًا ، إذا قارنت بـ Java ، فستبدو هكذا (وبالمناسبة ، في هذا المثال يمكنك تقييم مقدار Kotlin الذي يمكنه تقليل كمية الشفرة):
public class MyClassAJava { private final String param1; private Integer param2; public MyClassAJava(String p1, Integer p2, Boolean param3) { this.param1 = p1; this.param2 = p2; } public String getParam1() { return param1; } public Integer getParam2() { return param2; } public void setParam2(final Integer param2) { this.param2 = param2; }
لنتحدث عن مصممين إضافيين. فهم يذكرون أكثر بالمنشئات العادية في Java: فهم يقبلون المعلمات ، وقد يكون لديهم كتلة قابلة للتنفيذ. عند الإعلان عن مُنشئات إضافية ، تكون الكلمة الأساسية للمنشئ مطلوبة. كما ذكرنا سابقًا ، على الرغم من إمكانية إنشاء كائن عن طريق استدعاء مُنشئ إضافي ، يجب أيضًا استدعاء المُنشئ الأساسي (إن وجد) بمساعدة
this
. على مستوى النحو ، يتم تنظيم ذلك على النحو التالي:
class MyClassA(val p1: String) { constructor(p1: String, p2: Int, p3: Boolean) : this(p1) {
على سبيل المثال المنشئ الإضافي ، كما كان ، الوريث الأساسي.
الآن ، إذا أنشأنا كائنًا عن طريق استدعاء مُنشئ إضافي ، فسيحدث ما يلي:
استدعاء منشئ إضافي ؛
استدعاء المنشئ الرئيسي.
تهيئة حقل من الفئة
p1
في المنشئ الرئيسي ؛
تنفيذ التعليمات البرمجية في نص مُنشئ إضافي.
هذا مشابه لمثل هذا البناء في جافا:
class MyClassAJava { private final String param1; public MyClassAJava(String p1) { param1 = p1; } public MyClassAJava(String p1, Integer p2, Boolean param3) { this(p1);
تذكر أنه في Java يمكننا استدعاء مُنشئ من آخر باستخدام
this
فقط في بداية نص المُنشئ. في Kotlin ، تم البت في هذه المشكلة بشكل جذري - جعلوا مثل هذه المكالمة جزءًا من توقيع المنشئ. فقط في حالة ، ألاحظ أنه ممنوع استدعاء أي مُنشئ (أساسي أو إضافي) مباشرة من نص المُنشئ الإضافي.
يجب أن يشير المنشئ الإضافي دائمًا إلى المنشئ الرئيسي (إن وجد) ، ولكن يمكنه القيام بذلك بشكل غير مباشر ، في إشارة إلى مُنشئ إضافي آخر. خلاصة القول هي أنه في نهاية السلسلة ما زلنا نصل إلى الشيء الرئيسي. من الواضح أن تشغيل المنشئين سيحدث في الترتيب العكسي للمصممين الذين يتحولون إلى بعضهم البعض:
class MyClassA(p1: String) constructor(p1: String, p2: Int, p3: Boolean, p4: String) : this(p1, p2, p3)
الآن التسلسل:
- استدعاء منشئ إضافي مع 4 معلمات ؛
- استدعاء منشئ إضافي مع 3 معلمات ؛
- استدعاء المنشئ الأساسي ؛
- تهيئة حقل من الفئة p1 في المنشئ الأساسي ؛
- تنفيذ التعليمات البرمجية في جسم المنشئ مع 3 معلمات ؛
- تنفيذ التعليمات البرمجية في جسم المنشئ مع 4 معلمات.
على أي حال ، لن يتركنا المترجم ننسى أبدًا الوصول إلى المنشئ الأساسي.
يحدث أن الفئة لا تحتوي على مُنشئ أساسي ، في حين أنها قد تحتوي على مُنشئ إضافي واحد أو أكثر. ثم لا يُطلب من المصنّعين الإضافيين الإشارة إلى شخص ما ، ولكن يمكنهم أيضًا الرجوع إلى مُنشئات إضافية أخرى من هذه الفئة. في وقت سابق ، اكتشفنا أن المنشئ الرئيسي ، غير المحدد صراحة ، يتم إنشاؤه تلقائيًا ، ولكن هذا ينطبق على الحالات التي لا يوجد فيها مُنشئون على الإطلاق في الفصل. إذا كان هناك مُنشئ إضافي واحد على الأقل ، فلن يتم إنشاء مُنشئ أساسي بدون معلمات:
class MyClassA {
يمكننا إنشاء كائن فئة عن طريق استدعاء:
val myClassA = MyClassA()
في هذه الحالة:
class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) {
يمكننا إنشاء كائن فقط مع هذه المكالمة:
val myClassA = MyClassA(“some string”, 10, True)
لا يوجد شيء جديد في Kotlin مقارنة بـ Java.
بالمناسبة ، مثل المنشئ الأساسي ، قد لا يكون للمنشئ الإضافي جسمًا إذا كانت مهمته هي تمرير المعلمات إلى منشئين آخرين فقط.
class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) : this(p1, p2, p3, "") constructor(p1: String, p2: Int, p3: Boolean, p4: String) {
من الجدير أيضًا الانتباه إلى حقيقة أنه ، على عكس المنشئ الأساسي ، يحظر تهيئة حقول الصف في قائمة الحجج للمنشئ الإضافي.
على سبيل المثال مثل هذا السجل سيكون غير صالح:
class MyClassA { constructor(val p1: String, var p2: Int, p3: Boolean){
بشكل منفصل ، تجدر الإشارة إلى أن المنشئ الإضافي ، مثل المنشئ الأساسي ، قد يكون بدون معلمات:
class MyClassA { constructor(){
بالحديث عن المنشئين ، لا يسع المرء إلا أن يذكر إحدى ميزات Kotlin المريحة - القدرة على تعيين القيم الافتراضية للحجج.
لنفترض الآن أن لدينا فئة بها العديد من المنشئين الذين لديهم عدد مختلف من الحجج. سأعطي مثالا في جافا:
public class MyClassAJava { private String param1; private Integer param2; private boolean param3; private int param4; public MyClassAJava(String p1) { this (p1, 5); } public MyClassAJava(String p1, Integer p2) { this (p1, p2, true); } public MyClassAJava(String p1, Integer p2, boolean p3) { this(p1, p2, p3, 20); } public MyClassAJava(String p1, Integer p2, boolean p3, int p4) { this.param1 = p1; this.param2 = p2; this.param3 = p3; this.param4 = p4; }
كما تظهر الممارسة ، فإن هذه التصاميم شائعة جدًا. دعونا نرى كيف يمكن كتابة نفس الشيء على Kotlin:
class MyClassA (var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){
الآن ، دعنا نربت Kotlin معًا على مقدار قطع الرمز. بالمناسبة ، بالإضافة إلى تقليل عدد الخطوط ، نحصل على المزيد من النظام. تذكر ، لابد أنك رأيت شيئًا كهذا أكثر من مرة:
public MyClassAJava(String p1, Integer p2, boolean p3) { this(p3, p1, p2, 20); } public MyClassAJava(boolean p1, String p2, Integer p3, int p4) {
عندما ترى هذا ، فأنت تريد العثور على الشخص الذي كتبه ، والتقاطه عن طريق زر ، وإحضاره إلى الشاشة واسأل بصوت حزين: "لماذا؟"
على الرغم من أنه يمكنك تكرار هذا الإنجاز على Kotlin ، ولكن ليس ضروريًا.
ومع ذلك ، هناك تفاصيل واحدة مفادها أنه في حالة مثل هذا التدوين المختصر على Kotlin ، من الضروري أن تأخذ في الاعتبار: إذا أردنا استدعاء المُنشئ بالقيم الافتراضية من Java ، فيجب علينا إضافة التعليق التوضيحي
@JvmOverloads
إليه:
class MyClassA @JvmOverloads constructor(var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20)
خلاف ذلك ، حصلنا على خطأ.
الآن لنتحدث
عن المُهيئين .
المُهيئ هو كتلة من التعليمات البرمجية المميزة بالكلمة الرئيسية
init
. في هذه الكتلة ، يمكنك تنفيذ بعض المنطق لتهيئة عناصر الفئة ، بما في ذلك استخدام قيم الوسيطات التي جاءت في المنشئ الأساسي. يمكننا أيضًا استدعاء وظائف من هذه الكتلة.
تحتوي Java أيضًا على كتل التهيئة ، ولكنها ليست نفس الشيء. لا يمكننا فيها ، كما هو الحال في Kotlin ، تمرير قيمة من الخارج (حجج المنشئ الأساسي). يشبه المُهيئ إلى حد كبير جسم المنشئ الأساسي ، المأخوذ في كتلة منفصلة. ولكن من النظرة الأولى. في الواقع ، هذا ليس صحيحًا تمامًا. دعنا نحصل على حق.
يمكن أن يوجد المُهيئ أيضًا عندما لا يكون هناك مُنشئ أساسي. إذا كان الأمر كذلك ، فإن الشفرة الخاصة به ، مثل جميع عمليات التهيئة ، يتم تنفيذها قبل رمز المنشئ الإضافي. يمكن أن يكون هناك أكثر من مُهيئ. في هذه الحالة ، سيتزامن ترتيب مكالماتهم مع ترتيب موقعهم في الرمز. لاحظ أيضًا أن تهيئة حقل الفئة يمكن أن تحدث خارج كتل
init
. في هذه الحالة ، تحدث التهيئة أيضًا وفقًا لترتيب العناصر في الشفرة ، ويجب أخذ ذلك في الاعتبار عند استدعاء طرق من كتلة التهيئة. إذا كنت تتعامل معها بلا مبالاة ، فهناك فرصة لحدوث خطأ.
سأعطيك بعض الحالات المثيرة للاهتمام للعمل مع الأولياء.
class MyClassB { init { testParam = "some string" showTestParam() } init { testParam = "new string" } var testParam: String = "after" constructor(){ Log.i("wow", "in constructor testParam = $testParam") } fun showTestParam(){ Log.i("wow", "in showTestParam testParam = $testParam") } }
هذا الرمز صالح تمامًا ، على الرغم من أنه ليس واضحًا تمامًا. إذا نظرت ، يمكنك أن ترى أن تعيين قيمة لحقل
testParam
في كتلة التهيئة يحدث قبل إعلان المعلمة. بالمناسبة ، هذا يعمل فقط إذا كان لدينا مُنشئ إضافي في الصف ، ولكن ليس لدينا مُنشئ أساسي (إذا رفعنا
testParam
حقل
testParam
فوق كتلة
init
،
testParam
بدون مُنشئ). إذا قمنا بفك شفرة كود هذه الفئة في Java ، نحصل على ما يلي:
public class MyClassB { @NotNull private String testParam = "some string"; @NotNull public final String getTestParam() { return this.testParam; } public final void setTestParam(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.testParam = var1; } public final void showTestParam() { Log.i("wow", "in showTestParam testParam = " + this.testParam); } public MyClassB() { this.showTestParam(); this.testParam = "new string"; this.testParam = "after"; Log.i("wow", "in constructor testParam = " + this.testParam); } }
نرى هنا أن المكالمة الأولى للحقل أثناء التهيئة (في كتلة
init
أو خارجها) تعادل التهيئة المعتادة في جافا. يتم نقل جميع الإجراءات الأخرى المرتبطة بتخصيص قيمة أثناء عملية التهيئة ، باستثناء الإجراء الأول (يتم الجمع بين التعيين الأول لقيمة مع الإعلان الميداني) إلى المُنشئ.
إذا أجرينا تجارب على فك التجميع ، اتضح أنه إذا لم يكن هناك منشئ ، فسيتم إنشاء المنشئ الأساسي ، ويحدث كل السحر فيه. إذا كان هناك العديد من
testParam
الإضافيين الذين لا يشيرون إلى بعضهم البعض ، ولا يوجد أحدهم الأساسي ، فعندئذٍ في كود Java في هذه الفئة ،
testParam
تكرار جميع التعيينات اللاحقة لحقل
testParam
في جميع
testParam
الإضافية. إذا كان هناك مُنشئ أساسي ، فعندئذ فقط في الأساسي. فوف ...
والشيء الأكثر إثارة للاهتمام بالنسبة
testParam
:
testParam
نغير توقيع
testParam
من
var
إلى
val
:
class MyClassB { init { testParam = "some string" showTestParam() } init { testParam = "new string" } val testParam: String = "after" constructor(){ Log.i("wow", "in constructor testParam = $testParam") } fun showTestParam(){ Log.i("wow", "in showTestParam testParam = $testParam") } }
وفي مكان ما في الرمز نسميه:
MyClassB myClassB = new MyClassB();
كل شيء تم تجميعه بدون أخطاء ، بدأ ، والآن نرى إخراج السجلات:
في showTestParam testParam = بعض السلاسل
في testParam منشئ = بعد
اتضح أن الحقل المُعلن كقيمة
val
غير القيمة أثناء تنفيذ الكود. لماذا ذلك أعتقد أن هذا عيب في مترجم Kotlin ، وفي المستقبل ، ربما لن يتم تجميعه ، ولكن اليوم كل شيء كما هو.
من خلال استخلاص النتائج من الحالات المذكورة أعلاه ، يمكن للمرء أن ينصح فقط بعدم إنتاج كتل التهيئة وعدم تشتيتها عبر الفصل ، لتجنب التعيين المتكرر للقيم أثناء عملية التهيئة ، لاستدعاء الوظائف النقية فقط من كتل التهيئة. يتم كل هذا لتجنب الارتباك المحتمل.
لذا
إن المُهيئون عبارة عن مجموعة معينة من التعليمات البرمجية التي يجب تنفيذها عند إنشاء كائن ، بغض النظر عن المُنشئ الذي تم إنشاء هذا الكائن به.يبدو أنه تم تسويته. النظر في تفاعل المنشئين والمبدئين. في كل فصل دراسي ، كل شيء بسيط للغاية ، ولكن عليك أن تتذكر:
- استدعاء منشئ إضافي ؛
- استدعاء المنشئ الأساسي ؛
- تهيئة حقول الصف وكتل أداة التهيئة بترتيب موقعها في الكود ؛
- تنفيذ التعليمات البرمجية في نص مُنشئ إضافي.
تبدو الحالات مع الميراث أكثر إثارة للاهتمام.
تجدر الإشارة إلى أن Object هي الأساس لجميع الفئات في Java ، لذا فإن Any موجودة في Kotlin. ومع ذلك ، أي شيء وشيء ليسا الشيء نفسه.
لتبدأ في كيفية عمل الميراث. قد يكون للفئة المتحدرة ، مثل الفئة الرئيسية ، مُنشئ أساسي أو قد لا يكون لها ، ولكن يجب أن تشير إلى مُنشئ محدد للفئة الرئيسية.
إذا كان للفئة السليل مُنشئ أساسي ، فيجب أن يشير هذا المُنشئ إلى مُنشئ محدد للفئة الأساسية. في هذه الحالة ، يجب على جميع المنشئين الإضافيين للفئة اللاحقة الرجوع إلى المُنشئ الرئيسي لفئتهم.
class MyClassC(p1: String): MyClassA(p1) { constructor(p1: String, p2: Int): this(p1) { //some code } //some code }
إذا لم يكن للفئة المتحدرة مُنشئ أساسي ، فيجب على كل مُنشئ إضافي الوصول إلى مُنشئ الفئة الرئيسية باستخدام الكلمة الأساسية
super
. في هذه الحالة ، يمكن للمُنشئين الإضافيين المختلفين للفئة اللاحقة الوصول إلى مُنشئين مختلفين للفئة الرئيسية:
class MyClassC : MyClassA constructor(p1: String, p2: Int): super(p1, p2)
لا تنس أيضًا إمكانية استدعاء مُنشئ الطبقة الأصل بشكل غير مباشر من خلال مُنشئي الفئة المُشتقة:
class MyClassC : MyClassA constructor(p1: String, p2: Int): this (p1)
إذا لم يكن لدى الفئة المتحدرة أي مُنشئ ، فإننا ببساطة نضيف استدعاء المُنشئ للفئة الرئيسية بعد اسم الفئة المنحدرة:
class MyClassC: MyClassA(“some string”) {
ومع ذلك ، لا يزال هناك خيار مع الميراث ، حيث لا يلزم الرجوع إلى مُنشئ الفئة الأصل. مثل هذا السجل صالح:
class MyClassC : MyClassB constructor(p1: String)
ولكن فقط إذا كانت الفئة الرئيسية تحتوي على مُنشئ بدون معلمات ، وهو المُنشئ الافتراضي (الأساسي أو الاختياري - لا يهم).
الآن فكر في أمر استدعاء المبدعين والمنشئين أثناء الميراث:
- استدعاء منشئ الوريث الإضافي ؛
- استدعاء المنشئ الأساسي للوريث ؛
- استدعاء المنشئ الإضافي للوالد ؛
- استدعاء المنشئ الأساسي للوالد ؛
init
كتل init
الرئيسية- تنفيذ كود الجسم من منشئ الوالدين إضافية ؛
- تنفيذ كتلة
init
الأول للوريث ؛ - تنفيذ كود جسم المنشئ الإضافي للوريث.
دعونا نتحدث عن المقارنة مع Java ، حيث لا يوجد في الواقع نظير للمنشئ الأساسي من Kotlin. في Java ، جميع المنشئين هم من الأقران ويمكن استدعاؤهم أو عدم استدعاؤهم من بعضهم البعض. في Java و Kotlin يوجد مُنشئ افتراضي ، وهو مُنشئ بدون معلمات ، ولكنه يكتسب حالة خاصة فقط عند الوراثة. هنا يجدر الانتباه إلى ما يلي: عند الوراثة في Kotlin ، يجب أن نقول صراحة للفصل اللاحق أي مُنشئ للفئة الأم لاستخدامه - لن يسمح المترجم لنا بنسيانها. في Java ، لا يمكننا الإشارة إلى ذلك صراحة. كن حذرًا: في هذه الحالة ، سيتم استدعاء المُنشئ الافتراضي للفئة الرئيسية (إن وجد).
في هذه المرحلة ، سنفترض أننا درسنا المصممين والمبدعين بعمق شديد والآن نعرف كل شيء تقريبًا عنهم. سوف نرتاح قليلاً ونحفر في الاتجاه الآخر!