Refactoring به Kotlin

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

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

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

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

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

مفروضات

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

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

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

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

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

کد

ما یک شی مدل 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;
    }

}

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

اگر نام firstName خالی است، name یا lastName یا "Unknown" است:

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

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

بنابراین در کد زیر 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 UserNames بگیریم و ببینیم چگونه می‌توانیم آن را اصطلاحی‌تر کنیم. در حال حاضر کد به صورت زیر عمل می کند:

  • یک لیست جدید از رشته ها ایجاد می کند
  • از طریق لیست کاربران تکرار می شود
  • نام قالب بندی شده را برای هر کاربر بر اساس نام و نام خانوادگی کاربر می سازد
  • لیست تازه ایجاد شده را برمی گرداند
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 از تابع application اصطلاحی‌تر کنیم:

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

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

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

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 و نحوه استفاده از آن در پلتفرم خود، این منابع را بررسی کنید: