Refactoring به Kotlin

در این کد لبه، یاد خواهید گرفت که چگونه کد خود را از جاوا به کاتلین تبدیل کنید. همچنین یاد خواهید گرفت که قراردادهای زبان کاتلین چیست و چگونه اطمینان حاصل کنید که کدی که می نویسید از آنها پیروی می کند.

این کد لبه برای هر توسعه‌دهنده‌ای که از جاوا استفاده می‌کند و قصد مهاجرت پروژه خود به Kotlin را دارد، مناسب است. ما با چند کلاس جاوا شروع می کنیم که با استفاده از IDE آنها را به Kotlin تبدیل خواهید کرد. سپس نگاهی به کد تبدیل شده بیندازیم و ببینیم چگونه می‌توانیم آن را با اصطلاحی‌تر کردن آن بهبود بخشیم و از دام‌های رایج جلوگیری کنیم.

چیزی که یاد خواهید گرفت

شما یاد خواهید گرفت که چگونه جاوا را به کاتلین تبدیل کنید. با انجام این کار، ویژگی ها و مفاهیم زبان کاتلین زیر را یاد خواهید گرفت:

  • مدیریت پوچ پذیری
  • اجرای تک قلوها
  • کلاس های داده
  • دست زدن به رشته ها
  • اپراتور الویس
  • در حال تخریب
  • خواص و ویژگی های پشتوانه
  • آرگومان های پیش فرض و پارامترهای نامگذاری شده
  • کار با مجموعه ها
  • توابع پسوند
  • توابع و پارامترهای سطح بالا
  • let ، apply ، with ، و کلمات کلیدی run

مفروضات

شما باید از قبل با جاوا آشنا باشید.

آنچه شما نیاز دارید

یک پروژه جدید ایجاد کنید

اگر از IntelliJ IDEA استفاده می کنید، یک پروژه جاوا جدید با Kotlin/JVM ایجاد کنید.

اگر از Android Studio استفاده می کنید، یک پروژه جدید بدون فعالیت ایجاد کنید.

کد

ما یک شی مدل User و یک کلاس Repository singleton ایجاد می کنیم که با اشیاء 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;
    }

}

بسته به نوع پروژه خود، اگر از پروژه Android استفاده می کنید androidx.annotation.Nullable یا 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 ما می‌تواند کار بسیار خوبی برای تبدیل خودکار کد جاوا به کد Kotlin انجام دهد، اما گاهی اوقات به کمک کمی نیاز دارد. ابتدا این کار را انجام می‌دهیم و سپس کد بازسازی‌شده را مرور می‌کنیم تا بفهمیم که چگونه و چرا به این روش بازسازی شده است.

به فایل User.java بروید و آن را به Kotlin تبدیل کنید: نوار منو -> کد -> تبدیل فایل جاوا به فایل کاتلین .

اگر IDE شما بعد از تبدیل درخواست تصحیح کرد، Yes را فشار دهید.

شما باید کد Kotlin زیر را ببینید:

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

توجه داشته باشید که User.java به User.kt تغییر نام داد. فایل های Kotlin دارای پسوند .kt هستند.

در کلاس Java User ما دو ویژگی داشتیم: firstName و lastName . هر کدام یک روش گیرنده و تنظیم کننده داشتند که باعث می شد مقدار آن قابل تغییر باشد. کلمه کلیدی کاتلین برای متغیرهای قابل تغییر var است، بنابراین مبدل برای هر یک از این ویژگی ها از var استفاده می کند. اگر خصوصیات جاوا ما فقط گیرنده داشت، آنها غیرقابل تغییر بودند و به عنوان متغیرهای val اعلام می شدند. val شبیه کلمه کلیدی final در جاوا است.

یکی از تفاوت های کلیدی بین Kotlin و Java این است که Kotlin به صراحت مشخص می کند که آیا یک متغیر می تواند یک مقدار تهی را بپذیرد یا خیر. این کار را با ضمیمه کردن یک « ? به اعلان نوع.

از آنجایی که ما firstName و lastName به عنوان nullable علامت گذاری کردیم، تبدیل خودکار به طور خودکار ویژگی ها را با String? . اگر اعضای جاوا خود را غیر تهی حاشیه نویسی کنید (با استفاده از org.jetbrains.annotations.NotNull یا androidx.annotation.NonNull )، مبدل این را تشخیص می دهد و فیلدها را در Kotlin نیز غیر تهی می کند.

بازسازی اولیه در حال حاضر انجام شده است. اما می‌توانیم این را به شکلی اصطلاحی‌تر بنویسیم. بیایید ببینیم چگونه.

کلاس داده

کلاس User ما فقط داده ها را نگه می دارد. کاتلین یک کلمه کلیدی برای کلاس هایی با این نقش دارد: data . با علامت گذاری این کلاس به عنوان کلاس data ، کامپایلر به طور خودکار دریافت کننده ها و تنظیم کننده ها را برای ما ایجاد می کند. همچنین توابع equals() ، hashCode() و toString() مشتق می کند.

بیایید کلمه کلیدی data را به کلاس User خود اضافه کنیم:

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

کاتلین مانند جاوا می تواند یک سازنده اولیه و یک یا چند سازنده ثانویه داشته باشد. یکی در مثال بالا سازنده اصلی کلاس User است. اگر در حال تبدیل یک کلاس جاوا هستید که چندین سازنده دارد، مبدل به طور خودکار چندین سازنده را در Kotlin نیز ایجاد می کند. آنها با استفاده از کلمه کلیدی constructor تعریف می شوند.

اگر بخواهیم یک نمونه از این کلاس ایجاد کنیم، می توانیم این کار را به صورت زیر انجام دهیم:

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

برابری

کاتلین دو نوع برابری دارد:

  • برابری ساختاری از عملگر == استفاده می کند و 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( ) بود) دارای نحو متفاوتی نسبت به جاوا است (Repository.kt#L15)

قبل از اینکه جلوتر برویم، اجازه دهید کد را کمی پاک کنیم. می‌توانیم ببینیم که مبدل users ما را فهرست‌بندی کرده است که اشیاء قابل تهی را در خود نگه می‌دارد. در حالی که لیست واقعاً می تواند تهی باشد، بیایید بگوییم که نمی تواند کاربران تهی را نگه دارد. پس بیایید کارهای زیر را انجام دهیم:

  • حذف کنید ? در User? در اعلان تایپ users
  • getUsers باید List<User>?

مبدل خودکار همچنین اعلان های متغیر متغیرهای کاربران و آنهایی را که در بلوک init تعریف شده اند، به طور غیر ضروری به 2 خط تقسیم می کند. بیایید هر اعلان متغیر را در یک خط قرار دهیم. در اینجا کد ما باید شبیه باشد:

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

بلوک راه اندازی

در 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 کاتلین

در جاوا، از کلمه کلیدی static برای فیلدها یا توابع استفاده می کنیم تا بگوییم که آنها متعلق به یک کلاس هستند اما به نمونه ای از کلاس تعلق ندارند. به همین دلیل است که ما فیلد استاتیک INSTANCE در کلاس Repository خود ایجاد کردیم. معادل Kotlin برای این بلوک companion object است. در اینجا شما همچنین می توانید فیلدهای استاتیک و توابع استاتیک را اعلام کنید. مبدل فیلد INSTANCE را ایجاد و به اینجا منتقل کرد.

رسیدگی به تک قلوها

از آنجایی که ما فقط به یک نمونه از کلاس Repository نیاز داریم، از الگوی singleton در جاوا استفاده کردیم. با 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

در حال تخریب

کاتلین امکان تخریب یک شی را به تعدادی متغیر با استفاده از نحوی به نام destructuring declaration می دهد. ما چندین متغیر ایجاد می کنیم و می توانیم به طور مستقل از آنها استفاده کنیم.

برای مثال، کلاس‌های داده از ساختارشکنی پشتیبانی می‌کنند، بنابراین می‌توانیم شی 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 (که بعداً در نرم افزار کد پوشش داده شد)

در مورد ما، ما می دانیم که لیست کاربران نیازی به پوچ شدن ندارد، زیرا بلافاصله پس از ساخته شدن شی مقداردهی اولیه می شود، بنابراین می توانیم مستقیماً زمانی که شی را اعلام می کنیم نمونه سازی کنیم.

هنگام ایجاد نمونه‌هایی از انواع مجموعه، کاتلین چندین توابع کمکی را ارائه می‌کند تا کد شما را خواناتر و انعطاف‌پذیرتر کند. در اینجا ما از یک 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) {
    ...
}

همچنین، از آنجایی که متغیر 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 اگر null نباشد برگردانده می شود. اگر user.firstName null باشد، عبارت در سمت راست مقدار "Unknown" را برمی گرداند:

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

Kotlin کار با String را با قالب های String آسان می کند. قالب های رشته ای به شما این امکان را می دهند که به متغیرهای داخل اعلان رشته ها ارجاع دهید.

مبدل خودکار الحاق نام و نام خانوادگی را برای ارجاع مستقیم به نام متغیر در رشته با استفاده از نماد $ به روز کرد و عبارت را بین { } قرار داد.

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

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

در کد، الحاق رشته را با:

name = "$firstName $lastName"

در Kotlin if , when , for , و while عباراتی هستند - مقداری را برمی گردانند. IDE شما حتی یک اخطار نشان می دهد که تخصیص باید از این if حذف شود:

بیایید پیشنهاد IDE را دنبال کنیم و تخصیص را برای هر دو دستور if حذف کنیم. آخرین خط دستور if اختصاص داده می شود. مانند این، واضح‌تر است که این بلوک تنها هدف اولیه مقدار نام است:

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

در مرحله بعد، اخطاری دریافت می‌کنیم مبنی بر اینکه می‌توان اعلان 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 جایگزین کرد که یک گیرنده سفارشی دارد. در زیر هود، کاتلین همچنان یک متد getFormattedUserNames() تولید می کند که یک List برمی گرداند.

در جاوا، ویژگی های کلاس خود را از طریق توابع گیرنده و تنظیم کننده نمایش می دهیم. کاتلین به ما این امکان را می‌دهد که تمایز بهتری بین ویژگی‌های یک کلاس، که با فیلدها بیان می‌شوند، و عملکردها، اقداماتی که یک کلاس می‌تواند انجام دهد و با توابع بیان می‌شود، داشته باشیم. در مورد ما، کلاس Repository بسیار ساده است و هیچ عملی را انجام نمی دهد، بنابراین فقط دارای فیلدها است.

منطقی که در تابع getFormattedUserNames() جاوا راه اندازی شده بود، اکنون هنگام فراخوانی گیرنده ویژگی formattedUserNames Kotlin فعال می شود.

در حالی که ما صراحتاً فیلدی مطابق با ویژگی formattedUserNames نداریم، Kotlin یک فیلد پشتیبان خودکار به نام field در اختیار ما قرار می دهد. که در صورت نیاز می توانیم از گیرنده ها و ستترهای سفارشی به آن دسترسی داشته باشیم.

با این حال، گاهی اوقات ما می خواهیم برخی از عملکردهای اضافی که زمینه پشتیبان خودکار فراهم نمی کند. بیایید یک مثال را در زیر مرور کنیم.

در داخل کلاس Repository ، ما یک لیست قابل تغییر از کاربران داریم که در تابع getUsers() که از کد جاوا ما تولید شده است، در معرض نمایش قرار می گیرد:

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

مشکل اینجاست که با بازگشت users هر مصرف کننده ای از کلاس Repository می تواند لیست کاربران ما را تغییر دهد - ایده خوبی نیست! بیایید با استفاده از یک ویژگی پشتیبان این مشکل را برطرف کنیم.

ابتدا نام users را به _users تغییر می دهیم. اکنون یک ویژگی عمومی غیرقابل تغییر اضافه کنید که لیستی از کاربران را برمی گرداند. بیایید آن را users بنامیم:

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

با این تغییر، ویژگی private _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 از کتابخانه استاندارد تعریف شده است.

در جاوا، هر زمان که به برخی از قابلیت های کاربردی نیاز داشتید، به احتمال زیاد یک کلاس 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 از توابع افزونه برای گسترش عملکرد چندین API جاوا استفاده می کند. بسیاری از عملکردهای Iterable و Collection به عنوان توابع افزونه پیاده سازی می شوند. برای مثال، تابع map که در مرحله قبل استفاده کردیم، یک تابع پسوندی در Iterable است.

در کد کلاس Repository خود، چندین شیء کاربر را به لیست _users اضافه می کنیم. این فراخوانی ها را می توان با کمک توابع scope اصطلاحی تر کرد.

برای اجرای کد فقط در زمینه یک شی خاص، بدون نیاز به دسترسی به شی بر اساس نام آن، کاتلین 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)
    }
 }

در این کد لبه، ما اصولی را که برای شروع دوباره سازی کد خود از جاوا به کاتلین نیاز دارید، توضیح دادیم. این refactoring مستقل از پلتفرم توسعه شما است و به اطمینان از اصطلاحی بودن کدی که می نویسید کمک می کند.

Kotlin اصطلاحی نوشتن کد را کوتاه و شیرین می کند. با تمام ویژگی‌هایی که کاتلین ارائه می‌کند، راه‌های زیادی برای ایمن‌تر، مختصرتر و خواناتر کردن کد شما وجود دارد. برای مثال، ما حتی می‌توانیم کلاس Repository خود را با نمونه‌سازی فهرست _users با کاربرانی که مستقیماً در اعلان هستند، بهینه کنیم و از شر بلوک init خلاص شویم:

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

ما مجموعه وسیعی از موضوعات را پوشش دادیم، از مدیریت پوچ‌پذیری، تک‌تن‌ها، رشته‌ها و مجموعه‌ها گرفته تا موضوعاتی مانند توابع افزودنی، توابع سطح بالا، ویژگی‌ها و توابع دامنه. ما از دو کلاس جاوا به دو کلاس کاتلین رفتیم که اکنون به شکل زیر هستند:

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

در اینجا یک TL;DR از عملکردهای جاوا و نگاشت آنها به Kotlin آمده است:

جاوا

کاتلین

شی final

val

equals()

==

==

===

کلاسی که فقط داده ها را نگه می دارد

کلاس data

مقداردهی اولیه در سازنده

مقداردهی اولیه در بلوک init

فیلدها و توابع static

فیلدها و توابع اعلام شده در یک companion object

کلاس سینگلتون

object

برای کسب اطلاعات بیشتر در مورد Kotlin و نحوه استفاده از آن در پلتفرم خود، این منابع را بررسی کنید:

،

در این کد لبه، یاد خواهید گرفت که چگونه کد خود را از جاوا به کاتلین تبدیل کنید. همچنین یاد خواهید گرفت که قراردادهای زبان کاتلین چیست و چگونه اطمینان حاصل کنید که کدی که می نویسید از آنها پیروی می کند.

این کد لبه برای هر توسعه‌دهنده‌ای که از جاوا استفاده می‌کند و قصد مهاجرت پروژه خود به Kotlin را دارد، مناسب است. ما با چند کلاس جاوا شروع می کنیم که با استفاده از IDE آنها را به Kotlin تبدیل خواهید کرد. سپس نگاهی به کد تبدیل شده بیندازیم و ببینیم چگونه می‌توانیم آن را با اصطلاحی‌تر کردن آن بهبود بخشیم و از دام‌های رایج جلوگیری کنیم.

چیزی که یاد خواهید گرفت

شما یاد خواهید گرفت که چگونه جاوا را به کاتلین تبدیل کنید. با انجام این کار، ویژگی ها و مفاهیم زبان کاتلین زیر را یاد خواهید گرفت:

  • مدیریت پوچ پذیری
  • اجرای تک قلوها
  • کلاس های داده
  • دست زدن به رشته ها
  • اپراتور الویس
  • در حال تخریب
  • خواص و ویژگی های پشتوانه
  • آرگومان های پیش فرض و پارامترهای نامگذاری شده
  • کار با مجموعه ها
  • توابع پسوند
  • توابع و پارامترهای سطح بالا
  • let ، apply ، with ، و کلمات کلیدی run

مفروضات

شما باید از قبل با جاوا آشنا باشید.

آنچه شما نیاز دارید

یک پروژه جدید ایجاد کنید

اگر از IntelliJ IDEA استفاده می کنید، یک پروژه جاوا جدید با Kotlin/JVM ایجاد کنید.

اگر از Android Studio استفاده می کنید، یک پروژه جدید بدون فعالیت ایجاد کنید.

کد

ما یک شی مدل User و یک کلاس Repository singleton ایجاد می کنیم که با اشیاء 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;
    }

}

بسته به نوع پروژه خود، اگر از پروژه Android استفاده می کنید androidx.annotation.Nullable یا 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 ما می‌تواند کار بسیار خوبی برای تبدیل خودکار کد جاوا به کد Kotlin انجام دهد، اما گاهی اوقات به کمک کمی نیاز دارد. ابتدا این کار را انجام می‌دهیم و سپس کد بازسازی‌شده را مرور می‌کنیم تا بفهمیم که چگونه و چرا به این روش بازسازی شده است.

به فایل User.java بروید و آن را به Kotlin تبدیل کنید: نوار منو -> کد -> تبدیل فایل جاوا به فایل کاتلین .

اگر IDE شما بعد از تبدیل درخواست تصحیح کرد، Yes را فشار دهید.

شما باید کد Kotlin زیر را ببینید:

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

توجه داشته باشید که User.java به User.kt تغییر نام داد. فایل های Kotlin دارای پسوند .kt هستند.

در کلاس Java User ما دو ویژگی داشتیم: firstName و lastName . هر کدام یک روش گیرنده و تنظیم کننده داشتند که باعث می شد مقدار آن قابل تغییر باشد. کلمه کلیدی کاتلین برای متغیرهای قابل تغییر var است، بنابراین مبدل برای هر یک از این ویژگی ها از var استفاده می کند. اگر خصوصیات جاوا ما فقط گیرنده داشت، آنها غیرقابل تغییر بودند و به عنوان متغیرهای val اعلام می شدند. val شبیه کلمه کلیدی final در جاوا است.

یکی از تفاوت های کلیدی بین Kotlin و Java این است که Kotlin به صراحت مشخص می کند که آیا یک متغیر می تواند یک مقدار تهی را بپذیرد یا خیر. این کار را با ضمیمه کردن یک « ? به اعلان نوع.

از آنجایی که ما firstName و lastName به عنوان nullable علامت گذاری کردیم، تبدیل خودکار به طور خودکار ویژگی ها را با String? . اگر اعضای جاوا خود را غیر تهی حاشیه نویسی کنید (با استفاده از org.jetbrains.annotations.NotNull یا androidx.annotation.NonNull )، مبدل این را تشخیص می دهد و فیلدها را در Kotlin نیز غیر تهی می کند.

بازسازی اولیه در حال حاضر انجام شده است. اما می‌توانیم این را به شکلی اصطلاحی‌تر بنویسیم. بیایید ببینیم چگونه.

کلاس داده

کلاس User ما فقط داده ها را نگه می دارد. کاتلین یک کلمه کلیدی برای کلاس هایی با این نقش دارد: data . با علامت گذاری این کلاس به عنوان کلاس data ، کامپایلر به طور خودکار دریافت کننده ها و تنظیم کننده ها را برای ما ایجاد می کند. همچنین توابع equals() ، hashCode() و toString() مشتق می کند.

بیایید کلمه کلیدی data را به کلاس User خود اضافه کنیم:

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

کاتلین مانند جاوا می تواند یک سازنده اولیه و یک یا چند سازنده ثانویه داشته باشد. یکی در مثال بالا سازنده اصلی کلاس User است. اگر در حال تبدیل یک کلاس جاوا هستید که چندین سازنده دارد، مبدل به طور خودکار چندین سازنده را در Kotlin نیز ایجاد می کند. آنها با استفاده از کلمه کلیدی constructor تعریف می شوند.

اگر بخواهیم یک نمونه از این کلاس ایجاد کنیم، می توانیم این کار را به صورت زیر انجام دهیم:

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

برابری

کاتلین دو نوع برابری دارد:

  • برابری ساختاری از عملگر == استفاده می کند و 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( ) بود) دارای نحو متفاوتی نسبت به جاوا است (Repository.kt#L15)

قبل از اینکه جلوتر برویم، اجازه دهید کد را کمی پاک کنیم. می‌توانیم ببینیم که مبدل users ما را فهرست‌بندی کرده است که اشیاء قابل تهی را در خود نگه می‌دارد. در حالی که لیست واقعاً می تواند تهی باشد، بیایید بگوییم که نمی تواند کاربران تهی را نگه دارد. پس بیایید کارهای زیر را انجام دهیم:

  • حذف کنید ? در User? در اعلان تایپ users
  • getUsers باید List<User>?

مبدل خودکار همچنین اعلان های متغیر متغیرهای کاربران و آنهایی را که در بلوک init تعریف شده اند، به طور غیر ضروری به 2 خط تقسیم می کند. بیایید هر اعلان متغیر را در یک خط قرار دهیم. در اینجا کد ما باید شبیه باشد:

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

بلوک راه اندازی

در 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 کاتلین

در جاوا، از کلمه کلیدی static برای فیلدها یا توابع استفاده می کنیم تا بگوییم که آنها متعلق به یک کلاس هستند اما به نمونه ای از کلاس تعلق ندارند. به همین دلیل است که ما فیلد استاتیک INSTANCE در کلاس Repository خود ایجاد کردیم. معادل Kotlin برای این بلوک companion object است. در اینجا شما همچنین می توانید فیلدهای استاتیک و توابع استاتیک را اعلام کنید. مبدل فیلد INSTANCE را ایجاد و به اینجا منتقل کرد.

رسیدگی به تک قلوها

از آنجایی که ما فقط به یک نمونه از کلاس Repository نیاز داریم، از الگوی singleton در جاوا استفاده کردیم. با 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

در حال تخریب

کاتلین امکان تخریب یک شی را به تعدادی متغیر با استفاده از نحوی به نام destructuring declaration می دهد. ما چندین متغیر ایجاد می کنیم و می توانیم به طور مستقل از آنها استفاده کنیم.

برای مثال، کلاس‌های داده از ساختارشکنی پشتیبانی می‌کنند، بنابراین می‌توانیم شی 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 (که بعداً در نرم افزار کد پوشش داده شد)

در مورد ما ، ما می دانیم که لیست کاربران نیازی به تهی ندارند ، زیرا بلافاصله پس از ساخت شیء شروع می شود ، بنابراین می توانیم هنگام اعلام آن ، مستقیماً شی را فوری کنیم.

هنگام ایجاد نمونه هایی از انواع مجموعه ، کوتلین چندین کارکرد یاور را ارائه می دهد تا کد شما خواندنی و انعطاف پذیر تر شود. در اینجا ما از یک 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"
}

این را می توان با استفاده از اپراتور الویس بیشتر به صورت احمقانه نوشت ?: . در صورت عدم تهی یا بیان در سمت راست خود ، اپراتور الویس عبارت را در سمت چپ خود باز می گرداند.

بنابراین در کد زیر user.firstName اگر تهی نباشد برگردانده می شود. اگر user.firstName تهی باشد ، عبارت از دست راست ، "Unknown" را برمی گرداند:

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

Kotlin کار با String S را با الگوهای رشته ای آسان می کند. الگوهای رشته ای به شما امکان می دهد متغیرهای موجود در اعلامیه های رشته را مرجع کنید.

مبدل اتوماتیک جمع بندی نام اول و خانوادگی را به روز کرد تا با استفاده از نماد $ ، نام متغیر را مستقیماً در رشته ارجاع داده و بیان را بین { } قرار دهد.

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

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

در کد ، هماهنگی رشته را با:

name = "$firstName $lastName"

در کوتلین if ، when ، for و while عبارات هستند - آنها یک ارزش را برمی گردانند. IDE شما حتی هشدار دهنده ای را نشان می دهد که باید از این if برداشته شود:

بیایید پیشنهاد IDE را دنبال کنیم و تکلیف را برای هر دو if بلند کنیم. آخرین خط بیانیه IF اختصاص خواهد یافت. مانند این ، واضح تر است که تنها هدف این بلوک ، اولیه کردن مقدار نام است:

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

در مرحله بعد ، هشدار می دهیم که می توان اعلام 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
        }

کوتلین لیست گسترده ای از تحولات مجموعه را ارائه می دهد که با گسترش قابلیت های 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 است که دارای یک گیرنده سفارشی است. در زیر کاپوت ، کوتلین هنوز یک روش getFormattedUserNames() تولید می کند که List برمی گرداند.

در جاوا ، ما ویژگی های کلاس خود را از طریق توابع گیرنده و تنظیم کننده در معرض دید قرار می دهیم. کوتلین به ما اجازه می دهد تا تمایز بهتری بین خواص یک کلاس ، بیان شده با زمینه ها و عملکردها ، اقداماتی که یک کلاس می تواند انجام دهد ، با توابع بیان کنیم. در مورد ما ، کلاس Repository بسیار ساده است و هیچ اقدامی انجام نمی دهد ، بنابراین فقط زمینه هایی دارد.

منطقی که در عملکرد Java getFormattedUserNames() ایجاد شده است ، اکنون هنگام فراخوانی از ویژگی های formattedUserNames Kotlin ایجاد شده است.

در حالی که ما صریحاً زمینه ای را مطابق با ویژگی formattedUserNames نداریم ، Kotlin یک قسمت پشتیبان اتوماتیک به نام field را برای ما فراهم می کند که در صورت نیاز از گیرنده ها و تنظیم کننده های سفارشی می توانیم به آن دسترسی پیدا کنیم.

با این حال ، گاهی اوقات ، ما می خواهیم برخی از قابلیت های اضافی را که قسمت پشتی اتوماتیک ارائه نمی دهد. بیایید مثالی زیر را طی کنیم.

در داخل کلاس Repository ما ، ما یک لیست قابل تغییر از کاربران داریم که در عملکرد getUsers() که از کد جاوا ما ایجاد شده است ، در معرض دید قرار می گیرند:

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

مشکل اینجاست که با بازگشت users هر مصرف کننده کلاس مخزن می تواند لیست کاربران ما را تغییر دهد - ایده خوبی نیست! بیایید این کار را با استفاده از یک ویژگی پشتیبان برطرف کنیم.

ابتدا بیایید 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 منتقل کنیم.

کوتلین امکان اعلام توابع و خصوصیات خارج از هر کلاس ، شی یا رابط را فراهم می کند. به عنوان مثال ، تابع mutableListOf() که ما برای ایجاد یک نمونه جدید از یک List استفاده کردیم ، مستقیماً در Collections.kt تعریف می شود. KT از کتابخانه استاندارد.

در جاوا ، هر زمان که به برخی از قابلیت های کاربردی نیاز دارید ، به احتمال زیاد یک کلاس Util ایجاد می کنید و آن عملکرد را به عنوان یک عملکرد استاتیک اعلام می کنید. در کوتلین می توانید بدون داشتن کلاس ، عملکردهای سطح بالا را اعلام کنید. با این حال ، کوتلین همچنین توانایی ایجاد توابع پسوند را فراهم می کند. اینها کارکردهایی هستند که نوع خاصی را گسترش می دهند اما در خارج از نوع اعلام می شوند. به همین ترتیب ، آنها به آن نوع وابستگی دارند.

با استفاده از اصلاح کننده های دید ، می توان دید توابع و خواص پسوند را محدود کرد. اینها استفاده را فقط به کلاسهایی که به پسوندها نیاز دارند محدود می کند و فضای نام را آلوده نمی کند.

برای کلاس 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)
    }
}

کتابخانه استاندارد کوتلین از توابع پسوند برای گسترش عملکرد چندین API های جاوا استفاده می کند. بسیاری از ویژگی های مربوط به 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)
    }
 }

در این CodeLab ، اصول اولیه مورد نیاز شما را برای شروع مجدد کد خود از جاوا به کوتلین پوشش دادیم. این اصلاح مجدد مستقل از پلتفرم توسعه شما است و به اطمینان از اینکه کدی که می نویسید ایدیوماتیک است ، کمک می کند.

idiomatic کوتلین نوشتن کد را کوتاه و شیرین می کند. با وجود تمام ویژگی هایی که Kotlin ارائه می دهد ، روش های زیادی برای ایمن تر ، مختصر تر و قابل خواندن کد شما وجود دارد. به عنوان مثال ، ما حتی می توانیم با استفاده از لیست _users با کاربران مستقیماً در اعلامیه ، کلاس Repository خود را بهینه کنیم و از شر بلوک init خلاص شویم:

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

ما مجموعه بزرگی از موضوعات را پوشش دادیم ، از رسیدگی به تهی ، مجرد ، رشته ها و مجموعه ها گرفته تا موضوعاتی مانند توابع پسوند ، عملکردهای سطح بالا ، خصوصیات و عملکردهای دامنه. ما از دو کلاس جاوا به دو گروه کوتلین رفتیم که اکنون به این شکل است:

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

در اینجا یک tl ؛ dr از ویژگی های جاوا و نقشه برداری آنها به کوتلین وجود دارد:

جاوا

کاتلین

هدف final

val

equals()

==

==

===

کلاس که فقط داده ها را در اختیار دارد

کلاس data

اولیه سازی در سازنده

اولیه سازی در بلوک init

زمینه ها و عملکردهای static

زمینه ها و توابع اعلام شده در یک companion object

کلاس Singleton

object

برای کسب اطلاعات بیشتر در مورد Kotlin و نحوه استفاده از آن در سیستم عامل خود ، این منابع را بررسی کنید: