بخش دوم از فصل اول در اینجا مشاهده کنید
در حال حاضر، شما باید راحت باشید برای استفاده از کاتلین به همان روشی که از جاوا استفاده می کنید. شما دیده اید که چگونه مفاهیم آشنای شما از جاوا به کاتلین ترجمه می شوند و چگونه کاتلین اغلب آنها را مختصر و خوانا می کند.
در این فصل، خواهید دید که چگونه کاتلین یکی از عناصر کلیدی هر برنامه را بهبود می بخشد: اعلام و فراخوانی توابع. ما همچنین احتمالات سازگاری کتابخانه های جاوا با سبک کاتلین را از طریق استفاده از extension functions بررسی خواهیم کرد، که به شما این امکان را می دهد تا از مزایای کامل کاتلین در پروژه های زبان ترکیبی بهره ببرید.
برای اینکه بحثمان مفیدتر و صریح تر شود، روی مجموعه ها، رشته ها و regular expressions کاتلین به عنوان حوزه مشکل خود تمرکز می کنیم. به عنوان مقدمه، بیایید نحوه ایجاد مجموعه در کاتلین را بررسی کنیم.
قبل از اینکه بتوانید مشغول کار با مجموعه ها شوید، باید نحوه ایجاد آنها را یاد بگیرید. در بخش 2.3.3، شما به راه ایجاد یک مجموعه جدید برخورد کردید: با تابع setOf. شما در آن زمان مجموعه ای از رنگ ها را ایجاد کردید، اما در حال حاضر، اجازه دهید آن را ساده نگه داریم و با اعداد کار کنیم:
val set = hashSetOf(1, 7, 53)
شما یک لیست یا یک map به روشی مشابه ایجاد می کنید:
val list = arrayListOf(1, 7, 53) val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
توجه داشته باشید که to یک ساختار خاص نیست، بلکه یک تابع عادی است. بعداً در فصل به آن باز خواهیم گشت.
آیا می توانید کلاس های اشیایی که در اینجا ایجاد می شوند را حدس بزنید؟ مثال زیر را اجرا کنید تا خودتان متوجه این موضوع شوید
>>> println(set.javaClass) class java.util.HashSet >>> println(list.javaClass) class java.util.ArrayList >>> println(map.javaClass) class java.util.HashMap
همانطور که می بینید، کاتلین از کلاس های مجموعه استاندارد جاوا استفاده می کند. این خبر خوبی برای توسعه دهندگان جاوا است: کاتلین مجموعه کلاس های خاص خود را ندارد. تمام دانش موجود شما در مورد مجموعه های جاوا همچنان در اینجا اعمال می شود.
چرا هیچ مجموعه کاتلین وجود ندارد؟ زیرا استفاده از مجموعه های استاندارد جاوا تعامل با کد جاوا را بسیار آسان تر می کند. هنگام فراخوانی توابع جاوا از کاتلین یا بالعکس، نیازی به تبدیل مجموعه ها به این صورت نیست.
حتی اگر مجموعه های کاتلین دقیقاً همان کلاس های مجموعه های جاوا هستند، می توانید کارهای بیشتری با آنها در کاتلین انجام دهید. به عنوان مثال، می توانید آخرین عنصر را در یک لیست دریافت کنید یا حداکثر را در مجموعه ای از اعداد بیابید:
>>> val strings = listOf("first", "second", "fourteenth") >>> println(strings.last()) fourteenth >>> val numbers = setOf(1, 14, 2) >>> println(numbers.max()) 14
در فصل های آینده، وقتی شروع به صحبت در مورد لامبدا می کنیم، کارهای بیشتری را با مجموعه ها می توانید انجام دهید، اما ما به استفاده از کلاس های مجموعه استاندارد جاوا ادامه خواهیم داد. و در بخش 6.3، نحوه نمایش کلاس های مجموعه جاوا در سیستم نوع کاتلین را خواهید آموخت.
قبل از بحث در مورد چگونگی ماندگاری و حداکثر عملکرد توابع جادویی در مجموعه های جاوا، بیایید چند مفهوم جدید برای اعلان یک تابع بیاموزیم.
اکنون که می دانید چگونه مجموعه ای از عناصر را ایجاد کنید، بیایید کاری ساده انجام دهیم: محتویات آن را چاپ کنید. اگر این خیلی ساده به نظر می رسد نگران نباشید. در طول مسیر، با یک سری مفاهیم مهم روبرو خواهید شد.
مجموعه های جاوا یک پیاده سازی پیش فرض toString دارند، اما فرمت خروجی ثابت است و همیشه آن چیزی نیست که شما نیاز دارید:
>>> val list = listOf(1, 2, 3) >>> println(list) [1, 2, 3]
تصور کنید که به جای پرانتزهایی که در اجرای پیش فرض استفاده می شود، لازم است عناصر با نقطه ویرگول از هم جدا شوند و با پرانتز احاطه شوند: (1; 2; 3). برای حل این مشکل، پروژه های جاوا از کتابخانه های شخص ثالث مانند Guava و Apache Commons استفاده می کنند یا منطق داخل پروژه را دوباره پیاده سازی می کنند. در کاتلین، یک تابع برای مدیریت این بخش از کتابخانه استاندارد وجود دارد.
در این بخش، این تابع را خودتان پیاده سازی خواهید کرد. شما با یک پیاده سازی ساده شروع می کنید که از امکانات کاتلین برای ساده سازی اعلان های عملکرد استفاده نمی کند، و سپس آن را به سبک idiomatic بازنویسی می کنید.
تابع joinToString که در ادامه نشان داده شده است، عناصر مجموعه را با یک جداکننده بین آنها، یک پیشوند در ابتدا و یک پسوند در پایان به StringBuilder اضافه می کند.
fun <T> joinToString( collection: Collection<T>, separator: String, prefix: String, postfix: String ): String { val result = StringBuilder(prefix) for ((index, element) in collection.withIndex()) { if (index > 0) result.append(separator) result.append(element) } result.append(postfix) return result.toString() }
تعریف ژنریک generics برای کسانی که اشنایی ندارند : توابع و کلاس های Generic در جاوا (Java)، برنامه نویس را قادر می سازد، تا با تعریف تنها یک تابع، مجموعه ای از توابع مرتبط را، و یا با تعریف تنها یک کلاس، مجموعه ای از کلاس های مرتبط را تحت پوشش قرار دهد.
توجه : Generic ها در جاوا، همچنین به برنامه نویس اجازه می دهد نوع داده های نامعتبر را در زمان کامپایل (compile-time) تشخیص دهد، که این موضوع ایمنی نوع داده در زمان کامپایل را فراهم می کند.
با استفاده از مفهوم Generic در جاوا، ما ممکن است یک تابع Generic برای مرتب کردن آرایه ای از نوع Object نوشته، و سپس تابع Generic را برای مرتب کردن مقادیر آرایه با آرایه ای از نوع Integer، یا Double، یا String و غیره فراخوانی کنیم.
تابع (Function ) ژنریک genericاست: روی مجموعه هایی کار می کند که حاوی عناصری از هر نوع داده هستند. syntax برای ژنریک شبیه جاوا است. (بحث مفصل تر در مورد ژنریک موضوع فصل 9 خواهد بود.)
بیایید بررسی کنیم که تابع همانطور که در نظر گرفته شده است کار می کند:
>>> val list = listOf(1, 2, 3) >>> println(joinToString(list, " ", "(", ")")
پیاده سازی (implementation) خوب است، و شما اکثراً آن را همانطور که هست رها می کنید. آنچه ما روی آن تمرکز خواهیم کرد این است: چگونه می توانید آن را تغییر دهید تا تماس های این تابع کمتر پرمخاطب باشد؟ شاید بتوانید هر بار که تابع را فراخوانی می کنید، از ارسال چهار آرگومان اجتناب کنید. بیایید ببینیم چه کاری می توانید انجام دهید.
اولین مشکلی که به آن خواهیم پرداخت به خوانایی فراخوانی تابع مربوط می شود. به عنوان مثال، به فراخوانی زیر joinToString نگاه کنید:
joinToString(collection, " ", " ", ".")
آیا می توانید بگویید همه این رشته ها با چه پارامترهایی مطابقت دارند؟ آیا عناصر با فضای خالی یا نقطه از هم جدا می شوند؟ پاسخ به این سؤالات بدون نگاه کردن به امضای تابع دشوار است. شاید آن را به خاطر داشته باشید، یا شاید IDE به شما کمک کند، اما از کد فراخوانی مشخص نیست.
این مشکل به ویژه در Boolean flags رایج است. برای حل آن، برخی از سبک های کدنویسی جاوا توصیه می کنند به جای استفاده از Booleans، نوع داده enum ایجاد کنید. برخی دیگر حتی از شما می خواهند که نام پارامترها را به صراحت مشخص کنید، مانند مثال زیر:
/* Java */ joinToString(collection, /* separator */ " ", /* prefix */ " ", /* postfix */ ".");
با کاتلین ، می توانید کارهای بهتری انجام دهید:
joinToString(collection, separator = " ", prefix = " ", postfix = ".")
هنگام فراخوانی یک تابع نوشته شده در کاتلین، می توانید نام برخی از آرگومان هایی را که به تابع ارسال می کنید، مشخص کنید. اگر نام یک آرگومان را در فراخوانی مشخص می کنید، برای جلوگیری از سردرگمی، باید نام همه آرگومان ها را بعد از آن نیز مشخص کنید.
نکته : اگر نام پارامتر تابع فراخوانی شده را تغییر دهید، IntelliJ IDEA می تواند نام آرگومان های نوشته شده را به روز نگه دارد. فقط مطمئن شوید که به جای ویرایش دستی نام پارامترها، از عمل تغییر نام یا تغییر امضا استفاده می کنید.
هشدار متأسفانه، هنگام فراخوانی متدهای نوشته شده در جاوا، از جمله متدهایی از JDK و Android framework ، نمی توانید از پارامترهای نام گذاری شده استفاده کنید. ذخیره نام پارامترها در فایل های .class به عنوان یک ویژگی اختیاری فقط با Java 8 پشتیبانی می شود، و کاتلین سازگاری با جاوا 6 را حفظ می کند. در نتیجه، کامپایلر نمی تواند نام پارامترهای استفاده شده در فراخوانی شما را تشخیص دهد و آنها را با method definition مطابقت دهد.
آرگومان های نام گذاری شده به ویژه با مقادیر پارامترهای پیش فرض خوب کار می کنند، که در ادامه به آن ها خواهیم پرداخت.
یکی دیگر از مشکلات رایج جاوا، فراوانی بیش از حد متدهای بارگذاری شده در برخی کلاس ها است. فقط به java.lang.Thread و هشت سازنده آن (http://mng.bz/4KZC) نگاه کنید! overload را می توان به خاطر سازگاری با عقب، برای راحتی کاربران API یا دلایل دیگر ارائه کرد، اما نتیجه نهایی یکسان است: تکراری. نام ها و نوع داده پارامترها بارها و بارها تکرار می شوند، و اگر برنامه نویس خوبی هستید، باید بیشتر مستندات را در هر overload تکرار کنید. در عین حال، اگر overload را فراخوانی کنید که برخی از پارامترها را حذف می کند، همیشه مشخص نیست که چه مقادیری برای آنها استفاده می شود.
در کاتلین، اغلب می توانید از ایجاد overloads جلوگیری کنید زیرا می توانید مقادیر پیش فرض پارامترها را در یک اعلان تابع مشخص کنید. بیایید از آن برای بهبود تابع joinToString استفاده کنیم. برای اکثر موارد، رشته ها را می توان با کاما بدون هیچ پیشوند یا پسوندی از هم جدا کرد. بنابراین، بیایید این مقادیر را پیش فرض قرار دهیم.
fun <T> joinToString collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String = "" ): String
اکنون می توانید تابع را با همه آرگومان ها فراخوانی کنید یا برخی از آنها را حذف کنید:
>>> joinToString(list, ", ", "", "") 1, 2, 3 >>> joinToString(list) 1, 2, 3 >>> joinToString(list, " ") 1; 2; 3
هنگام استفاده از سینتکس فراخوانی معمولی، باید آرگومان ها را به همان ترتیبی که در اعلان تابع مشخص شده است، مشخص کنید و فقط می توانید آرگومان های انتهایی را حذف کنید. اگر از آرگومان های نام گذاری شده استفاده می کنید، می توانید برخی از آرگومان ها را از وسط فهرست حذف کنید و فقط آن هایی را که نیاز دارید، به ترتیبی که می خواهید مشخص کنید:
>>> joinToString(list, suffix = "", prefix = "# ") # 1, 2, 3;
توجه داشته باشید که مقادیر پیش فرض پارامترها در تابعی که فراخوانی می شود، کدگذاری می شوند، نه در محل تماس. اگر مقدار پیش فرض را تغییر دهید و کلاس حاوی تابع را دوباره کامپایل کنید، تماس گیرندگانی که مقداری برای پارامتر تعیین نکرده اند شروع به استفاده از مقدار پیش فرض جدید می کنند.
با توجه به اینکه جاوا مفهوم مقادیر پارامترهای پیش فرض را ندارد، هنگام فراخوانی یک تابع Kotlin با مقادیر پارامترهای پیش فرض از جاوا، باید تمام مقادیر پارامتر را به صراحت مشخص کنید. اگر از جاوا تابعی می خواهید فراخوانی کنید شما میتوانید با استفاده از حاشیه نویسی @JvmOverloads آن را برای تماس گیرندگان جاوا آسان تر کنید. این به کامپایلر دستور می دهد تا متدهای بارگذاری شده جاوا را تولید کند و هر یک از پارامترها را یکی یکی حذف کند و از آخرین مورد شروع شود.
به عنوان مثال، اگر joinToString را با @JvmOverloads حاشیه نویسی کنید، اضافه بارهای زیر ایجاد می شوند.
/* Java */ String joinToString(Collection<T> collection, String separator, String prefix, String postfix); String joinToString(Collection<T> collection, String separator, String prefix); String joinToString(Collection<T> collection, String separator); String joinToString(Collection<T> collection);
هر اضافه بار از مقادیر پیش فرض پارامترهایی استفاده می کند که از امضا حذف شده اند.
تا کنون، بدون توجه context اطراف، روی function کاربردی خود کار کرده اید. مطمئناً باید روشی از کلاسی باشد که در فهرست های مثال نشان داده نشده است، درست است؟ در واقع، کاتلین این را غیر ضروری می کند
همه ما می دانیم که جاوا، به عنوان یک زبان شی گرا، نیاز دارد که همه ی کدها به عنوان متدهای کلاس ها نوشته شوند. معمولا، این کار به خوبی انجام می شود. اما در واقعیت، هر پروژه بزرگ با تعداد زیادی کد به پایان می رسد که به هیچ کلاسی تعلق ندارد. گاهی اوقات یک operation با objects از دو کلاس متفاوت کار می کند که نقش به همان اندازه برای آن مهم است. گاهی اوقات یک شی اصلی وجود دارد، اما نمی خواهید API آن را با اضافه کردن operation به عنوان یک method ، پر کنید.
در نتیجه، شما با کلاس هایی مواجه می شوید که شامل هیچ state یا method نیستند و به عنوان محفظه هایی برای دسته ای از متدهای استاتیک عمل می کنند. یک مثال کامل، کلاس Collections در JDK است. برای پیداکردن نمونه های دیگر در کد خود، به دنبال کلاس هایی بگردید که Util را به عنوان بخشی از نام دارند.
در Kotlin، شما نیازی به ایجاد تمام آن کلاس های بی معنی ندارید. در عوض، می توانید توابع را مستقیماً یک فایل منبع در سطح بالا ، خارج از هر کلاسی قرار دهید. چنین توابعی هنوز عضو package هستند که در بالای فایل اعلام شده است، و اگر می خواهید آنها را از package های دیگر فراخوانی کنید، باید آنها را وارد کنید، اما سطح اضافی تودرتو دیگر وجود ندارد.
بیایید تابع joinToString را مستقیماً در بسته رشته ها قرار دهیم. یک فایل به نام join.kt با محتویات زیر ایجاد کنید.
package strings fun joinToString(...): String { ... }
این کد چگونه اجرا می شود؟ می دانید که وقتی فایل را کامپایل می کنید، برخی از کلاس ها تولید می شوند، زیرا JVM فقط می تواند کد را در کلاس ها اجرا کند. وقتی فقط با Kotlin کار می کنید، این تنها چیزی است که باید بدانید. اما اگر نیاز به فراخوانی چنین تابعی از جاوا دارید، باید نحوه کامپایل شدن آن را بدانید. برای روشن شدن این موضوع، اجازه دهید به کد جاوا که در همان کلاس کامپایل می شود نگاه کنیم:
/* Java */ package strings; public class JoinKt { public static String joinToString(...) { ... } }
نام کلاس تولید شده توسط کامپایلر Kotlin با نام فایل که حاوی تابع اس مطابقت دارد. تمام توابع سطح بالا در فایل به متدهای ثابت آن کلاس کامپایل می شوند. بنابراین، فراخوانی این تابع از جاوا به آسانی فراخوانی هر روش استاتیک دیگر است:
/* Java */ import strings.JoinKt; ... JoinKt.joinToString(list, ", ", "", "");
برای تغییر نام کلاس تولید شده که حاوی توابع سطح بالای Kotlin است، یک حاشیه نویسی @JvmName به فایل اضافه می کنید. آن را در ابتدای فایل، قبل از نام بسته قرار دهید:
@file:JvmName("StringFunctions") package strings fun joinToString(...): String { ... }
حالا تابع را می توان به صورت زیر فراخوانی کرد:
/* Java */ import strings.StringFunctions; StringFunctions.joinToString(list, ", ", "", "");
بحث مفصلی در مورد نحو حاشیه نویسی بعداً در فصل 10 ارائه می شود.
درست مانند توابع، PROPERTIES ها را می توان در بالای یک فایل قرار داد. ذخیره تک تک داده ها در خارج از یک کلاس اغلب مورد نیاز نیست، اما همچنان مفید است.
به عنوان مثال، می توانید از یک ویژگی var برای شمارش تعداد دفعاتی که برخی از عملیات انجام شده است استفاده کنید:
var opCount = 0 fun performOperation() { opCount++ // ... } fun reportOperationCount() { println("Operation performed $opCount times") }
مقدار چنین ویژگی در یک فیلد ثابت ذخیره می شود.
ویژگی های سطح بالا همچنین به شما اجازه می دهد تا در کد خود ثابت ها را تعریف کنید:
val UNIX_LINE_SEPARATOR = "\n"
به طور پیش فرض، ویژگی های سطح بالا، درست مانند هر ویژگی دیگر، درکد جاوا به عنوان متدهای دسترسی هستند (یک گیرنده برای ویژگی val و یک جفت getter/setter برای یک ویژگی var).
const val UNIX_LINE_SEPARATOR = "\n"
این معادل کد جاوا زیر را به شما می دهد:
/* Java */ public static final String UNIX_LINE_SEPARATOR = "\n"
شما عملکرد اولیه joinToString را بسیار بهبود بخشیده اید. حالا بیایید ببینیم که چگونه آن را حتی ساده تر کنیم.
یکی از موضوعات اصلی Kotlin هموار کردن ادغام کدها با کدهای موجود است. حتی پروژه های خالص Kotlin بر روی کتابخانه های جاوا مانند JDK، فریمورک اندروید و سایر فریمورک هایی که توسط سایر برنامه نویسان ساخته می شوند. و هنگامی که کاتلین را در یک پروژه جاوا ادغام می کنید، با کدهای موجود نیز سر و کار دارید که به کاتلین تبدیل نشده یا نخواهند شد. آیا بهتر نیست که بتوانیم از تمام ویژگی های کاتلین هنگام کار با آن API ها استفاده کنیم، بدون اینکه نیازی به بازنویسی آن ها باشد؟ این همان چیزی است که extension functions به شما اجازه انجام آن را می دهند.
از نظر مفهومی، یک extension function چیز ساده ای است: تابعی است که می تواند به عنوان عضوی از یک کلاس فراخوانی شود اما خارج از آن تعریف شده است. برای نشان دادن آن، بیایید روشی برای محاسبه آخرین کاراکتر یک رشته اضافه کنیم:
package strings fun String.lastChar(): Char = this.get(this.length - 1)
تنها کاری که باید انجام دهید این است که نام کلاس یا interface را قبل از نام تابعی که اضافه می کنید قرار دهید.
این نام کلاس receiver type نامیده می شود. مقداری که extension function را روی آن فراخوانی می کنید، receiver object نامیده می شود. این در شکل 3.1 نشان داده شده است.
شکل 3.1 receiver type نوعی است که پسوند بر روی آن تعریف شده است و receiver object نمونه ای از آن نوع است.
می توانید تابع را با استفاده از همان function که برای اعضای کلاس معمولی استفاده می کنید فراخوانی کنید:
>>> println("Kotlin".lastChar()) n
در این مثال، String نوع گیرنده و "Kotlin" شی گیرنده است.
به یک معنا، شما متد خود را به کلاس String اضافه کرده اید. اگرچه String بخشی از کد شما نیست و حتی ممکن است کد منبع آن کلاس را نداشته باشید، ولی می توانید آن را با methodهایی که در پروژه خود نیاز دارید گسترش دهید. حتی مهم نیست که String به جاوا، کاتلین یا زبان JVM دیگری مانند Groovy نوشته شده باشد. تا زمانی که در یک کلاس جاوا کامپایل شده است، می توانید پسوندهای خود را به آن کلاس اضافه کنید.
در بدنه یک extension function ، شما از آن همانطور که در یک متد استفاده می کنید استفاده کنید. و مانند یک روش معمولی، می توانید آن را حذف کنید:
package string fun String.lastChar(): Char = get(length - 1)
در extension function ، می توانید مستقیماً به متدها و ویژگی های کلاسی که در حال گسترش آن هستید دسترسی داشته باشید، مانند متدهایی که در خود کلاس تعریف شده است. توجه داشته باشید که extension function به شما اجازه نمی دهند encapsulation را بشکنید. برخلاف متدهای تعریف شده در کلاس، extension function به اعضای خصوصی یا محافظت شده کلاس دسترسی ندارند.
بعداً از متد هم برای اعضای کلاس و هم برای extension function استفاده خواهیم کرد. به عنوان مثال، می توان گفت که در بدنه extension function می توانید هر متدی را در receiver فراخوانی کنید، به این معنی که می توانید هم اعضا و هم extension function را فراخوانی کنید. در محل فراخوانی، extension function از اعضا قابل تشخیص نیستند، و اغلب فرقی نمی کند که روش خاص یک عضو باشد یا یک extension .
وقتی یک extension function را تعریف می کنید، به طور خودکار در کل پروژه شما در دسترس قرار نمی گیرد. در عوض، مانند هر کلاس یا تابع دیگری، باید وارد شود. این به جلوگیری از تضاد نام تصادفی کمک می کند. Kotlin به شما امکان می دهد توابع جداگانه را با استفاده از همان syntax که برای کلاس ها استفاده می کنید وارد کنید:
import strings.lastChar val c = "Kotlin".lastChar()
البته ، * imports نیز کار می کند:
import strings.* val c = "Kotlin".lastChar()
می توانید نام کلاس یا تابعی را که وارد می کنید با استفاده از کلمه کلیدی as تغییر دهید:
import strings.lastChar as last val c = "Kotlin".last()
تغییر نام در هنگام import زمانی مفید است که چندین تابع با یک نام در package های مختلف دارید و می خواهید از آنها در یک فایل استفاده کنید. برای کلاس ها یا توابع معمولی، یک انتخاب دیگر دارید: می توانید از یک نام کاملاً واجد شرایط برای اشاره به کلاس یا تابع استفاده کنید. برای extension function ، سیتکس از شما می خواهد که از نام کوتاه استفاده کنید، بنابراین کلمه کلیدی as در دستور import تنها راه حل است.
در پس زمینه، یک extension function یک روش ثابت است که receiver object را به عنوان اولین آرگومان خود می پذیرد. فراخوانی آن مستلزم ایجاد اشیاء آداپتور یا overhead زمان اجرا دیگری نیست.
این کار استفاده از extension functions از جاوا را بسیار آسان می کند: شما متد استاتیک را فراخوانی می کنید و نمونه receiver object را ارسال می کنید. درست مانند سایر توابع سطح بالا، نام کلاس جاوا حاوی متد از نام فایلی که تابع در آن اعلان شده است تعیین می شود. فرض کنید در یک فایل StringUtil.kt اعلام شده است:
/* Java */ char c = StringUtilKt.lastChar("Java");
این extension functions به عنوان یک تابع سطح بالا اعلام شده است، بنابراین به یک روش ایستا کامپایل می شود. شما می توانید متد lastChar را به صورت ایستا از جاوا وارد کنید و استفاده از آن را به ("جاوا")lastChar ساده کنید. این کد تا حدودی کمتر از نسخه Kotlin قابل خواندن است، اما از نظر جاوا قابل خواندن نیست.
اکنون می توانید نسخه نهایی تابع joinToString را بنویسید. این تقریباً همان چیزی است که در کتابخانه استاندارد Kotlin پیدا خواهید کرد
fun <T> Collection<T>.joinToString( separator: String = ", ", prefix: String = "", postfix: String = "" ): String { val result = StringBuilder(prefix) for ((index, element) in this.withIndex()) if (index > 0) result.append(separator) result.append(element) } result.append(postfix) return result.toString() } >>> val list = listOf(1, 2, 3) >>> println(list.joinToString(separator = " ", ... prefix = "(", postfix = ")")) (1; 2; 3)
شما آن extension را برای مجموعه ای از عناصر قرار می دهید و مقادیر پیش فرض را برای همه آرگومان ها ارائه می دهید. اکنون می توانید joinToString را مانند عضوی از یک کلاس فراخوانی کنید:
>>> val list = arrayListOf(1, 2, 3) >>> println(list.joinToString(" ")) 1 2 3
از آنجایی که extension functions به طور مؤثر یک syntactic شیرینیه که در فراخوانی متد ایستا هستند، می توانید از نوع خاص تری به عنوان receiver type استفاده کنید، نه فقط یک کلاس. فرض کنید می خواهید یک تابع join داشته باشید که فقط در مجموعه ای از stringها فراخوانی شود.
fun Collection<String>.join( separator: String = ", ", prefix: String = "", postfix: String = "" ) = joinToString(separator, prefix, postfix) >>> println(listOf("one", "two", "eight").join(" ")) one two eight
فراخوانی این تابع با لیستی از اشیاء از نوع دیگر کار نخواهد کرد:
>>> listOf(1, 2, 8).join() Error: Type mismatch: inferred type is List<Int> but Collection<String> was expected.
extensions ایستا به این معنی است که extension functions را نمی توان در کلاس های فرعی نادیده گرفت. بیایید به یک مثال نگاه کنیم.
متد overriding در Kotlin برای توابع عضو طبق معمول کار می کند، اما شما نمی توانید یک extension function را بازنویسی(overrid) کنید. فرض کنید شما دو کلاس دارید، View و زیر کلاس آن Button، و کلاس Button تابع کلیک را از superclass بازنویسی(overrid) می کند.
open class View { open fun click() = println("View clicked") } class Button: View() { override fun click() = println("Button clicked") }
اگر متغیری از نوع View را اعلام کنید، می توانید مقداری از نوع Button را در آن متغیر ذخیره کنید، زیرا Button زیرنوع(subtype) View است. اگر یک متد معمولی مانند کلیک را بر روی این متغیر فراخوانی کنید و آن متد در کلاس Button بازنویسی(overrid) شود، پیاده سازی overrid شده از کلاس Button استفاده خواهد شد:
>>> val view: View = Button() >>> view.click() Button clicked
اما همانطور که در شکل 3.2 نشان داده شده است، برای روش های extensions کار نمی کند.
extension function ها بخشی از کلاس نیستند. آنها به صورت خارجی به آن اعلام شده اند. حتی اگر می توانید extension function را با همان نام و نوع پارامتر برای یک کلاس پایه و زیر کلاس آن تعریف کنید، تابعی که فراخوانی می شود به نوع استاتیک اعلام شده متغیر بستگی دارد، نه به نوع زمان اجرا مقدار ذخیره شده در آن متغیر.
مثال زیر دو extension functions را نشان می دهد که در کلاس های View و Button اعلام شده اند.
fun View.showOff() = println("I'm a view!") fun Button.showOff() = println("I'm a button!") >>> val view: View = Button() >>> view.showOff() I'm a view!
وقتی showOff را روی متغیری از نوع View فراخوانی می کنید، extension مربوطه فراخوانی می شود، حتی اگر نوع واقعی مقدار Button باشد.
اگر به یاد داشته باشید که یک extension functions به یک تابع ثابت در جاوا با گیرنده به عنوان اولین آرگومان کامپایل شده است، این رفتار باید برای شما واضح باشد، زیرا جاوا تابع را به همین ترتیب انتخاب می کند:
/* Java */ >>> View view = new Button(); >>> ExtensionsKt.showOff(view); I'm a view!
همانطور که می بینید، overrid برای extension function اعمال نمی شود: Kotlin آنها را به صورت ایستا حل می کند.
نکته : اگر کلاس دارای یک تابع عضو با امضای یک extension function باشد، تابع عضو همیشه اولویت دارد. هنگام گسترش API کلاس ها باید این را در نظر داشته باشید: اگر یک تابع عضو با امضای مشابه به عنوان extension function که کاربر کلاس شما تعریف کرده است اضافه کنید و سپس آنها کد خود را دوباره کامپایل کنند، معنی آن تغییر می کند و با اشاره به تابع عضو جدید شروع می شود.
ما در مورد چگونگی ارائه متد های اضافی برای کلاس های خارجی بحث کرده ایم. حال بیایید ببینیم که چگونه می توان همین کار را با properties انجام داد.
ویژگی های extension راهی برای گسترش کلاس ها با API ارائه می دهد که می توان با استفاده از syntax ویژگی(property) به جای syntax تابع به آنها دسترسی داشت. حتی اگر به آنها ویژگی(properties) بگن، نمی توانند هیچ حالتی داشته باشند، زیرا مکان مناسبی برای ذخیره آن وجود ندارد: اضافه کردن فیلدهای اضافی به نمونه های موجود از اشیاء جاوا امکان پذیر نیست. اما syntax کوتاهتر هنوز هم گاهی اوقات مفید است
در قسمت قبل، تابع lastChar را تعریف کردید. حالا بیایید آن را به یک ویژگی(property) تبدیل کنیم
val String.lastChar: Char get() = get(length - 1)
ی نید ببینید که مثل توابع، یک ویژگی extension مانند یک ویژگی معمولی با یک نوع گیرنده(receiver) به نظر می رسد. دریافت کننده(get) همیشه باید تعریف شود، زیرا هیچ فیلد پشتیبان و گیرنده(get) پیش فرضی وجود ندارد. مقدار دهی اولیه نشدند به همین دلیل مجاز نیستند: جایی برای ذخیره مقدار مشخص شده به عنوان مقدار اولیه وجود ندارد.
اگر همان خاصیت را در StringBuilder تعریف کنید، می توانید آن را var کنید، زیرا محتویات یک StringBuilder قابل تغییر است.
var StringBuilder.lastChar: Char get() = get(length - 1) set(value: Char) { this.setCharAt(length - 1, value) }
شما دقیقاً مانند ویژگی های عضو به ویژگی های افزونه دسترسی دارید:
>>> println("Kotlin".lastChar) n >>> val sb = StringBuilder("Kotlin?") >>> sb.lastChar = '!' >>> println(sb) Kotlin!
توجه داشته باشید که وقتی نیاز به دسترسی به یک ویژگی extension از جاوا دارید، باید دریافت کننده آن را به طور واضح فراخوانی کنید: StringUtilKt.getLastChar("Java").
ما در مورد مفهوم extensions ها به طور کلی بحث کرده ایم. اکنون اجازه دهید به مبحث مجموعه ها برگردیم و به چند function کتابخانه دیگر که به شما در مدیریت آنها کمک می کنند و همچنین ویژگی های زبانی که در آن توابع ارائه می شوند نگاهی بیندازیم.
این بخش برخی از functions های کتابخانه استاندارد Kotlin را برای کار با مجموعه ها نشان می دهد. در طول مسیر، چند ویژگی زبان مرتبط را شرح می دهد:
ما این فصل را با این ایده شروع کردیم که مجموعه ها در کاتلین همان کلاس های جاوا هستند، اما با یک API توسعه یافته. بعنوان مثال دریافت آخرین عنصر در لیست و یافتن بزرگترین عدد در مجموعه ای از اعداد را مشاهده کردید:
>>> val strings: List<String> = listOf("first", "second", "fourteenth") >>> strings.last() fourteenth >>> val numbers: Collection<Int> = setOf(1, 14, 2) >>> numbers.max() 14
ما به نحوه عملکرد آن علاقه مند بودیم: چرا می توان کارهای زیادی را با مجموعه ها در Kotlin انجام داد، حتی اگر آنها نمونه هایی از کلاس های کتابخانه جاوا هستند. اکنون پاسخ باید واضح باشد: توابع last و max به عنوان extension functions اعلام می شوند!
آخرین تابع پیچیده تر از lastChar برای String نیست، که در بخش قبل مورد بحث قرار گرفت: این یک extension در کلاس List است. حداکثر، یک اعلان ساده نشان می دهیم (function کتابخانه واقعی نه تنها برای اعداد Int، بلکه برای هر عنصر قابل مقایسه کار می کند):
fun <T> List<T>.last(): T { /* returns the last element */ } fun Collection<Int>.max(): Int { /* finding a maximum in a collection */ }
بسیاری از extension functions در کتابخانه استاندارد Kotlin نوشته شده اند و ما همه آنها را در اینجا فهرست نمی کنیم. ممکن است در مورد بهترین راه برای یادگیری همه چیز در کتابخانه استاندارد Kotlin تعجب کنید. شما مجبور نیستید کل کتابخانه کاتلین را بلد باشید هر زمان نیاز به انجام کاری با یک مجموعه یا هر شی دیگری داشتید خود IDE در تکمیل کد به شما کمک میکند تمام توابع موجود برای آن نوع شی را به شما نشان می دهد. این فهرست شامل متدهای معمولی و extension functions است. می توانید function مورد نیاز خود را انتخاب کنید. علاوه بر آن، مرجع استاندارد کتابخانه تمام متد های موجود برای هر کلاس کتابخانه را فهرست می کند - اعضا و همچنین extension ها.
در ابتدای فصل، توابع ایجاد مجموعه ها را نیز مشاهده کردید. یک ویژگی مشترک این توابع این است که می توان آنها را با تعداد دلخواه آرگومان فراخوانی کرد. در بخش زیر، سینتکسی را برای اعلان چنین توابعی را توضیح خواهیم داد.
هنگامی که یک تابع را برای ایجاد یک لیست فراخوانی می کنید، می توانید هر تعداد آرگومان را به آن ارسال کنید:
val list = listOf(2, 3, 5, 7, 11)
اگر به نحوه اعلان این تابع در کتابخانه نگاه کنید، موارد زیر را خواهید دید:
fun listOf<T>(vararg values: T): List<T> { ... }
احتمالاً با varargs جاوا آشنا هستید: ویژگی که به شما امکان می دهد تعداد دلخواه از مقادیر را با packing آنها در یک آرایه به یک متد ارسال کنید. Varargsهای کاتلین شبیه به جاوا هستند، اما syntax انها کمی متفاوت است: به جای سه نقطه بعد از نوع، کاتلین از vararg modifier روی پارامتر استفاده می کند.
یکی دیگر از تفاوت های کاتلین و جاوا، syntax فراخوانی تابع که چه زمانی آرگومان ها نیاز به ارسال قبل از packed در یک آرایه دارند. در جاوا، آرایه را همانطور که هست ارسال می کنید، در حالی که کاتلین از شما می خواهد که به صراحت آرایه را باز کنید، به طوری که هر عنصر آرایه به یک آرگومان جداگانه برای تابع فراخوانی شده تبدیل شود. از نظر فنی، این ویژگی با استفاده از عملگر spread (سه نقطه) نامیده می شود، اما در عمل به سادگی قرار دادن آن است
* کاراکتر قبل از آرگومان مربوطه:
fun main(args: Array<String>) { val list = listOf("args: ", *args) println(list) }
این مثال نشان می دهد که عملگر spread به شما امکان می دهد مقادیر یک آرایه و برخی مقادیر ثابت را در یک فراخوانی ترکیب کنید. این در جاوا پشتیبانی نمی شود.
حالا بریم سراغ map ها. ما به طور خلاصه روش دیگری برای بهبود خوانایی فراخوانی تابع Kotlin را مورد بحث قرار خواهیم داد: فراخوانی infix
برای ایجاد نقشه ها، از تابع map Of استفاده می کنید:
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
این زمان خوبی است تا توضیح دیگری را که در ابتدای فصل به شما قول داده بودیم ارائه دهیم. کلمه to در این خط کد یک ساختار داخلی نیست، بلکه یک فراخوانی متد از نوع خاصی است که infix call نامیده می شود.
در فراخوانی infix ، نام متد بلافاصله بین نام شی هدف و پارامتر بدون جدا کننده اضافی قرار می گیرد. مثل دو فراخوانی زیر هستند:
فراخوانی Infix را می توان با متد های معمولی و extension functions که یک پارامتر مورد نیاز دارند، استفاده کرد. برای اینکه یک تابع با استفاده از نماد infix فراخوانی شود، باید آن را با اصلاح کننده infix علامت گذاری کنید. در اینجا یک مثال ساده از اعلان تابع to میزنیم:
infix fun Any.to(other: Any) = Pair(this, other)
تابع to نمونه ای از Pair را برمی گرداند، که یک کلاس کتابخانه استاندارد Kotlin است ، عنصر pair را نشان می دهد. اعلانات واقعی Pair و استفاده از generics ها، اما ما آنها را در اینجا حذف می کنیم تا همه چیز ساده باشد.
توجه داشته باشید که می توانید مستقیماً دو متغیر را با محتویات یک Pair مقداردهی اولیه کنید:
val (number, name) = 1 to "one"
به این ویژگی یک اعلان تخریبی می گویند. شکل 3.3 نحوه کار با Pair را نشان می دهد.
شکل 3.3 شما یک جفت با استفاده از تابع to ایجاد می کنید و آن را با یک اعلان ساختارشکن باز می کنید.
ویژگی اعلان تخریب ساختار به pair محدود نمی شود. به عنوان مثال، شما همچنین می توانید دو متغیر کلید(key) و مقدار(value) را با محتویات یک ورودی نقشه مقداردهی اولیه کنید.
همانطور که در اجرای joinToString که از تابع withIndex استفاده می کند، این کار با حلقه ها نیز کار می کند:
for ((index, element) in collection.withIndex()) { println("$index: $element") }
بخش 7.4 قوانین کلی برای تخریب یک عبارت و استفاده از آن برای مقداردهی اولیه چندین متغیر را شرح می دهد.
تابع to یک extension function است. شما می توانید یک جفت(pair) از هر عنصر را ایجاد کنید، به این معنی که این یک extension برای یک گیرنده عمومی(generic receiver) است: می توانید 1 را به ""one ، one"" را به 1، list را به list.size() و غیره بنویسید. بیایید به اعلان تابع mapOf نگاه کنیم:
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>
مانند listOf، mapOf تعداد متغیری از آرگومان ها را می پذیرد، اما این بار آنها باید جفت(pair) کلید(key) و مقادیر(value) باشند.
اگرچه ایجاد یک map جدید ممکن است مانند یک ساختار خاص در Kotlin به نظر برسد، اما یک تابع منظم با یک syntax مختصر است. در مرحله بعد، بیایید در مورد اینکه چگونه extensions برخورد با string ها و expressions منظم را ساده می کنند، بحث کنیم.
استرینگ های کاتلین دقیقاً همان string های جاوا هستند. می توانید string ای را که در کد کاتلین ایجاد شده است به method جاوا منتقل کنید و می توانید از متد کتابخانه استاندارد کاتلین روی string هایی که از کد جاوا دریافت می کنید استفاده کنید. هیچ تبدیلی در کار نیست، و هیچ شیء پوشش اضافی ایجاد نمی شود.
کاتلین کار با string های استاندارد جاوا را با ارائه یکسری extension functions مفید لذت بخش تر می کند. همچنین، برخی از method های گیج کننده را پنهان می کند و extensions هایی را که واضح تر هستند اضافه می کند. به عنوان اولین مثال ما از تفاوت های API، بیایید ببینیم کاتلین چگونه string ها را تقسیم می کند.
احتمالاً با روش تقسیم در String آشنا هستید. همه از آن استفاده می کنند، اما گاهی اوقات افراد در Stack Overflow (http://stackoverflow.com) از آن شکایت می کنند: "متد split در جاوا با یک نقطه (.) کار نمی کند.
نوشتن "12.345-6.A".split(".") و انتظار داشتن آرایه [12, 345-6, A] در نتیجه، یک تله معمولی است. اما متد split جاوا یک آرایه خالی برمی گرداند! این اتفاق می افتد زیرا یک regular expressions را به عنوان پارامتر می گیرد و split یک string را با توجه به expression به چندین رشته تقسیم می کند. در اینجا، نقطه (.) یک regular expressions است که هر کاراکتری را نشان می دهد.
Kotlin متد گیج کننده را پنهان می کند و چندین پسوند اضافه بار به نام split را به عنوان جایگزین ارائه می دهد که آرگومان های متفاوتی دارند. چیزی که یک regular expressions می گیرد به مقداری از نوع Regex نیاز دارد، نه String. این تضمین می کند که آیا String ای که به یک متد ارسال می شود به عنوان متن ساده یا یک regular expressions تفسیر می شود.
در اینجا نحوه تقسیم رشته با یک نقطه یا یک خط تیره آمده است:
>>> println("12.345-6.A".split("\\.|-".toRegex())) [12, 345, 6, A]
کاتلین دقیقاً از همان سینتکس regular expressions مانند جاوا استفاده می کند. الگوی(pattern) اینجا با یک نقطه مطابقت دارد (ما از آن فرار کردیم تا نشان دهیم منظور ما یک کاراکتر تحت اللفظی است، نه یک علامت عام) یا یک خط تیره. APIهای کار با regular expressions نیز مشابه APIهای استاندارد کتابخانه جاوا هستند، اما اصطلاحی تر هستند. به عنوان مثال، در کاتلین شما از یک تابع پسوند(extension function) toRegex برای تبدیل یک String به یک regular expressions استفاده می کنید.
اما برای چنین مورد ساده ای، نیازی به استفاده از regular expressions ندارید. overload دیگر split extension function در کاتلین تعداد دلخواه خود را به عنوان رشته های متن ساده می گیرد:
>>> println("12.345-6.A".split(".", "-")) [12, 345, 6, A]
توجه داشته باشید که به جای آن می توانید آرگومان های کاراکتر را مشخص کنید و بنویسید .split('-', '.')"12.345-6.A" که به همان نتیجه منجر می شود. این متد جایگزین متد جاوای مشابه می شود که می تواند تنها یک کاراکتر را به عنوان جداکننده بگیرد.
بیایید به مثال دیگری با دو پیاده سازی متفاوت نگاه کنیم: اولی از پسوندها در String استفاده می کند و دومی با عبارات منظم کار می کند. وظیفه شما این است که نام مسیر کامل یک فایل را به اجزای آن تفکیک کنید: یک directory ، یک نام فایل و یک پسوند. کتابخانه استاندارد Kotlin شامل توابعی برای دریافت زیررشته قبل (یا بعد از) اولین (یا آخرین) وقوع جداکننده داده شده است. در اینجا نحوه استفاده از آنها برای حل این کار آمده است (شکل 3.4 را نیز ببینید).
fun parsePath(path: String) { val directory = path.substringBeforeLast("/") val fullName = path.substringAfterLast("/") val fileName = fullName.substringBeforeLast(".") val extension = fullName.substringAfterLast(".") println("Dir: $directory, name: $fileName, ext: $extension") } >>> parsePath("/Users/yole/kotlin-book/chapter.adoc") Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
رشته فرعی قبل از آخرین علامت اسلش(/) مسیر فایل، مسیر یک دایرکتوری محصور کننده است، رشته فرعی بعد از آخرین نقطه یک پسوند فایل است و نام فایل بین آنها قرار می گیرد.
کاتلین تفکیک رشته ها را بدون استفاده از عبارات منظم (regular expressions) آسان تر می کند، عباراتی که قدرتمند هستند اما گاهی اوقات درک آنها پس از نوشتن دشوار است. اگر می خواهید از عبارات منظم استفاده کنید، کتابخانه استاندارد کاتلین می تواند کمک کند. در اینجا نحوه انجام یک کار مشابه با استفاده از عبارات منظم آمده است:
fun parsePath(path: String) { val regex = """(.+)/(.+)\.(.+)""".toRegex() val matchResult = regex.matchEntire(path) if (matchResult != null) { val (directory, filename, extension) = matchResult.destructured println("Dir: $directory, name: $filename, ext: $extension") } }
در این مثال، عبارت منظم در یک رشته نقل قول سه گانه نوشته شده است. در چنین رشته ای، نیازی به حذف از هیچ کاراکتری از جمله بک اسلش نیست، بنابراین می توانید نماد نقطه را با \ رمزگذاری کنید. به جای \\. همانطور که در یک رشته معمولی می نویسید (شکل 3.5 را ببینید).
این عبارت منظم یک مسیر(path) را به سه گروه تقسیم می کند که با یک اسلش و یک نقطه از هم جدا می شوند. الگو با هر کاراکتری از ابتدا مطابقت دارد، بنابراین اولین گروه (+.) شامل رشته فرعی قبل از آخرین اسلش است. این زیر رشته شامل تمام اسلش های قبلی است، زیرا با الگوی "هر کاراکتری" مطابقت دارند. به همین ترتیب، گروه دوم شامل زیر رشته قبل از آخرین نقطه و گروه سوم شامل قسمت باقی مانده است.
حال بیایید پیاده سازی تابع parsePath را از مثال قبلی مورد بحث قرار دهیم. شما یک عبارت منظم ایجاد می کنید و آن را با یک مسیر ورودی مطابقت می دهید. اگر تطابق موفقیت آمیز باشد (نتیجه صفر نیست)، مقدار ویژگی بدون ساختار آن را به متغیرهای مربوطه اختصاص می دهید. این همان syntax است که هنگام مقدار دهی اولیه دو متغیر با هم استفاده می شود. بخش 7.4 جزئیات را پوشش می دهد.
هدف از رشته های سه گانه فقط جلوگیری از جداسازی کاراکترها نیست. چنین رشته ای می تواند شامل هر کاراکتری از جمله شکست خط(line breaks) باشد. این به شما راهی آسان برای قراردادن متنی که حاوی خطوط شکسته است را در برنامه خود می دهد. به عنوان مثال، اجازه دهید برخی از توانایی های ASCII را ترسیم کنیم:
val kotlinLogo = """| // .|// .|/ \""" >>> println(kotlinLogo.trimMargin(".")) | // |// |/ \
رشته(string) چند خطی شامل تمام کاراکترهای بین نقل قول های سه گانه، از جمله تورفتگی های(indent) مورد استفاده برای قالب بندی کد است. اگر می خواهید نمایش بهتری از چنین رشته ای داشته باشید، می توانید تورفتگی (به عبارت دیگر، حاشیه سمت چپ) را کوتاه کنید. برای انجام این کار، یک پیشوند به محتوای رشته اضافه می کنید و انتهای حاشیه را علامت گذاری می کنید و سپس trimMargin را صدا می زنید تا پیشوند و فضای خالی قبلی در هر خط حذف شود. این مثال از نقطه به عنوان پیشوند استفاده می کند.
یک رشته با نقل قول سه گانه می تواند شامل شکسته های خط باشد، اما نمی توانید از کاراکترهای خاصی مانند \n استفاده کنید. از طرف دیگر، شما نیازی به جداسازی ندارید، بنابراین مسیر به سبک ویندوز "C:\\Users\\yole\\kotlin-book" را می توان به صورت """C:\Users\yole\ kotlin-book""" نوشت.
همچنین می توانید از قالب های رشته ای در رشته های چند خطی استفاده کنید. از آنجایی که رشته های چند خطی (escape sequences) توالی جداسازی را پشتیبانی نمی کنند، اگر نیاز به استفاده از علامت واقعی دلار در محتویات رشته خود دارید، باید از یک عبارت تعبیه شده استفاده کنید. به مثال توجه کنید :
val price = """${'$'}99.9"""
یکی از زمینه هایی که رشته های چند خطی می تواند در برنامه های شما مفید باشد (علاوه بر بازی هایی که از توانایی ASCII استفاده می کنند) تست ها است. در آزمایش ها، اجرای عملیاتی که متن چند خطی تولید می کند (به عنوان مثال، یک تکه صفحه وب) و مقایسه نتیجه با خروجی مورد انتظار، نسبتاً معمول است. رشته های چند خطی راه حلی عالی برای گنجاندن خروجی مورد انتظار به عنوان بخشی از آزمایش به شما می دهد. بدون نیاز به جداسازی ناشیانه یا بارگذاری متن از فایل های خارجی - فقط چند علامت نقل قول قرار دهید و HTML مورد انتظار یا خروجی دیگر را بین آنها قرار دهید. و برای قالب بندی بهتر از تابع trimMargin فوق الذکر استفاده کنید که نمونه دیگری از تابع افزونه (extension function) است.
توجه: اکنون می توانید ببینید که توابع افزونه راهی قدرتمند برای گسترش APIهای کتابخانه های موجود و تطبیق آنها با اصطلاحات زبان جدید شما هستند – چیزی به نام الگوی Pimp My Library و در واقع، بخش بزرگی از کتابخانه استاندارد کاتلین از توابع پسوندی برای کلاس های استاندارد جاوا تشکیل شده است. کتابخانه Anko ( https://github.com/kotlin/anko ) ، که توسط JetBrains نیز ساخته شده است، توابع افزونه (extension functions) را ارائه می کند که API Android را سازگارتر با کاتلین می کند. همچنین می توانید بسیاری از کتابخانه های جامعه برنامه نویسان را بیابید که بسته بندی های مناسب کاتلین را در کتابخانه های اصلی شخص ثالث مانند Spring ارائه و بسته بندی میکند.
اکنون که می توانید ببینید کاتلین چگونه APIهای بهتری را برای کتابخانه هایی که استفاده می کنید به شما می دهد، بیایید توجه خود را به کد شما معطوف کنیم. برخی از کاربردهای جدید برای توابع افزونه را خواهید دید، و همچنین در مورد مفهوم جدیدی بحث خواهیم کرد: توابع محلی(local functions).
بسیاری از توسعه دهندگان بر این باورند که یکی از مهمترین ویژگی های کد خوب و با کیفیت عدم تکرار است. حتی یک نام خاص برای این اصل وجود دارد: خودت را تکرار نکن "Don’t Repeat Yourself"(DRY). اما در جاوا رعایت این اصل همیشه بی اهمیت نیست. در بسیاری از موارد، این امکان وجود دارد که از ویژگی Extract Method refactoring IDE خود برای تقسیم متدهای طولانی تر به تکه های کوچک تر و سپس استفاده مجدد از آن تکه ها استفاده کنید. اما این می تواند درک کد را سخت تر کند، زیرا در نهایت با کلاسی مواجه می شوید که متدهای کوچک زیادی دارد و هیچ رابطه واضحی بین آنها وجود ندارد. شما می توانید حتی فراتر رفته و روش های استخراج شده را در یک کلاس داخلی گروه بندی کنید، که به شما امکان می دهد ساختار را حفظ کنید، اما این رویکرد به مقدار قابل توجهی از کد های تکراری نیاز دارد.
کاتلین راه حل تمیزتری به شما می دهد: می توانید توابعی را که خارج کرده اید در تابع دیگر بصورت تودرتو استفاده کنید. به این ترتیب، شما ساختار مورد نیاز خود را بدون سربار دستوری اضافی خواهید داشت.
بیایید ببینیم که چگونه از توابع محلی برای رفع یک مورد نسبتاً رایج تکرار کد استفاده کنیم. در فهرست زیر، یک تابع یک کاربر را در پایگاه داده ذخیره می کند و شما باید مطمئن شوید که شی کاربر حاوی داده های معتبر است.
class User(val id: Int, val name: String, val address: String) fun saveUser(user: User) { if (user.name.isEmpty()) { throw IllegalArgumentException( "Can't save user ${user.id}: empty Name") } if (user.address.isEmpty()) { throw IllegalArgumentException( "Can't save user ${user.id}: empty Address") } // Save user to the database } >>> saveUser(User(1, "", "")) java.lang.IllegalArgumentException: Can't save user 1: empty Name
مقدار کدهای تکراری در اینجا نسبتاً کم است، و احتمالاً نمی خواهید متد کامل در کلاس خود داشته باشید که یک مورد خاص از اعتبارسنجی یک کاربر را مدیریت کند. اما اگر کد اعتبار سنجی را در یک تابع محلی(local function) قرار دهید، می توانید از شر تکرار خلاص شوید و همچنان ساختار کد واضحی را حفظ کنید. در اینجا نحوه عملکرد آن آمده است.
class User(val id: Int, val name: String, val address: String) fun saveUser(user: User) { fun validate(user: User, value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${user.id}: empty $fieldName") } } validate(user, user.name, "Name") validate(user, user.address, "Address") // Save user to the database }
این بهتر به نظر می رسد. منطق اعتبارسنجی تکراری نیست، و در صورت نیاز به افزودن فیلدهای دیگر به کاربر در حین تکامل پروژه، می توانید به راحتی اعتبارسنجی های بیشتری اضافه کنید. اما ارسال شی User به تابع اعتبارسنجی تا حدودی زشت است. خبر خوب این است که کاملاً غیر ضروری است، زیرا توابع محلی به تمام پارامترها و متغیرهای تابع اصلی که در داخل آن است دسترسی دارند. بیایید از آن استفاده کنیم و از شر پارامتر کاربر اضافی خلاص شویم.
class User(val id: Int, val name: String, val address: String) fun saveUser(user: User) { fun validate(value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user ${user.id}: " + "empty $fieldName") } } validate(user.name, "Name") validate(user.address, "Address") // Save user to the database }
class User(val id: Int, val name: String, val address: String) fun User.validateBeforeSave() { fun validate(value: String, fieldName: String) { if (value.isEmpty()) { throw IllegalArgumentException( "Can't save user $id: empty $fieldName") } } validate(name, "Name") validate(address, "Address") } fun saveUser(user: User) { user.validateBeforeSave() // Save user to the database }
استخراج یک قطعه کد در یک تابع افزونه(extension function) به طرز شگفت انگیزی مفید است. حتی اگر User بخشی از پایگاه کد شما است و نه یک کلاس کتابخانه، شما نمی خواهید این منطق را در یک متد User قرار دهید، زیرا به مکان های دیگری که User در آن استفاده می شود مربوط نیست. اگر از این رویکرد پیروی کنید، API کلاس فقط شامل متدهای ضروری است که در همه جا استفاده می شوند، بنابراین کلاس کوچک می ماند و به راحتی می توانید سرتان را در اطراف کلاس بچرخانید. از سوی دیگر، توابعی که عمدتاً با یک شی سر و کار دارند و نیازی به دسترسی به داده های خصوصی آن ندارند، می توانند مانند فهرست 3.14 بدون شرایط اضافی به اعضای آن دسترسی داشته باشند.
توابع برنامه افزودنی(Extension functions) همچنین می توانند به عنوان توابع محلی اعلام شوند، بنابراین می توانید حتی فراتر رفته و User.validateBeforeSave را به عنوان یک تابع محلی در saveUser قرار دهید. اما معمولاً خواندن توابع محلی عمیق تو در تو نسبتاً سخت است. بنابراین، به عنوان یک قاعده کلی، استفاده از بیش از یک سطح تودرتو را توصیه نمی کنیم.
با نگاهی به تمام کارهای جالبی که می توانید با توابع انجام دهید، در فصل بعدی به کارهایی که می توانید با کلاس ها انجام دهید، خواهیم پرداخت.