إعادة بناء لغة Kotlin

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

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

أهداف الدورة التعليمية

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

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

الافتراضات

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

المتطلبات

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

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

إذا كنت تستخدم "استوديو Android"، أنشئ مشروعًا جديدًا بدون أي نشاط.

الرمز

سننشئ عنصر نموذج 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;
    }

}

استورِد androidx.annotation.Nullable إذا كنت تستخدم مشروع Android، أو org.jetbrains.annotations.Nullable إذا كنت تستخدم مشروعًا آخر، وذلك حسب نوع مشروعك.

أنشئ ملفًا جديدًا باسم 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;
    }
}

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

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

إذا طلب منك بيئة التطوير المتكاملة تصحيح الخطأ بعد التحويل، انقر على نعم.

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

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

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

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

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

بما أنّنا وضعنا علامة "يقبل قيمة فارغة" على firstName وlastName، وضع المحوّل التلقائي علامة "يقبل قيمة فارغة" على السمات تلقائيًا باستخدام 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")

Equality

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

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

سيتم استخدام الخصائص المحدّدة في الدالة الإنشائية الأساسية لفئة البيانات لإجراء عمليات التحقّق من التساوي البنيوي.

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

في 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("John", "Doe")

يمكن تسمية مَعلمات الدالة عند استدعاء الدوال:

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")

قبل المتابعة، تأكَّد من أنّ صفك على 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)
   }
}

لنرَ ما فعله المحوّل التلقائي:

  • تمت إضافة حظر init (Repository.kt#L50)
  • أصبح الحقل static الآن جزءًا من الكتلة companion object (Repository.kt#L33)
  • يمكن أن تكون قائمة users قابلة للتصغير لأنّه لم يتم إنشاء العنصر في وقت التعريف (Repository.kt#L7).
  • أصبحت الدالة getFormattedUserNames() الآن سمة باسم formattedUserNames (Repository.kt#L11)
  • يختلف بناء الجملة الخاص بالتكرار على قائمة المستخدمين (التي كانت في البداية جزءًا من getFormattedUserNames() عن بناء الجملة الخاص بلغة Java (Repository.kt#L15).

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

  • إزالة ? في User? ضمن تعريف النوع users
  • يجب أن تعرض getUsers القيمة List<User>?

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

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)
    }
}

كتلة Init

في 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 users = Repository.users

تفكيك البنية

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

على سبيل المثال، تتيح فئات البيانات إمكانية تقسيم البنية، لذا يمكننا تقسيم بنية الكائن User في حلقة for إلى (firstName, lastName). يتيح لنا ذلك العمل مباشرةً مع القيمتين firstName وlastName. لنعدّل حلقة for على النحو التالي:

 
for ((firstName, lastName) in users!!) {
       val name: String?

       if (lastName != null) {
          if (firstName != null) {
                name = "$firstName $lastName"
          } else {
                name = lastName
          }
       } else if (firstName != null) {
            name = firstName
       } else {
            name = "Unknown"
       }
       userNames.add(name)
}

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

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

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

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

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

private var users: MutableList<User>? = null

لتبسيط الأمر، يمكننا استخدام الدالة mutableListOf()، وتوفير نوع عنصر القائمة، وإزالة استدعاء الدالة الإنشائية ArrayList من الكتلة init، وإزالة تعريف النوع الصريح للخاصية users.

private val users = mutableListOf<User>()

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

بعد إجراء هذه التغييرات، أصبحت السمة users غير فارغة، ويمكننا إزالة جميع حالات ظهور عامل التشغيل !! غير الضرورية.

val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
    ...
}

بالإضافة إلى ذلك، بما أنّه تمّت تهيئة متغيّر المستخدمين من قبل، علينا إزالة عملية التهيئة من الحظر 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)
}

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

val name: String

إذا كانت قيمة lastName فارغة، تكون قيمة name إما firstName أو "Unknown":

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

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

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

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

تسهّل لغة Kotlin العمل مع Strings باستخدام نماذج String. تتيح لك نماذج السلاسل الرجوع إلى المتغيرات داخل تعريفات السلاسل.

عدّل المحوّل التلقائي عملية ربط الاسم الأول واسم العائلة للإشارة إلى اسم المتغير مباشرةً في السلسلة باستخدام الرمز $ ووضع العبارة بين { } .

// Java
name = user.getFirstName() + " " + user.getLastName();

// Kotlin
name = "${user.firstName} ${user.lastName}"

في الرمز، استبدِل تسلسل السلسلة بما يلي:

name = "$firstName $lastName"

في Kotlin، تكون if وwhen وfor وwhile عبارات، أي أنّها تعرض قيمة. تعرض بيئة التطوير المتكاملة (IDE) تحذيرًا بأنّه يجب نقل عملية التعيين خارج if:

لنتبع اقتراح بيئة التطوير المتكاملة وننقل عملية التعيين إلى خارج عبارتَي if. سيتم تعيين السطر الأخير من عبارة if. بهذه الطريقة، يصبح من الواضح أنّ الغرض الوحيد من هذا الرمز هو تهيئة قيمة الاسم:

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

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

val formattedUserNames: List<String?>
   get() {
       val userNames: MutableList<String?> = ArrayList(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
   }

لنلقِ نظرة فاحصة على الدالة 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
            }
        }

لتبسيط ذلك أكثر، يمكننا إزالة المتغيّر 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"
                }
            }
        }

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

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

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

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

ومع ذلك، نريد أحيانًا بعض الوظائف الإضافية التي لا يوفّرها الحقل الاحتياطي التلقائي. لنستعرض مثالاً أدناه.

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

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

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

أولاً، لنغيّر اسم users إلى _users. الآن، أضِف سمة عامة غير قابلة للتغيير تعرض قائمة بالمستخدمين. لنسمّها users:

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

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

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

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)
    }
}

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

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

في 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.

بما أنّ الاسم المنسّق هو خاصية للمستخدم وليس وظيفة للفئة 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.

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

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

إليك ورقة غش مفيدة لمساعدتك في تذكُّر ذلك:

بما أنّنا نضبط إعدادات العنصر _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)
    }
 }

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

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

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

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

User.kt

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

فئة سينغلتون

object

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