(ميزة متوقّفة نهائيًا) التحويل إلى لغة Kotlin

‫1. مرحبًا

في هذا الدليل التعليمي حول الرموز البرمجية، ستتعرّف على كيفية تحويل الرمز البرمجي من Java إلى Kotlin. ستتعرّف أيضًا على اصطلاحات لغة Kotlin وكيفية التأكّد من أنّ الرمز البرمجي الذي تكتبه يراعيها.

يناسب هذا الدرس التطبيقي حول الترميز أي مطوّر يستخدم Java ويفكر في نقل مشروعه إلى Kotlin. سنبدأ ببعض فئات Java التي ستحوّلها إلى Kotlin باستخدام IDE. بعد ذلك، سنلقي نظرة على الرمز البرمجي المحوَّل ونرى كيف يمكننا تحسينه من خلال جعله أكثر متوافقًا مع قواعد اللغة وتجنُّب الأخطاء الشائعة.

المُعطيات

ستتعرّف على كيفية تحويل Java إلى Kotlin. وخلال ذلك، ستتعرّف على ميزات لغة Kotlin ومفاهيمه التالية:

  • التعامل مع إمكانية قبول القيم الفارغة
  • تنفيذ العناصر الفردية
  • فئات البيانات
  • التعامل مع السلاسل
  • عامل Elvis
  • إزالة البنية
  • المواقع ومواقع الدعم
  • الوسيطات التلقائية والمَعلمات المُعنوَنة
  • العمل مع المجموعات
  • دوال الإضافات
  • الدوال والمَعلمات على مستوى أعلى
  • الكلمات الرئيسية let وapply وwith وrun

الافتراضات

من المفترض أن تكون على دراية بلغة Java.

المتطلبات

‫2- الإعداد

إنشاء مشروع جديد

إذا كنت تستخدم IntelliJ IDEA، أنشئ مشروع Java جديدًا باستخدام Kotlin/JVM.

إذا كنت تستخدم "استوديو Android"، أنشئ مشروعًا جديدًا باستخدام نموذج ما مِن نشاط. اختَر Kotlin كلغة المشروع. يمكن أن يكون الحد الأدنى لإصدار حزمة تطوير البرامج (SDK) أي قيمة، ولن يؤثر ذلك في النتيجة.

الرمز

سننشئ نموذجًا لكائن User وفئة Repository فردية تعمل مع كائنات User وتُعرِض قوائم بالمستخدمين وأسماء مستخدمين منسَّقة.

أنشئ ملفًا جديدًا باسم User.java ضمن app/java/<yourpackagename> والصق الرمز البرمجي التالي فيه:

public class User {

    @Nullable
    private String firstName;
    @Nullable
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

}

سيُعلمك محرّر بيئة التطوير المتكامل بأنّ @Nullable غير محدّد. لذلك، استورِد androidx.annotation.Nullable إذا كنت تستخدم "استوديو Android" أو org.jetbrains.annotations.Nullable إذا كنت تستخدم IntelliJ.

أنشئ ملفًا جديدًا باسم Repository.java والصق الرمز البرمجي التالي فيه:

import java.util.ArrayList;
import java.util.List;

public class Repository {

    private static Repository INSTANCE = null;

    private List<User> users = null;

    public static Repository getInstance() {
        if (INSTANCE == null) {
            synchronized (Repository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Repository();
                }
            }
        }
        return INSTANCE;
    }

    // keeping the constructor private to enforce the usage of getInstance
    private Repository() {

        User user1 = new User("Jane", "");
        User user2 = new User("John", null);
        User user3 = new User("Anne", "Doe");

        users = new ArrayList();
        users.add(user1);
        users.add(user2);
        users.add(user3);
    }

    public List<User> getUsers() {
        return users;
    }

    public List<String> getFormattedUserNames() {
        List<String> userNames = new ArrayList<>(users.size());
        for (User user : users) {
            String name;

            if (user.getLastName() != null) {
                if (user.getFirstName() != null) {
                    name = user.getFirstName() + " " + user.getLastName();
                } else {
                    name = user.getLastName();
                }
            } else if (user.getFirstName() != null) {
                name = user.getFirstName();
            } else {
                name = "Unknown";
            }
            userNames.add(name);
        }
        return userNames;
    }
}

3- تحديد فئات البيانات وval وvar وnullability

يمكن لبيئة تطوير البرامج المتكاملة (IDE) لدينا تحويل رمز Java إلى رمز Kotlin تلقائيًا بشكل جيد، ولكن في بعض الأحيان تحتاج إلى مساعدة بسيطة. لنسمح لأداة تطوير البرامج (IDE) بإجراء جولة أولية في عملية التحويل. بعد ذلك، سنراجع الرمز الناتج لمعرفة كيفية تحويله بهذه الطريقة وسبب ذلك.

انتقِل إلى ملف User.java واحوِله إلى ملف Kotlin: شريط القوائم -> الرمز البرمجي -> تحويل ملف Java إلى ملف Kotlin.

إذا طلب منك بيئة تطوير البرامج (IDE) تصحيح الأخطاء بعد التحويل، اضغط على نعم.

e6f96eace5dabe5f.png

من المفترض أن يظهر لك رمز Kotlin التالي:

class User(var firstName: String?, var lastName: String?)

يُرجى العلم أنّه تمت إعادة تسمية User.java إلى User.kt. تحتوي ملفات Kotlin على الامتداد ‎ .kt.

في فئة Java User، كانت لدينا سمتان: firstName وlastName. وكان لكل منها طريقة للحصول على القيمة وضبطها، ما يجعل قيمتها قابلة للتغيير. الكلمة الرئيسية في Kotlin للمتغيّرات القابلة للتغيير هي var، لذا يستخدم المحوِّل var لكلّ من هذه السمات. إذا كانت سمات Java تتضمّن أدوات جلب فقط، ستكون للقراءة فقط وسيتمّ تعريفها على أنّها متغيّرات val. تشبه val الكلمة الرئيسية final في Java.

من الاختلافات الرئيسية بين Kotlin وJava أنّ Kotlin تحدّد صراحةً ما إذا كان المتغيّر يمكنه قبول قيمة فارغة. ويتم ذلك من خلال إلحاق ? ببيان النوع.

بما أنّنا وضعنا علامة nullable على firstName وlastName، وضع المحوِّل التلقائي علامة nullable على السمتَين تلقائيًا باستخدام String?. إذا أضفت تعليقًا توضيحيًا على عناصر Java على أنّها غير فارغة (باستخدام org.jetbrains.annotations.NotNull أو androidx.annotation.NonNull)، سيتعرّف المحوِّل على ذلك ويجعل الحقول غير فارغة في Kotlin أيضًا.

سبق أن تمّت عملية الإحالة الناجحة الأساسية. ولكن يمكننا كتابة ذلك بطريقة أكثر تعبيرية. لنطّلِع على كيفية إجراء ذلك.

فئة البيانات

لا تحتوي فئة User إلا على البيانات. تحتوي لغة Kotlin على كلمة رئيسية للفئات التي تؤدي هذا الدور: data. من خلال وضع علامة على هذه الفئة باعتبارها فئة data، سينشئ المُجمِّع تلقائيًا وظائف الحصول على القيم ووظائف ضبطها. وسيؤدي ذلك أيضًا إلى اشتقاق الدوالّ equals() وhashCode() وtoString().

لنضيف الكلمة الرئيسية data إلى فئة User:

data class User(var firstName: String?, var lastName: String?)

يمكن أن تحتوي لغة Kotlin، مثل Java، على دالة أساسية واحدة ودالة أساسية واحدة أو أكثر. إنّ الأسلوب الوارد في المثال أعلاه هو المُنشئ الأساسي لفئة User. إذا كنت تحوّل فئة Java تحتوي على وظائف إنشاء متعددة، سينشئ المحوِّل تلقائيًا وظائف إنشاء متعددة في Kotlin أيضًا. ويتم تحديدها باستخدام الكلمة الرئيسية constructor.

إذا أردنا إنشاء مثيل لهذه الفئة، يمكننا إجراء ذلك على النحو التالي:

val user1 = User("Jane", "Doe")

المساواة

تتضمّن Kotlin نوعَين من المساواة:

  • يستخدم تكافؤ البنية عامل التشغيل == ويُطلِب equals() لتحديد ما إذا كانت النُسختان متساويتين.
  • يستخدم التكافؤ المرجعي عامل التشغيل === ويتحقّق مما إذا كانت الإشارتان تشيران إلى العنصر نفسه.

سيتم استخدام السمات المحدّدة في المنشئ الأساسي لفئة البيانات للتحقّق من المساواة الهيكلية.

val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false

‫4- الوسيطات التلقائية والوسيطات المُسمّاة

في Kotlin، يمكننا تعيين قيم تلقائية للوسائط في استدعاءات الدوال. يتم استخدام القيمة التلقائية عند حذف الوسيطة. في Kotlin، تكون وظائف الإنشاء هي أيضًا دوال، لذا يمكننا استخدام الوسيطات التلقائية لتحديد أنّ القيمة التلقائية لـ lastName هي null. لإجراء ذلك، ما عليك سوى منح null إلى lastName.

data class User(var firstName: String?, var lastName: String? = null)

// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("Joe", "Doe")

تسمح لك Kotlin بتصنيف الوسائط عند استدعاء دوالّك:

val john = User(firstName = "John", lastName = "Doe") 

في حالة استخدام مختلفة، لنفترض أنّ السمة firstName تحتوي على null كقيمة تلقائية ولا تحتوي السمة lastName على قيمة تلقائية. في هذه الحالة، يجب استدعاء الدالة باستخدام وسيطات مُسمّاة لأنّ المَعلمة التلقائية ستسبق مَعلمة بدون قيمة تلقائية:

data class User(var firstName: String? = null, var lastName: String?)

// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")

القيم التلقائية هي مفهوم مهم يتم استخدامه غالبًا في رمز Kotlin. في ملف الترميز، نريد تحديد الاسم الأول واسم العائلة دائمًا في بيان عنصر User، لذلك لا نحتاج إلى قيم تلقائية.

5. إعداد العناصر والعناصر المصاحبة والعناصر الفردية

قبل مواصلة استخدام "مختبر الرموز البرمجية"، تأكَّد من أنّ فئة User هي فئة data. الآن، لنحوِّل فئة Repository إلى Kotlin. من المفترض أن تظهر نتيجة الإحالة الناجحة التلقائية على النحو التالي:

import java.util.*

class Repository private constructor() {
    private var users: MutableList<User?>? = null
    fun getUsers(): List<User?>? {
        return users
    }

    val formattedUserNames: List<String?>
        get() {
            val userNames: MutableList<String?> =
                ArrayList(users!!.size)
            for (user in users) {
                var name: String
                name = if (user!!.lastName != null) {
                    if (user!!.firstName != null) {
                        user!!.firstName + " " + user!!.lastName
                    } else {
                        user!!.lastName
                    }
                } else if (user!!.firstName != null) {
                    user!!.firstName
                } else {
                    "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

    companion object {
        private var INSTANCE: Repository? = null
        val instance: Repository?
            get() {
                if (INSTANCE == null) {
                    synchronized(Repository::class.java) {
                        if (INSTANCE == null) {
                            INSTANCE =
                                Repository()
                        }
                    }
                }
                return INSTANCE
            }
    }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

لنطّلِع على الإجراءات التي اتّخذها المحوِّل التلقائي:

  • قائمة users غير قابلة للتحديد لأنّه لم يتم إنشاء مثيل للكائن في وقت التعريف.
  • يتم تعريف الدوالّ في Kotlin، مثل getUsers()، باستخدام المُعدِّل fun.
  • أصبحت طريقة getFormattedUserNames() الآن خاصية باسم formattedUserNames.
  • التكرار في قائمة المستخدمين (التي كانت في البداية جزءًا من getFormattedUserNames() له بنية مختلفة عن بنية Java
  • أصبح حقل static الآن جزءًا من مجموعة companion object.
  • تمت إضافة عنصر init

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

  • أزِل ? في User? ضمن بيان نوع users.
  • أزِل ? في User? لنوع الإرجاع getUsers() حتى يتم عرض List<User>?.

وحدة الإعداد

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

class Repository private constructor() {
    ...
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

تعالج معظم تعليمات init البرمجية عملية إعداد السمات. ويمكن إجراء ذلك أيضًا في تعريف السمة. على سبيل المثال، في إصدار Kotlin من فئة Repository، نرى أنّه تمّت بدء تشغيل خاصيّة users في البيان.

private var users: MutableList<User>? = null

static السمات والطُرق في Kotlin

في Java، نستخدم الكلمة الرئيسية static للحقول أو الدوال للإشارة إلى أنّها تنتمي إلى فئة ولكن ليس إلى مثيل من الفئة. لهذا السبب، أنشأنا الحقل الثابت INSTANCE في فئة Repository. ويتمثل مكافئ Kotlin في العنصر companion object. يمكنك أيضًا هنا تحديد الحقول الثابتة والدوالّ الثابتة. أنشأ المحوِّل كتلة العنصر المصاحب ونقل الحقل INSTANCE إلى هنا.

معالجة القيم الفردية

بما أنّنا نحتاج إلى نسخة افتراضية واحدة فقط من فئة Repository، استخدمنا نمط العنصر الفردي في Java. باستخدام Kotlin، يمكنك فرض هذا النمط على مستوى المُجمِّع من خلال استبدال الكلمة الرئيسية class بـ object.

أزِل الدالة الإنشائية الخاصة واستبدِل تعريف الفئة بـ object Repository. عليك أيضًا إزالة العنصر المصاحب.

object Repository {

    private var users: MutableList<User>? = null
    fun getUsers(): List<User>? {
       return users
    }

    val formattedUserNames: List<String>
        get() {
            val userNames: MutableList<String> =
                ArrayList(users!!.size)
        for (user in users) {
            var name: String
            name = if (user!!.lastName != null) {
                if (user!!.firstName != null) {
                    user!!.firstName + " " + user!!.lastName
                } else {
                    user!!.lastName
                }
            } else if (user!!.firstName != null) {
                user!!.firstName
            } else {
                "Unknown"
            }
            userNames.add(name)
       }
       return userNames
   }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

عند استخدام فئة object، ما عليك سوى استدعاء الدوالّ والسمات مباشرةً على الكائن، على النحو التالي:

val formattedUserNames = Repository.formattedUserNames

يُرجى العلم أنّه إذا لم يكن للموقع قيمة مُعدِّل مستوى العرض، يكون متاحًا للجميع تلقائيًا، كما هو الحال مع الموقع formattedUserNames في الكائن Repository.

6. التعامل مع إمكانية قبول القيم الفارغة

عند تحويل فئة Repository إلى Kotlin، جعل المحوِّل التلقائي قائمة المستخدمين nullable، لأنّه لم يتمّ إعدادها كعنصر عند الإعلان عنها. ونتيجةً لذلك، يجب استخدام عامل التشغيل !! لتأكيد عدم القيمة الخالية في جميع استخدامات عنصر users. (ستظهر لك users!! وuser!! في الرمز المحوَّل). يحوّل عامل التشغيل !! أي متغيّر إلى نوع غير فارغ، حتى تتمكّن من الوصول إلى السمات أو استدعاء الدوالّ عليه. ومع ذلك، سيتم طرح استثناء إذا كانت قيمة المتغيّر فارغة. باستخدام !!، أنت تخاطر بظهور استثناءات أثناء وقت التشغيل.

بدلاً من ذلك، يُفضَّل التعامل مع قيمة العدم باستخدام إحدى الطريقتَين التاليتَين:

  • إجراء عملية التحقّق من القيمة الخالية ( if (users != null) {...} )
  • استخدام عامل التشغيل elvis ?: (سيتم تناوله لاحقًا في الدرس التطبيقي)
  • استخدام بعض الدوالّ العادية في Kotlin (سيتم تناولها لاحقًا في ورشة رموز البرامج)

في حالتنا، نعلم أنّه ليس من الضروري أن تكون قائمة المستخدمين nullable، لأنّه يتمّ إعدادها مباشرةً بعد إنشاء العنصر (في البنية init). وبالتالي، يمكننا إنشاء مثيل لعنصر users مباشرةً عند تحديده.

عند إنشاء نُسخ من أنواع المجموعات، توفّر Kotlin عدة دوال مساعدة لجعل رمزك أكثر سهولة ومرونة. في ما يلي مثال على استخدام MutableList بدلاً من users:

private var users: MutableList<User>? = null

للتبسيط، يمكننا استخدام الدالة mutableListOf() وتقديم نوع عنصر القائمة. تنشئ mutableListOf<User>() قائمة فارغة يمكنها استيعاب User عنصر. بما أنّه يمكن الآن للمجمِّع استنتاج نوع بيانات المتغيّر، عليك إزالة بيان النوع الصريح للسمة users.

private val users = mutableListOf<User>()

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

بما أنّه سبق أن تمّت بدء عملية إعداد المتغيّر users، عليك إزالة عملية الإعداد هذه من العنصر init:

users = ArrayList<Any?>()

بعد ذلك، من المفترض أن تظهر مجموعة init على النحو التالي:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    users.add(user1)
    users.add(user2)
    users.add(user3)
}

بعد إجراء هذه التغييرات، أصبح الحقل users غير فارغ، ويمكننا إزالة كل مرّات ظهور عامل التشغيل !! غير الضرورية. يُرجى العِلم أنّه سيستمر ظهور أخطاء الترجمة في "استوديو Android"، ولكن عليك مواصلة الخطوات القليلة التالية في ورشات رموز البرامج لحلّها.

val userNames: MutableList<String?> = ArrayList(users.size)
for (user in users) {
    var name: String
    name = if (user.lastName != null) {
        if (user.firstName != null) {
            user.firstName + " " + user.lastName
        } else {
            user.lastName
        }
    } else if (user.firstName != null) {
        user.firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

بالنسبة إلى قيمة userNames، إذا حدّدت نوع ArrayList على أنّه يحتوي على Strings، يمكنك إزالة النوع الصريح في البيان لأنّه سيتم الاستدلال عليه.

val userNames = ArrayList<String>(users.size)

إعادة التنظيم

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

على سبيل المثال، تتيح فئات data إزالة البنية حتى نتمكّن من إزالة بنية عنصر User في حلقة for إلى (firstName, lastName). يتيح لنا ذلك العمل مباشرةً مع قيمتَي firstName وlastName. عدِّل حلقة for كما هو موضّح أدناه. استبدِل كلّ تكرارات user.firstName بـ firstName واستبدِل user.lastName بـ lastName.

for ((firstName, lastName) in users) {
    var name: String
    name = if (lastName != null) {
        if (firstName != null) {
            firstName + " " + lastName
        } else {
            lastName
        }
    } else if (firstName != null) {
        firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

if expression

إنّ الأسماء الواردة في قائمة userNames ليست بالشكل المطلوب بعد. بما أنّ كلاً من lastName وfirstName يمكن أن يكون null، علينا التعامل مع إمكانية عدم توفّر القيمة عند إنشاء قائمة بأسماء المستخدمين المنسَّقة. نريد عرض "Unknown" إذا لم يكن أي من الاسمَين متوفّرًا. بما أنّ المتغيّر name لن يتم تغييره بعد ضبطه مرة واحدة، يمكننا استخدام val بدلاً من var. عليك إجراء هذا التغيير أولاً.

val name: String

اطّلِع على الرمز الذي يضبط متغيّر الاسم. قد يبدو لك الأمر جديدًا أن يتم ضبط متغيّر ليكون مساويًا لوحدة رمز if / else. يُسمح بذلك لأنّ if وwhen هما تعبيران في Kotlin، ويعرضان قيمة. سيتمّ إسناد السطر الأخير من عبارة if إلى name. الغرض الوحيد من هذا الرمز هو إعداد قيمة name.

في الأساس، يشير هذا المنطق الذي تم تقديمه هنا إلى أنّه إذا كانت قيمة lastName فارغة، يتم ضبط name على firstName أو "Unknown".

name = if (lastName != null) {
    if (firstName != null) {
        firstName + " " + lastName
    } else {
        lastName
    }
} else if (firstName != null) {
    firstName
} else {
    "Unknown"
}

عامل التشغيل Elvis

يمكن كتابة هذه التعليمة البرمجية بشكل أكثر شيوعًا باستخدام عامل التشغيل elvis ?:. سيُرجِع عامل elvis التعبير على الجانب الأيسر إذا لم يكن فارغًا، أو التعبير على الجانب الأيمن إذا كان الجانب الأيسر فارغًا.

وبالتالي، في الرمز التالي، يتم عرض firstName إذا لم تكن قيمة فارغة. إذا كانت firstName فارغة، يعرض التعبير القيمة على يسار "Unknown":

name = if (lastName != null) {
    ...
} else {
    firstName ?: "Unknown"
}

7. نماذج السلاسل

تسهّل Kotlin العمل مع String باستخدام نماذج السلاسل. تسمح لك نماذج السلاسل بالإشارة إلى المتغيّرات داخل تعريفات السلاسل باستخدام الرمز $ قبل المتغيّر. يمكنك أيضًا وضع تعبير ضمن تعريف سلسلة، وذلك عن طريق وضع التعبير ضمن { } واستخدام الرمز $ قبله. مثال: ${user.firstName}

يستخدم الرمز البرمجي حاليًا تسلسل سلاسل لجمع firstName وlastName في اسم المستخدم.

if (firstName != null) {
    firstName + " " + lastName
}

بدلاً من ذلك، استبدِل تسلسل السلسلة بما يلي:

if (firstName != null) {
    "$firstName $lastName"
}

يمكن أن يؤدي استخدام نماذج السلاسل إلى تبسيط الرمز البرمجي.

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

من المفترض أن يظهر لك حاليًا تحذير بأنّه يمكن ربط بيان name بالتكليف. لنطبّق ذلك. بما أنّه يمكن استنتاج نوع المتغيّر name، يمكننا إزالة بيان نوع String الصريح. أصبح formattedUserNames الآن على النحو التالي:

val formattedUserNames: List<String?>
    get() {
        val userNames = ArrayList<String>(users.size)
        for ((firstName, lastName) in users) {
            val name = if (lastName != null) {
                if (firstName != null) {
                    "$firstName $lastName"
                } else {
                    lastName
                }
            } else {
                firstName ?: "Unknown"
            }
            userNames.add(name)
        }
        return userNames
    }

يمكننا إجراء تعديل إضافي. يعرض منطق واجهة المستخدم "Unknown" في حال عدم توفّر الاسم الأول واسم العائلة، لذا لا نسمح بعرض عناصر فارغة. وبالتالي، بالنسبة إلى نوع البيانات formattedUserNames، استبدِل List<String?> بـ List<String>.

val formattedUserNames: List<String>

8. العمليات على المجموعات

لنلقِ نظرة فاحصة على دالة الحصول على formattedUserNames ونرى كيف يمكننا جعلها أكثر ملاءمةً للغة. في الوقت الحالي، تؤدي التعليمة البرمجية ما يلي:

  • لإنشاء قائمة جديدة من السلاسل
  • تكرار قائمة المستخدمين
  • تُنشئ الاسم المنسَّق لكل مستخدم استنادًا إلى الاسم الأول واسم العائلة للمستخدم.
  • عرض القائمة التي تم إنشاؤها حديثًا
    val formattedUserNames: List<String>
        get() {
            val userNames = ArrayList<String>(users.size)
            for ((firstName, lastName) in users) {
                val name = if (lastName != null) {
                    if (firstName != null) {
                        "$firstName $lastName"
                    } else {
                        lastName
                    }
                } else {
                    firstName ?: "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

توفّر Kotlin قائمة واسعة من عمليات تحويل المجموعات التي تجعل عملية التطوير أسرع وأكثر أمانًا من خلال توسيع إمكانات Java Collections API. ومن بين هذه الدوالّ دالة map. تعرض هذه الدالة قائمة جديدة تحتوي على نتائج تطبيق دالة التحويل المحدّدة على كل عنصر في القائمة الأصلية. لذا، بدلاً من إنشاء قائمة جديدة والتنقّل في قائمة المستخدمين يدويًا، يمكننا استخدام الدالة map ونقل المنطق الذي كان لدينا في حلقة for داخل محتوى map. تلقائيًا، يكون اسم عنصر القائمة الحالي المستخدَم في map هو it، ولكن لتسهيل القراءة، يمكنك استبدال it باسم المتغيّر الخاص بك. في هذه الحالة، لنسمّيها user:

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                val name = if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
                name
            }
        }

يُرجى العِلم أنّنا نستخدم مشغل Elvis لعرض "Unknown" إذا كان user.lastName فارغًا، لأنّ user.lastName من النوع String? ويجب أن يكون String مطلوبًا للعنصر name.

...
else {
    user.lastName ?: "Unknown"
}
...

لتبسيط ذلك أكثر، يمكننا إزالة المتغيّر name بالكامل:

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

9. المواقع ومواقع الدعم

تبيّن لنا أنّ المحوِّل التلقائي استبدل الدالة getFormattedUserNames() بخاصية باسم formattedUserNames تتضمّن دالة جلب مخصّصة. في الخلفية، لا تزال Kotlin تنشئ دالة getFormattedUserNames() تعرض List.

في Java، يمكننا عرض سمات الفئة من خلال وظائف getter وsetter. تسمح لنا Kotlin بالتمييز بشكل أفضل بين سمات الفئة، التي يتم التعبير عنها باستخدام الحقول، والوظائف، أي الإجراءات التي يمكن للفئة تنفيذها، والتي يتم التعبير عنها باستخدام الدوالّ. في حالتنا، تكون فئة Repository بسيطة جدًا ولا تُجري أي إجراءات، لذا تحتوي على حقول فقط.

يتم الآن تشغيل المنطق الذي تم تشغيله في دالة getFormattedUserNames() في Java عند استدعاء دالة الحصول على سمة formattedUserNames في Kotlin.

على الرغم من أنّنا لا نملك حقلًا يتوافق صراحةً مع السمة formattedUserNames، توفّر لنا Kotlin حقلًا احتياطيًا تلقائيًا باسم field يمكننا الوصول إليه عند الحاجة من خلال وظائف الحصول على القيم وضبطها المخصّصة.

في بعض الأحيان، نحتاج إلى بعض الوظائف الإضافية التي لا يوفّرها الحقل الاحتياطي التلقائي.

لنطّلِع على مثال.

داخل فئة Repository، لدينا قائمة قابلة للتغيير بالمستخدمين يتم عرضها في الدالة getUsers() التي تم إنشاؤها من رمز Java البرمجي:

fun getUsers(): List<User>? {
    return users
}

لأنّنا لم نريد أن يعدّل المُتصلون بفئة Repository قائمة المستخدمين، أنشأنا الدالة getUsers() التي تعرض List<User> للقراءة فقط. في Kotlin، نفضّل استخدام السمات بدلاً من الدوالّ في مثل هذه الحالات. بعبارة أدق، سنعرض List<User> للقراءة فقط يكون مدعومًا بـ mutableListOf<User>.

أولاً، لنعدّل اسم users إلى _users. ميِّز اسم المتغيّر وانقر عليه بزر الماوس الأيمن لإعادة صياغة > إعادة تسمية المتغيّر. بعد ذلك، أضِف موقعًا متاحًا للجميع للقراءة فقط يعرض قائمة بالمستخدمين. لنفترض أنّه users:

private val _users = mutableListOf<User>()
val users: List<User>
    get() = _users

في هذه المرحلة، يمكنك حذف طريقة getUsers().

بعد إجراء التغيير أعلاه، يصبح الموقع الإلكتروني الخاص _users هو الموقع الإلكتروني الأساسي للموقع الإلكتروني العلني users. خارج فئة Repository، لا يمكن تعديل قائمة _users، لأنّ مستخدِمي الفئة لا يمكنهم الوصول إلى القائمة إلا من خلال users.

عند استدعاء users من رمز Kotlin، يتم استخدام تنفيذ List من مكتبة Kotlin العادية، حيث لا يمكن تعديل القائمة. في حال استدعاء users من Java، يتم استخدام تنفيذ java.util.List، حيث تكون القائمة قابلة للتعديل وتتوفّر عمليات مثل add() وremove()‎.

الرمز الكامل:

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

10. الدوالّ والمواقع ذات المستوى الأعلى وتلك الخاصة بامتدادات

في الوقت الحالي، تعرف فئة Repository كيفية احتساب اسم المستخدم المنسَّق لكائن User. ولكن إذا أردنا إعادة استخدام منطق التنسيق نفسه في فئات أخرى، علينا إما نسخه ولصقه أو نقله إلى فئة User.

توفّر Kotlin إمكانية تعريف الدوالّ والسمات خارج أي فئة أو عنصر أو واجهة. على سبيل المثال، تمّ تحديد الدالة mutableListOf() التي استخدمناها لإنشاء مثيل جديد من List في Collections.kt من مكتبة Kotlin العادية.

في Java، عندما تحتاج إلى بعض وظائف المرافق، من المرجّح أن تنشئ فئة Util وتعلن عن هذه الوظيفة كدالة ثابتة. في Kotlin، يمكنك تحديد الدوالّ ذات المستوى الأعلى بدون الحاجة إلى فئة. ومع ذلك، توفّر Kotlin أيضًا إمكانية إنشاء دالّات إضافة. هذه دوالّ تُوسّع نوعًا معيّنًا ولكن يتمّ تعريفها خارج النوع.

يمكن تقييد مستوى رؤية دوالّ الإضافات وخصائصها باستخدام مُعدِّلات مستوى الرؤية. وتفرض هذه القواعد قيودًا على الاستخدام تقتصر على الفئات التي تحتاج إلى الإضافات، ولا تؤدي إلى تلويث مساحة الاسم.

بالنسبة إلى فئة User، يمكننا إما إضافة دالة إضافة تحسب الاسم المنسَّق، أو يمكننا الاحتفاظ بالاسم المنسَّق في سمة إضافة. يمكن إضافته خارج فئة Repository، في الملف نفسه:

// extension function
fun User.getFormattedName(): String {
    return if (lastName != null) {
        if (firstName != null) {
            "$firstName $lastName"
        } else {
            lastName ?: "Unknown"
        }
    } else {
        firstName ?: "Unknown"
    }
}

// extension property
val User.userFormattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName

يمكننا بعد ذلك استخدام دوالّ وخصائص الإضافة كما لو كانت جزءًا من فئة User.

بما أنّ الاسم المنسَّق هو سمة لفئة User وليست وظيفة لفئة Repository، لنستخدِم سمة الإضافة. يبدو ملف Repository الآن على النحو التالي:

val User.formattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
      get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user -> user.formattedName }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

تستخدم مكتبة Kotlin العادية دوال الإضافة لتوسيع وظائف العديد من واجهات برمجة تطبيقات Java، ويتم تنفيذ الكثير من الوظائف في Iterable وCollection كدوال إضافة. على سبيل المثال، الدالة map التي استخدمناها في خطوة سابقة هي دالة تمديد في Iterable.

11. دوال النطاق: let وapply وwith وrun وalso

في رمز الفئة Repository، نضيف عدة عناصر User إلى قائمة _users. يمكن جعل هذه الطلبات أكثر تعبيرية باستخدام دوالّ نطاق Kotlin.

لتنفيذ الرمز البرمجي في سياق عنصر معيّن فقط، بدون الحاجة إلى الوصول إلى العنصر استنادًا إلى اسمه، تقدّم Kotlin 5 وظائف نطاق: let وapply وwith وrun وalso. تجعل هذه الدوالّ رمزك البرمجي أسهل في القراءة وأكثر إيجازًا. تحتوي جميع دوال النطاق على مستلِم (this)، وقد تحتوي على وسيطة (it) وقد تعرض قيمة.

في ما يلي بطاقة معلومات سريعة لمساعدتك في تذكُّر الحالات التي تستدعي استخدام كلّ دالة:

6b9283d411fb6e7b.png

بما أنّنا بصدد ضبط عنصر _users في Repository، يمكننا جعل الرمز أكثر تعبيرية باستخدام الدالة apply:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")
   
    _users.apply {
       // this == _users
       add(user1)
       add(user2)
       add(user3)
    }
 }

12. الخاتمة

في هذا الدرس التطبيقي حول الترميز، غطّينا الأساسيات التي تحتاج إليها لبدء تحويل الرمز البرمجي من Java إلى Kotlin. لا تعتمد عملية التحويل هذه على منصّة التطوير، وتساعد في ضمان أنّ الرمز البرمجي الذي تكتبه هو رمز Kotlin عادي.

تجعل لغة Kotlin الأسلوبية كتابة الرموز البرمجية قصيرة وواضحة. مع كل الميزات التي تقدّمها Kotlin، هناك العديد من الطرق لجعل رمزك البرمجي أكثر أمانًا وأكثر إيجازًا وأكثر سهولة في القراءة. على سبيل المثال، يمكننا أيضًا تحسين فئة Repository من خلال إنشاء مثيل لقائمة _users باستخدام المستخدمين مباشرةً في البيان، والتخلص من القسم init:

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

لقد تناولنا مجموعة كبيرة من المواضيع، بدءًا من التعامل مع ما إذا كان العنصر قابلاً للقيمة الخالية أو لا، والعناصر الفردية، والسلاسل، والمجموعات، وصولاً إلى مواضيع مثل دوالّ الإضافات والدوالّ ذات المستوى الأعلى والسمات ودوالّ النطاق. لقد استبدلنا فئتَي Java بفئتَي Kotlin تبدوان الآن على النحو التالي:

User.kt

data class User(var firstName: String?, var lastName: String?)

Repository.kt

val User.formattedName: String
    get() {
       return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() = _users.map { user -> user.formattedName }
}

في ما يلي ملخّص لوظائف Java وربطها بلغة Kotlin:

Java

Kotlin

عنصر final

عنصر val

equals()

==

==

===

فئة تخزّن البيانات فقط

الصف data

الإعداد في الدالة الإنشائية

الإعداد في مجموعة init

static الحقول والدوالّ

الحقول والدوالّ التي تمّ تعريفها في companion object

فئة Singleton

object

للاطّلاع على مزيد من المعلومات حول Kotlin وكيفية استخدامها على منصتك، اطّلِع على هذه المراجع: