1. مرحبًا
في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية تحويل الرمز البرمجي من Java إلى Kotlin. ستتعرّف أيضًا على اصطلاحات لغة Kotlin وكيفية التأكّد من أنّ الرمز الذي تكتبه يتّبع هذه الاصطلاحات.
هذا الدرس التطبيقي حول الترميز مناسب لأي مطوّر يستخدم Java ويفكّر في نقل مشروعه إلى Kotlin. سنبدأ بفئتين من فئات Java ستحوّلهما إلى Kotlin باستخدام بيئة التطوير المتكاملة. بعد ذلك، سنلقي نظرة على الرمز البرمجي المحوَّل ونرى كيف يمكننا تحسينه من خلال جعله أكثر تعبيرًا عن اللغة وتجنُّب الأخطاء الشائعة.
المُعطيات
ستتعرّف على كيفية تحويل رمز 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;
}
}
ستلاحظ أنّ بيئة التطوير المتكاملة (IDE) تخبرك بأنّ @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 وفئات البيانات
يمكن لبيئة التطوير المتكاملة (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")
المساواة
تتضمّن لغة 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>?
كتلة 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 formattedUserNames = Repository.formattedUserNames
يُرجى العِلم أنّه إذا لم يكن للعنصر معدِّل إذن وصول، سيكون متاحًا للجميع تلقائيًا، كما هو الحال مع العنصر formattedUserNames في الكائن Repository.
6. التعامل مع إمكانية قبول القيم الفارغة
عند تحويل الفئة Repository إلى Kotlin، جعل المحوّل التلقائي قائمة المستخدمين تقبل القيم الخالية، لأنّه لم يتم ضبطها على عنصر عند تعريفها. نتيجةً لذلك، يجب استخدام عامل تأكيد عدم القيمة الفارغة !! في جميع استخدامات العنصر users. (سيظهر لك users!! وuser!! في جميع أنحاء الرمز المحوَّل). يحوّل عامل التشغيل !! أي متغير إلى نوع غير فارغ، ما يتيح لك الوصول إلى الخصائص أو استدعاء الدوال. ومع ذلك، سيتم عرض استثناء إذا كانت قيمة المتغيّر فارغة بالفعل. باستخدام !!، أنت تخاطر بحدوث استثناءات أثناء وقت التشغيل.
بدلاً من ذلك، يُفضَّل التعامل مع إمكانية قبول القيم الفارغة باستخدام إحدى الطرق التالية:
- إجراء عملية التحقّق من القيمة الخالية (
if (users != null) {...}) - استخدام عامل Elvis
?:(سيتم تناوله لاحقًا في الدرس التطبيقي حول الترميز) - استخدام بعض دوال Kotlin العادية (سيتم تناولها لاحقًا في الدرس العملي)
في حالتنا، نعلم أنّ قائمة المستخدمين لا تحتاج إلى أن تكون قابلة للتصغير، لأنّه يتم تهيئتها بعد إنشاء العنصر مباشرةً (في الحظر 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 العمل مع Strings باستخدام نماذج السلاسل. تتيح لك نماذج السلاسل الإشارة إلى المتغيّرات داخل تعريفات السلاسل باستخدام الرمز $ قبل المتغيّر. يمكنك أيضًا وضع تعبير داخل تعريف سلسلة، وذلك عن طريق وضع التعبير داخل { } واستخدام الرمز $ قبله. مثال: ${user.firstName}.
يستخدم الرمز حاليًا تسلسل السلاسل لدمج firstName وlastName في اسم المستخدم.
if (firstName != null) {
firstName + " " + lastName
}
بدلاً من ذلك، استبدِل عملية دمج السلسلة بما يلي:
if (firstName != null) {
"$firstName $lastName"
}
يمكن أن يؤدي استخدام نماذج السلاسل إلى تبسيط الرمز البرمجي.
ستعرض بيئة التطوير المتكاملة (IDE) تحذيرات إذا كانت هناك طريقة أكثر تعبيرًا لكتابة التعليمات البرمجية. سيظهر لك خط متعرّج تحت الرمز، وعندما تمرّر مؤشر الماوس فوقه، سيظهر لك اقتراح بشأن كيفية إعادة تصميم الرمز.
في الوقت الحالي، من المفترض أن يظهر لك تحذير بأنّه يمكن دمج تعريف 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 عند استدعاء الدالة getter للسمة 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. ظلِّل اسم المتغيّر، ثم انقر بزر الماوس الأيمن على Refactor > Rename لإعادة تسمية المتغيّر. بعد ذلك، أضِف سمة عامة للقراءة فقط تعرض قائمة بالمستخدمين. لنسمِّها 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) وقد تعرض قيمة.
إليك ورقة مرجعية مفيدة لمساعدتك في تذكُّر وقت استخدام كل دالة:

بما أنّنا نضبط إعدادات العنصر _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 |
|
|
|
|
|
|
فئة تحتوي على بيانات فقط | الصف |
الإعداد في الدالة الإنشائية | الإعداد في حزمة |
| الحقول والدوال المعرَّفة في |
فئة Singleton |
|
لمزيد من المعلومات حول Kotlin وكيفية استخدامها على نظامك الأساسي، اطّلِع على المراجع التالية:
- Kotlin Koans
- برامج تعليمية حول Kotlin
- أساسيات Android Kotlin
- برنامج تدريبي حول لغة Kotlin للمبرمِجين
- دورة Kotlin لمطوّري Java - دورة مجانية في "وضع التدقيق"