آموزشِ زبانِ کاتلین (درس ۲: مبانی)

نویسنده : سید ایوب کوکبی ۱۶ مهر ۱۳۹۸
آموزشِ زبانِ کاتلین (درس 2: مبانی)

در این درس با ارکانِ یک برنامه در کاتلین آشنا می‌شوید. دربارۀ متغیرها، توابع و کلاس‌ها توضیحاتی خواهیم داد. با پراپرتی‌ها آشنا می‌شوید. یاد می‌گیرید چگونه از ساختارهایِ کنترلی استفاده کنید و تفاوتِ این ساختارها را با زبانِ جاوا خواهید آموخت. در موردِ ویژگیِ جالبِ Smart Cast صحبت می‌کنیم. یاد می‌گیرید چطور اکسپشن هندلینگ انجام دهید. در انتهایِ این قادر خواهید بود برنامه‌هایِ ساده را در کاتلین بنویسید.

به این برنامۀ سادۀ Hello World توجه کنید:

fun main(args: Array<String>)
{ println("Hello, world!")
}
  • کلمۀ fun یک keyword برای تعریفِ توابع است. برنامه‌نویسی در زبانِ کاتلین همینقدر fun و سرگرم‌کننده است!
  • نوعِ پارامترِ args جلویِ آن نوشته شده که به همین صورت برای متغیرها هم استفاده می‌کنیم. بعداً خواهید دید؛
  • توابع را می‌توانید مستقیماً در داخلِ فایل و بدونِ قرار دادنِ در یک کلاس تعریف کنید؛
  • آرایه‌ها در کاتلین کلاس هستند. برخلافِ جاوا، کاتلین سینتکس ویژه‌ای برای تعریفِ آرایه‌ها ندارد؛
  • دستورِ println نسخۀ کوتاه‌شدۀ System.out.println است. کتابخانۀ استانداردِ کاتلین متشکل از wrapperهایِ فراوانی برای توابعِ استانداردِ کتابخانۀ جاواست. این ویژگی کاتلین را به یک زبانِ کوتاه و consise تبدیل کرده است؛
  • همانطور که می‌بینید در انتهایِ خطوط نقطه‌ویرگول وجود ندارد. بله؛ این علامت در کاتلین اختیاری است. می‌توانید استفاده کنید و می‌توانید استفاده نکنید. هیچ فرقی ندارد.

خب، با ساختار کلی آشنا شدید. اکنون توابع را بیشتر توضیح دهیم.

توابع

در مثالِ Hello World تابع هیچ مقداری برنمی‌گرداند. فقط عبارتِ Hello World را چاپ می‌کند. اما توابع می‌توانند مقدار بازگشتی داشته باشند.

fun max(a: Int, b: Int): Int
{ return if (a > b) a else
b
}
>>> println(max(1, 2))
۲

تعریفِ تابع با کلمۀ کلیدیِ fun آغاز شده است. بعد از آن نامِ تابع که در اینجا max انتخاب شده می‌آید. در ادامه اگر تابع به پارامتری نیاز داشت درونِ پرانتز قرار می‌دهیم. نوعِ برگشتیِ تابع را در انتهایِ پارامترها بعد از علامت دو نقطه قرار می‌دهیم. نهایتاً مقدارِ بازگشتی را مقابلِ return می‌نویسیم.

ساختارِ این تابع را می‌توانید در تصویرِ پایین ببینید. دقت کنید if در کاتلین یک عبارت (expression) با مقدارِ برگشتی است. چیزی شبیهِ شرطِ سه بخشی یا ternary operator در جاوا.

ساختارِ یک تابع در کاتلین

در کاتلین if یک expression است نه statement. تفاوتشان این است که expression دارایِ یک مقدارِ بازگشتی است و می‌توان در expression دیگری استفاده کرد ولی statement مقدارِ بازگشتی ندارد. در جاوا تمامِ ساختارهایِ کنترلی از نوعِ statement هستند؛ اما در کاتلین همۀ ساختارهایِ کنترلی به جز حلقه‌ها expression هستند. امکانِ ترکیبِ ساختارهایِ کنترلی با سایرِ عبارت‌ها به شما اجازه می‌دهد تا الگوهایِ مشترکِ زیادی را در کوتاه‌ترین شکلِ ممکن بنویسید. از سویِ دیگر مقداردهی متغیرها که در جاوا expression است در کاتلین به statement تبدیل شده‌اند.

با توجه به اینکه تابعِ بالا فقط دارایِ یک عبارت است می‌توان آن را ساده‌تر هم نوشت. به این صورت که آکولادها و return را حذف می‌کنیم.

fun max(a: Int, b: Int): Int = if (a > b) a else b

تابعی که درونِ دو آکولاد نوشته می‌شود اصطاحاً دارای یک Block Body است. و اگر مستقیماً یک Expression را برگرداند دارایِ یک expression body است.

ترفند: در IntelliJ IDEA قابلیتی وجود دارد که به راحتی می‌توانید نوعِ یک تابع را به expression body یا block body تبدیل کنید. این قابلیت هنگامِ کدنویسی با یک آیکون در سمتِ چپ (جایِ شماره خط) به شما پیشنهاد می‌شود.

توابعِ دارایِ expression body به وفور در کدهایِ کاتلین وجود دارند. این روش نه‌تنها در فانکشنهای یک خطی بلکه در توابعی با ساختارهایِ پیچیده‌تر نیز قابل استفاده است. مثالهایش را در ادامه خواهید خواهند.

حتی می‌توان با حذفِ نوعِ بازگشتی بازهم تابع را ساده‌تر کرد:

fun max(a: Int, b: Int) = if (a > b) a else b

نوعِ بازگشتی یعنی Int را حذف کرده‌ایم. هر متغیر و عبارتی دارایِ یک نوع است و هر تابعی یک return type دارد. ولی در توابع expression-body کامپایلر با آنالیزِ عبارت می‌تواند نوعِ بازگشتی را تشخیص دهد. به این نوع آنالیز، اصطلاحاً type inference گفته می‌شود.

دقت کنید که حذفِ نوعِ بازگشتی فقط در توابع expression body مجاز است. در توابعِ block body حتماً باید نوعِ بازگشتی و کلمۀ return وجود داشته باشد. توابعِ واقعی معمولاِ بلند هستند و چندین مقدارِ بازگشتی دارند. پس این وظیفۀ توسعه‌دهنده است که نوع و مقدارِ آن را مشخص کند.

متغیرها

در جاوا متغیرها را با مشخص کردنِ نوعشان آغاز می‌کنید؛ ولی در کاتلین اینطور نیست. دلیلش این است که در کاتلین می‌توان نوعِ بسیاری از متغیرها را ننوشت. ابتدا یک کلمۀ کلیدی می‌آید و بعد نام متغیر و در آخر اگر مایل بودید می‌توانید نوعِ متغیر را بعد از علامتِ دو نقطه مشخص کنید. به مثالِ پایین توجه کنید:

val question =
"The Ultimate Question of Life, the Universe, and Everything"
val answer = 42

در این مثال نوعِ متغیرها مشخص نشده ولی همانطور که گفتیم می‌توانید به صورتِ صریح نوعشان را مشخص کنید. البته در انتها.

val answer: Int = 42

همانند توابعِ Expression-body اگر نوعِ متغیر را مشخص نکنید، کامپایلر به صورتِ خودکار با آنالیز عبارت آن را تشخیص می‌دهد. در مثالِ بالا اگر نوعِ متغیر را ننویسیم کامپایلر با بررسی عددِ ۴۲ نوعِ متغیر را Int در نظر می‌گیرد. اگر عدد اعشاری بود نوعِ Double انتخاب می‌شد.

اگر متغیری بدون initialize شدن (مقداردهی اولیه) تعریف شود حتماً باید نوعش را مشخص کنید؛ چون کامپایلر قادر به تشخیصِ نوعِ آن نیست.

val answer: Int
answer = 42

دو کلمۀ کلیدی برای تعریفِ متغیرها وجود دارد که هر یک کاربردی دارد:

  • val (برگرفته از value) – برای تعریفِ متغیرهایِ Immutalbe یا تغییرناپذیر کاربرد دارد. متغیرهایی که با این کلمه تعریف می‌شوند معادلِ همان final در جاوا هستند و بعد از مقداردهی دیگر نمی‌توان مقدارشان را تغییر داد؛
  • var (برگرفته از variable) – برای تعریفِ متغیرهایِ mutable یا تغییرپذیر به کار می‌رود. مقدارِ این نوع متغیرها را می‌توان در هر جایی از برنامه تغییر داد. این نوع متغیرها معادلِ متغیرهایِ معمولی (غیر فاینال) در زبانِ جاوا هستند.

در حالت پیش فرض بهتر است تمامِ متغیرها را با val تعریف کنید و فقط در هنگامِ ضرورت از var استفاده کنید. استفاده از متغیرها، توابع و آبجکت‌هایِ تغییرناپذیر سبک برنامه‌نویسیِ شما را به فانکشنال نزدیک می‌کند. در برنامه‌نویسیِ فانکشنال تاثیر جانبیِ کد کاهش یافته و با خطاهایِ ناشناختۀ کمتری روبرو می‌شوید. در موردِ مزایایِ این نوع برنامه‌نویسی در درسِ قبلی توضیحاتی دادیم. در این مورد بعدها دوباره توضیحات بیشتری خواهیم داد.

متغیر val درست در لحظۀ تعریف باید مقداردهی شود؛ ولی می‌توانید مقدارِ نهایی را بر اساسِ یک شرط به آن اختصاص دهید. البته اگر آن شرط فقط یک نتیجۀ داشته باشد. مثلاً در کدِ پایین، بر اساسِ مقدارِ خروجی تابعِ ()canPerformOperation که یا true است یا False متغیرِ message مقداردهی می‌شود.

val message: String
if (canPerformOperation())
{ message = "Success"
// ... perform the operation
}
else {
message = "Failed"
}

با وجودِ اینکه val به یک مقدارِ immutable اشاره می‌کند ولی آن مقداری که به آن اشاره می‌کند اگر یک شی باشد می‌تواند mutable باشد. مثلاً در کدِ پایین متغیرِ language به صورتِ immutable تعریف شده ولی آرایه‌ای که به آن اشاره شده mutable است یعنی می‌توان عناصری را به آن اضافه یا از آن حذف کرد.

val languages = arrayListOf("Java")
languages.add("Kotlin")

در آینده با جزئیات بیشتری در موردِ اشیاء Mutable و Immutable صحبت می‌کنیم.

در موردِ var هم به این نکته توجه داشته باشید که گرچه مقدارش قابلِ تغییر است ولی نوعش ثابت است. برایِ مثال کدِ زیر کامپایل نمی‌شود:

var answer = 42
answer = "no answer"

دلیلش این است که کامپایلر فقط یکبار و آن هم در زمانِ تعریف نوعِ متغیر را مشخص می‌کند. در این مثال، هنگامِ تعریفِ answer کامپایلر از رویِ مقدارِ ۴۲ نوعش را Int در نظر می‌گیرد. اکنونِ نوعِ متغیر تعیین شده و دیگر نمی‌توان در خط‌هایِ بعد تغییر داد. در خطِ دوم تلاش شده تا مقداری از جنسِ string درونِ متغیری از نوعِ Int ذخیره شود که مسلماً باعثِ خطایِ کامپایل می‌شود.

در کاتلین ویژگیِ جالبی تحتِ عنوانِ string template وجود دارد:

fun main(args: Array<String>) {
val name = if (args.size > 0) args[0] else "Kotlin"
println("Hello, $name!")
}

در اینجا اگر برنامه بدون آرگومان اجرا شود Hello, Kotlin در خروجی چاپ می‌شود و اگر آرگومانی ارسال شود به جایِ Kotlin چاپ می‌شود؛ مثلاً Hello, Bob. اینجا Bob آرگومانِ ارسالی است. ابتدا بر اساسِ اینکه آرگومانی ارسال شده یا نه متغیرِ name مقداردهی می‌شود. سپس در تابعِ println به جایِ روشِ مرسوم در جاوا که رشته و متغیر با عملگر + به هم متصل می‌شد (String Concatenation)، از قابلیتِ جدید String Template در کاتلین استفاده می‌کنیم. این روش ساده‌تر است و انعطافِ بیشتری دارد. کامپایلر در پس زمینه یک StringBuilder می‌سازد و رشته‌ها را به آن اضافه می‌کند. name& یک string template است.

در مواردی که باید علامتِ $ را درونِ رشته قرار دهید و منظورِ شما String template نیست از بک اسلش \ برای escape کردنِ آن استفاده کنید. مثلاً رشتۀ “x$\” به این دلیل که قبل از $ از بک‌اسلش استفاده شده، دیگر به عنوانِ یک string template تفسیر نمی‌شود و به عنوانِ یک کاراکتر در خروجی چاپ می‌شود. خروجیِ این رشته x$ است.

انعطافِ string template بسیار زیاد است. مثلاً می‌توانید مقدارِ یکی از عناصرِ آرایه را به آن اختصاص دهید:

fun main(args: Array<String>)
{ if (args.size > 0) {
println("Hello, ${args[0]}!")
}
}
fun main(args: Array<String>) {
println("Hello, ${if (args.size > 0) args[0] else "someone"}!")
}

کلاس‌ها و پراپرتی‌ها

احتمالاً با مفاهیم شی‌گرایی و کلاس و اشیا آشنا هستید. در کاتلین نیز این مفاهیم وجود دارد منتهی با این تفاوت که با کدهایِ بسیار مختصرتر می‌توانید به هدفی که می‌خواهید برسید. در اینجا یک توضیحِ اولیه دربارۀ سینتکس کلاس‌ها می‌دهیم در درس‌هایِ بعد به صورت مفصل‌تر توضیح خواهیم داد.

بخوانید  پیشنهاد سنیور دولوپرها به برنامه‌نویسان مبتدی اندروید

کلاسِ پایین را که به زبانِ جاوا نوشته شده در نظر بگیرید:

/* Java */
public class Person {
private final String name;
public Person(String name)
{ this.name = name;
}
public String getName()
{ return name;
}
}

در جاوا، سازندۀ کلاس معمولاً حاویِ کدهایِ تکراری است. تعدادی پارامتر به تعدادی فیلد اختصاص می‌یابد و این کار در همۀ کلاس‌ها تکرار می‌شود. در کاتلین این تکرار به صورتِ کامل برداشته شده و شما با حداقل کد می‌توانید فیلدها را به متغیرها اختصاص دهید.

با ابزارِ Java-to-Kotlin converter در IntelliJ IDEA می‌توانید کد بالا را به کاتلین تبدیل کنید. در کمال تعجب خواهید دید که خروجی یک خط بیشتر نیست:

class Person(val name: String)

کلاس‌هایی شبیهِ این که فقط حاوی داده و بدونِ کد هستند معمولاً value object نام دارند. بسیاری از زبان‌ها برای تعریفِ این نوع کلاس‌ها سینتکس ساده‌ای در نظر گرفته‌اند و کاتلین هم یکی از این زبان‌هاست. همانطور که می‌بینید سینتکسِ جمع‌وجور و زیبایی دارد. حتی به Modifier (مودیفایر) Public هم نیازی نیست؛ چون Visibiblity پیش‌فرض در زبانِ کاتلین است.

هدف از کلاس، کپسوله کردن داده‌ها و کدها در یک موجودیتِ واحد است. در جاوا داده‌ها درونِ فیلدهایی ذخیره می‌شود که اغلب private هستند. اگر بخواهید دسترسی به فیلدهایی را به کلاینت بدهید از متدهایِ accessor استفاده می‌کنید (getter و شاید setter.) مثالش را در کلاسِ Person دیدید. متدِ setter می‌تواند حاویِ منطقِ اضافه برای اعتبارسنجی داده‌ها باشد.

در جاوا به ترکیبِ فیلدها و اکسسورها اغلب پراپرتی گفته می‌شود؛ اما در کاتلین پراپرتی‌ها جزء اساسی زبان بوده و کاملاً جایگزین فیلدها و اکسسورها شده است. در کاتلین پراپرتی‌ها همانندِ متغیرها با کلماتِ var و val تعریف می‌شوند. پراپرتی‌هایی که با val تعریف می‌شوند فقط-خواندنی یا read-only هستند. در حالی که پراپرتی‌هایِ var قابل تغییر یا mutable هستند.

class Person(
val name: String,
var isMarried: Boolean
)

پراپرتیِ name از نوعِ read-only است؛ انگار در جاوا فیلدی ساخته‌اید که فقط متدِ getter دارد. پراپرتیِ isMarried از نوعِ خواندنی-نوشتنی است؛ انگار در جاوا یک فیلد به همراهِ متدهایِ getter و setter ساخته‌اید. در حالتِ پیش‌فرض، پیاده‌سازیِ getter و setter غیرضروری است؛ چون اغلب این متدها برای مقداردهی و دسترسی به فیلدها ساخته می‌شوند. این کار فقط زمانی ضروری است که قصد داشته باشید کاری به جز ذخیره و بازیابیِ مقدارِ پراپرتی‌ها انجام دهید.

در کلاسِ Person بسیاری از جزئیاتِ پیاده‌سازی مخفی شده است. این کلاس درواقع حاویِ یک فیلدِ خصوصیِ name است که توسط سازندۀ کلاس مقداردهی شده و از طریقِ متدِ getter می‌توانید به محتوایِ آن دسترسی پیدا کنید. جالب اینجاست که شما به روشِ رایج در جاوا می‌توانید به اعضایِ این کلاس دسترسی پیدا کنید:

/* Java */
>>> Person person = new Person("Bob", true);
>>> System.out.println(person.getName());
Bob
>>> System.out.println(person.isMarried());
true

اگر کدِ بالا را به کاتلین تبدیل کنیم، نتیجۀ زیر را خواهیم داشت:

>>> val person = Person("Bob", true)
>>> println(person.name)
Bob
>>> println(person.isMarried)
true

در خطِ اول بدونِ هیچ کلمۀ کلیدی، سازندۀ کلاس فراخوانی شده است. در خط دومِ مستقیماً پراپرتی را صدا زده‌اید ولی در اصل متدِ getter آن را فراخوانی کرده‌اید.

همانطور که می‌بینید، منطق، همان منطقِ قبلی در جاوا است ولی به جای اینکه از متدهای getter و setter برای مقداردهی و دریافتِ مقادیرِ فیلدها استفاده کنید، مستقیماً پراپرتی را صدا زده‌اید. در این حالت، کد، بسیار خلاصه و زیبا می‌شود. برای استفاده از setter به جای (person.setMarried(false از person.isMarried = false استفاده می‌شود.

ترفند: برایِ دسترسی به کلاس‌هایِ جاوا می‌توانید از سینتکس کاتلین استفاده کنید. getter های جاوا را می‌توانید به عنوانِ یک پراپرتیِ val و getter/setter ها را به عنوانِ یک پراپرتیِ var در نظر بگیرید. برایِ مثال اگر کلاسِ جاوا متدی تحتِ عنوانِ ()getName و ()setName داشته باشد می‌توانید با پراپرتیِ name به آن دسترسی داشته باشید. اگر متدهایِ ()isMarried و ()setMarried وجود داشت در کاتلین معادلِ پراپرتی isMarried خواهد بود.

در بیشترِ موارد، پراپرتی دارایِ یک backing field برای ذخیرۀ داده‌هاست. ولی اگر مقدارِ این متغیرها بر اساسِ منطقِ سفارشی – مثلاً از طریقِ پراپرتی‌هایِ دیگر – محاسبه می‌شود می‌توانید از اکسسورهایِ سفارشی (Custom Accessor) استفاده کنید. فرض کنید کلاسِ Rectangle (مستطیل) باید به شما بگوید که مربع است یا مستطیل. برای این کار نیازی به ذخیرۀ داده‌ها در یک فیلدِ مجزا نیست. به راحتی می‌توانید با چک کردنِ برابریِ دو مقدارِ طول و عرض بفهمید که مستطیل است یا مربع.

class Rectangle(val height: Int, val width: Int)
{ val isSquare: Boolean
get() {
return height == width
}
}

پراپرتیِ isSquare نیازی به یک فیلدِ مجزا ندارد. مقدارِ این پراپرتی در زمانِ دسترسی توسطِ یک getter سفارشی محاسبه می‌شود. در مثالِ بالا می‌توانید آکولادها را نگذارید و بنویسید:
get() = height == width

در درس‌هایِ بعد مثال‌هایِ بیشتری از کار با کلاس‌ها و پراپرتی‌ها بیان خواهیم کرد. با این حال اگر عجله دارید می‌توانید با ابزارِ Java-to-Kotlin converter کدهایِ جاوا را به کاتلین تبدیل کرده و تفاوت‌ها را مشاهده کنید.

چیدمانِ فولدرها و پکیج‌ها

همانطور که می‌دانید در جاوا تمامِ کلاس‌ها درون پکیج‌ها سازماندهی می‌شوند. در کاتلین نیز مفهومِ پکیج به قوتِ خود باقی است؛ اما با یک سری تفاوت‌ها. در ابتدایِ هر فایلِ کاتلین می‌توانید نامِ پکیج را بنویسید. تمامِ تعاریف شاملِ کلاس‌ها، توابع و پراپرتی‌ها داخلِ این پکیج قرار می‌گیرند. سایرِ فایل‌ها مادامی که از یک پکیجِ یکسان استفاده کنند به عناصرِ آن پکیج دسترسی دارند. مثل این است که تمامِ فایل‌ها با یکدیگر ترکیب کرده باشید. اگر تعاریف در پکیجِ دیگری باشند باید با کلمۀ کلیدیِ import وارد کنید.

package geometry.shapes
import java.util.Random
class Rectangle(val height: Int, val width: Int)
{ val isSquare: Boolean
get() = height == width
}
fun createRandomRectangle(): Rectangle
{ val random = Random()
return Rectangle(random.nextInt(), random.nextInt())
}

در سطرِ اول نام پکیج قرار گرفته و در سطرِ دوم یکی از کتابخانه‌هایِ استانداردِ جاوا وارد شده است. در کاتلین تفاوتی بینِ ایمپورت کلاس‌ها و متدها وجود ندارد. می‌توانید یک تابع را مثلِ یک کلاس ایمپورت کنید.

package geometry.example
import geometry.shapes.createRandomRectangle
fun main(args: Array<String>) {
println(createRandomRectangle().isSquare)
}

همچنین امکانِ ایمپورتِ تمامِ تعاریفِ یک پکیج با اضافه کردن پسوند *. به آخرِ نامِ پکیج وجود دارد. توجه داشته باشید که این علامت نه‌تنها تمامِ کلاس‌ها بلکه تمامِ توابعِ top-level را هم شامل می‌شود.

کلاس‌های جاوا باید در فایل‌ها و فولدرهایی مطابقِ ساختارِ سلسله مراتبی پکیج قرار بگیرند. برایِ مثال فرض کنید پکیجی دارید به نامِ shapes که حاویِ چند کلاس است. باید هر یک از کلاس‌ها را در فایلِ جدایی همنامِ آن کلاس قرار دهید. و همۀ این فایل‌ها را در فولدری همنامِ پکیج یعنی shapes قرار دهید.

در کاتلین هیچ اجباری وجود ندارد. شما می‌توانید چند کلاس را درونِ فایل قرار دهید و هر اسمی هم برایش انتخاب کنید. همچنین اجباری برای تعریفِ فولدرها به شکلِ سلسله‌مراتبی نیست. هر طوری می‌توانید فایل‌ها و فولدرها را مدیریت کنید. مثلاً می‌توانید تمامِ محتوایِ پکیجِ geomerty.shapes را داخلِ یک فایلِ shapes.kt بنویسید و این فایل را درونِ فولدرِ geometry قرار دهید، بدونِ اینکه نیاز به ساختِ فولدرِ دیگری به نامِ shapes داشته باشید.

اگرچه نیازی نیست ساختارِ سلسله‌مراتبی پکیج‌ها در کاتلین همسان با ساختارِ تودرتوی فولدرها باشد، ولی بهتر است از همین روش پیروی کنید؛ چون در پروژه‌هایی ترکیبی (جاوا+کاتلین) چنین ساختاری ضروری است. با این حال می‌توانید چند کلاس را داخلِ یک فایل قرار دهید بدون اینکه نگرانِ تداخل‌هایی احتمالی باشید. این کار به‌خصوص برایِ کلاس‌ها کوچک توصیه می‌شود.

enum و when

در این قسمت می‌خواهیم در موردِ ساختار when صحبت کنیم که به نوعی جایگزینِ switch است که البته قدرت و انعطافش بسیار بیشتر از آن است.

کدِ پایین یک enum است که تعدادی رنگ را تعریف می‌کند:

enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

سینتکس کاتلین در موردِ enum کمی طولانی‌تر از جاواست. در جاوا می‌نوشتیم enum ولی اینجا باید بنویسیم enum class. گفتنی است که در کاتلین enum یک soft keyword است. یعنی رزرو شده نیست و صرفاً بر اساسِ جایگاهی که استفاده شده معنی پیدا می‌کند. بنابراین می‌توانید از این کلمه برای نامِ متغیر نیز استفاده کنید (هر چند پیشنهاد نمی‌کنیم.) اما class یک کلمۀ کلیدی است و به جز کاربردِ مشخصی که دارد جای دیگری قابل استفاده نیست. بنابراین نمی‌توان متغیری به نامِ class تعریف کرد.

همانندِ جاوا در کاتلین نیز می‌توانید درونِ enum از متدها و پراپرتی‌ها استفاده کنید.

enum class Color(val r: Int, val g: Int, val b: Int) {

        RED(255, 0, 0), 
        ORANGE(255, 165, 0),            
        YELLOW(255, 255, 0), 
        GREEN(0, 255, 0), 
        BLUE(0, 0, 255),
        INDIGO(75, 0, 130), 
        VIOLET(238, 130, 238);
        
        fun rgb() = (r * 256 + g) * 256 + b        
} 

>>> println(Color.BLUE.rgb())
۲۵۵

در خطِ اول، داخلِ پرانتز، پراپرتی‌هایی تعریف شده که برای مقادیرِ enum به کار می‌رود. مثلاً (RED(255,0,0 آرگومان اول r برای مقدارِ red است که نوعش Int است. آرگومانِ دوم g و بعدی b است. این مقداردهی دقیقاً مثل مقداردهیِ سازندۀ کلاس‌هاست. مقادیرِ درونِ enum چون غیرِ قابلِ تغییر هستند ثابت یا constant نامیده می‌شوند.

در صورتی که بخواهید متدی تعریف کنید حتماً بعد از آخرین ثابت یک سمی کالن قرار دهید. در واقع اینجا تنها جایی در کاتلین هست که استفاده از سمی کالن اجباری است. نقطه ویرگول حائلِ بین ثابت‌ها و متدهایِ enum است. بخشِ جالبِ ماجرا دسترسی به مقادیرِ enum است که در کاتلین بسیار قدرتمند است.

خارجی‌ها رنگ‌هایِ رنگین کمان را با جملۀ Richard Of York Gave Battle In Vain به بچه‌ها یاد می‌دهند. به این صورت که کاراکترِ اولِ هر یک از این کلمات نامِ یک رنگ است. مثلاً R در Richard برای Red و O در Of معادلِ Orange و … . حالا فرض کنید تابعی لازم دارید که برایِ هر رنگی یک نام برای یادآوری پیشنهاد دهد. (شما نمی‌خواهید این اطلاعات را درونِ خودِ enum ذخیره کنید.) در جاوا می‌توانید از کلمۀ switch استفاده کنید. در کاتلین از when استفاده می‌شود.

when نیز همانندِ if یک expression است و مقداری را برمی‌گرداند. در ابتدایِ درس گفتیم که بعداً یک مثال از توابعی که دارایِ چند خط بوده و دارایِ یک expression body هستند می‌زنیم. این همان مثالی است که قول داده بودیم:

fun getMnemonic(color: Color) =
when (color) {
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}
>>> println(getMnemonic(Color.BLUE))
Battle

برخلافِ جاوا در when نیازی به استفاده از break بعد از هر statement نیست (فراموش کردن break همیشه باعثِ باگ‌هایی در کدهایِ جاوا می‌شد.)

بخوانید  بهترین روش نام‌گذاری ریسورسها در اندروید

همچنین می‌توانید چندین مقدار متفاوت را به یک مقدارِ خروجی نسبت دهید. برای این کار مقادیر را با کاما از هم جدا کنید.

fun getWarmth(color: Color) = when(color)
{ Color.RED, Color.ORANGE, Color.YELLOW ->
"warm" Color.GREEN -> "neutral"
Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}
>>> println(getWarmth(Color.ORANGE))
warm

در این مثال از نامِ کاملِ ثابت‌ها استفاده شده است. با ایمپورت کردنِ کلاسِ Color – که در یک پکیج دیگر قرار دارد – می‌توانید با سهولتِ بیشتری به مقادیرِ آن دسترسی پیدا کنید.

import ch02.colors.Color
import ch02.colors.Color.*
fun getWarmth(color: Color) = when(color)
{ RED, ORANGE, YELLOW -> "warm"
GREEN -> "neutral"
BLUE, INDIGO, VIOLET -> "cold"
}

در خطِ اول کلاسِ Color و در خطِ دوم ثابت‌هایِ آن ایمپورت شده است. اکنون می‌توانید تنها با نامِ ثوابت به آن‌ها دسترسی داشته باشید. در مثال‌هایِ بعدی از همین شیوۀ کوتاه برای دسترسی به مقادیر استفاده می‌کنیم. البته برای حفظِ سادگی از تکرار import ها پرهیز می‌کنیم. در کدهایِ واقعی حتماً باید import را بنویسید.

انعطافِ when در کاتلین بسیار بیشتر از switch در جاواست. برخلافِ سوئیچ که برای شرطِ آن فقط می‌توان از ثابت‌هایِ enum، رشته‌ها یا اعداد استفاده کرد when امکان می‌دهد تا از هر آبجکتی استفاده کنید. در مثالِ پایین نتیجۀ مخلوطِ دو رنگ برگردانده می‌شود:

fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color")
}
>>> println(mix(BLUE, YELLOW))
GREEN

دقت کنید آرگومانِ when می‌تواند هر آبجکتی باشد. مثلاً اگر آرگومان‌هایِ اول و دوم یا بالعکس قرمز و زرد باشند خروجی نارنجی است. کتابخانۀ استانداردِ کاتلین تابعی به نامِ setOf دارد که بر اساسِ آرگومان‌ها یک Set می‌سازد. همانطور که می‌دانید set یکی از ساختارهایِ داده‌ای است که مقادیری را بدونِ در نظر گرفتنِ ترتیبِ داده‌ها ذخیره می‌کند. بنابراین فرقی بینِ (setOf(RED, Yellow و (setOf(YELLOW, RED وجود ندراد. در مثالِ ما نیز ترتیبِ رنگ‌ها اهمیتی ندارد. در دستورِ when تمامِ branch ها برایِ برقراریِ شرایط جستجو می‌شود و نهایتاً اگر چیزی پیدا نشد بلاکِ else اجرا می‌شود.

اینکه بتوانید هر شرطی را در when بررسی کنید انعطافِ بسیار زیادی به شما می‌دهد. کارهایِ زیادی را می‌توانید بدونِ نوشتنِ متدهایِ اضافه انجام دهید. در این مثال صرفاً برابری بررسی شد. در مثالِ بعد خواهید دید که هر نوع عملگرِ شرطیِ دیگری را هم می‌توانید استفاده کنید.

شاید تا حدودی متوجهِ ناکارآمدیِ مثال قبل شده باشید. در واقع چندین نمونه از Set ساخته می‌شود و برای چک کردن برابریِ دو رنگ با دو رنگ دیگر استفاده می‌شود. در حالتِ عادی این موضوع مشکل‌ساز نیست ولی اگر این تابع را به دفعاتِ زیادی فراخوانی کنید ممکن است تولید garbage کند و حافظۀ زیادی از سیستم بگیرد. کد بالا را می‌توان با حذفِ آرگومانِ when به صورت زیر بهینه کرد. خواناییِ آن تا حدودی کاهش می‌یابد ولی در عوض پرفرمنسش بالا می‌رود:

fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == RED && c2 == YELLOW) ||
(c1 == YELLOW && c2 == RED) ->
ORANGE
(c1 == YELLOW && c2 == BLUE) ||
(c1 == BLUE && c2 == YELLOW) ->
GREEN
(c1 == BLUE && c2 == VIOLET) ||
(c1 == VIOLET && c2 == BLUE) ->
INDIGO
else -> throw Exception("Dirty color")
}
>>> println(mixOptimized(BLUE, YELLOW))
GREEN

این بار when آرگومان ندارد. در این حالت شرطِ مقایسه هر عملگری می‌تواند باشد. این تابع دقیقاً همان کارِ قبلی را انجام می‌دهد ولی مزیتش این است که اشیاء اضافه تولید نمی‌کند . که البتۀ هزینه‌اش کاهشِ خوانایی است.

Smart Casts

مثالی که در این بخش می‌نویسیم یک محاسبۀ سادۀ ریاضی است مثلاً ۴ + (۲+۱). این عبارت فقط شاملِ عملگر (جمع) است. سایرِ عملگرها (تفریق، ضرب و تقسیم) را خودتان به همین روش می‌توانید انجام دهید.

خب پیش از هر کاری ابتدا باید ساختارِ ذخیره‌سازی را مشخص کنیم. می‌توانیم از یک ساختارِ درختی استفاده کنیم. هر گره می‌تواند یک عدد (Num) یا یک عملِ جمع (Sum) باشد. Num همیشه برگ است ولی Sum گره‌ای متشکل از دو فرزند است که هر یک از این فرزندها خود می‌توانند یک عدد یا عملِ جمع باشد.

این ساختار همینطور می‌تواند ادامه داشته باشد. بنابراین خروجی (۲)Num می‌شود ۲ و خروجی ( (۲)Sum ( Num(3), Num برابر با ۵ است. ممکن است یکی از این آرگومان‌ها خود یک عبارتِ جمعِ دیگر باشد و این ساختار به صورت تو در تو ادامه یابد. چنین ساختاری را می‌توانیم به کمکِ کدهایِ پایین تعریف کنیم.

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

در خطِ اول یک اینترفیس و در دو خطِ بعد به ترتیب کلاس‌هایِ Num و Sum را تعریف کرده‌ایم. دقت کنید Expr یک اینترفیسِ خالی است که اصطلاحاً به آن Marker Interface گفته می‌شود. این نوع اینترفیس هیچ متدی در بدنۀ خود ندارد. در واقع هر کلاسی که این اینترفیس را پیاده می‌کند می‌گوید که من یک Expression هستم. به جایِ Expr هر نامِ دیگری هم می‌توانید انتخاب کنید. اینترفیسِ Serializable در جاوا یک نمونه از Marker Interface است. کلاسِ Num یک عدد را مشخص می‌کند و کلاسِ Sum یک عملِ جمع را تعریف می‌کند که دو آرگومان دارد. هر یکی از این آرگومان‌ها می‌تواند یک Num یا یک Sum باشد.

((Sum(Sum(Num(1), Num(2)), Num(4

خب حالا متدی به نامِ eval تعریف می‌کنیم تا این ساختار را دریافت کرده و نتیجۀ نهایی را چاپ کند. یعنی؛ به این صورت:

>>> eval(Sum(Sum(Num(1), Num(2)), Num(4)))
۷

اینترفیسِ Expr دو پیاده‌سازی دارد. بنابراین ارزیابیِ ساختار دو حالت دارد:

  • اگر عبارتِ ورودی Num باشد مقدارِ درونش را باید برگردانیم؛
  • اگر عبارتِ ورودی Sum باشد باید هر یک از دو بخشِ چپ و راست را محاسبه و با هم جمع کنیم.

ابتدا متدِ eval را به شیوۀ مرسوم در جاوا پیاده‌سازی می‌کنیم و سپس با ریفکتور، نسخۀ کاتلین را خواهیم نوشت. در جاوا معمولاً شرط‌ها با تعدادی if سنجیده می‌شود.

fun eval(e: Expr): Int {
    if (e is Num) {
        val n = e as Num
        return n.value
    }
    if (e is Sum) {
        return eval(e.right) + eval(e.left)
    }
    throw IllegalArgumentException("Unknown expression")
}

>>> println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
۷

در کاتلین برای شناساییِ نوعِ متغیر از عملگرِ is استفاده می‌شود. در زبانِ سی‌شارپ نیز از همین عملگر استفاده می‌شود. در جاوا برایِ این کار باید از instanceof استفاده کنید ولی در جاوا بعد از شناساییِ نوعِ متغیر برای دسترسی به مقدارِ آن حتماً باید تبدیلِ صریح انجام دهید. در کدِ بالا e as Num یک تبدیل صریح است و از آنجایی که ممکن است چند بار از این مقدار استفاده کنیم درونِ یک متغیر (n) ذخیره شده است.

در کاتلین نیازی به تبدیلِ نوع نیست. همینکه که بفهمید متغیری از نوعِ موردِ نظرِ شماست کافی است. مستقیماً می‌توانید به مقدارِ آن دسترسی پیدا کنید. کامپایلر خودش عملِ Cast را انجام می‌دهد. به این تبدیلِ ضمنی که توسطِ کامپایلر انجام می‌شود Smart Cast گفته می‌شود. در مثالِ بالا عمداً شرطِ اول را از تبدیلِ صریح رفته‌ایم و در شرطِ دوم از Smart Cast استفاده کرده‌ایم. همانطور که می‌بینید بعد از اینکه نوعِ e را Sum تشخیص دادیم مستقیماً با نوشتنِ e.left و e.right به پراپرتی‌هایِ آن دسترسی پیدا کرده‌ایم. در IDE به صورتِ خودکار smart cast ها با رنگِ پس‌زمینه مشخص می‌شوند.

به رنگِ پس‌زمینۀ سبز پشتِ e توجه کنید

دقت داشته باشید که smart cast فقط زمانی کار می‌کند که مقدارِ متغیر بعد از عملگرِ is قابلِ تغییر نباشد. در موردِ پراپرتی‌هایِ کلاس چون val و غیرِ قابلِ تغییر هستند مشکلی وجود ندارد. حالا همین کد را با زبانِ کاتلین بهینه‌سازی می‌کنیم.

قبلاً در موردِ تفاوتِ if در کاتلین و جاوا گفتیم. فرقشان این بود که if در کاتلین یک expression است و خروجیِ آن یک مقدار است؛ بنابراین نیازی به return برای بازگرداندن مقادیر نیست. این یعنی می‌توانیم تابعِ eval را به صورتِ expression-body بنویسیم و نتیجۀ if را به آن اختصاص دهیم.

fun eval(e: Expr): Int =
    if (e is Num)
        e.value
    else if (e is Sum)
        eval(e.right) + eval(e.left)
    else
        throw IllegalArgumentException("Unknown expression")

آکولادها چون فقط یک عبارت وجود دارد حذف کرده‌ایم. کد را باز هم می‌توان ساده‌تر نوشت. به جایِ if می‌توانیم از when استفاده کنیم. این موضوع را IntelliJ هم تشخیص داده و به شما پیشنهاد می‌دهد.

fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.right) + eval(e.left)
        else -> throw IllegalArgumentException("Unknown expression")
    }

در موردِ when می‌دانید که فقط برایِ سنجشِ برابری نیست. همانطور که می‌بینید در کدِ بالا از smart cast استفاده شده است. زمانی که منطقِ هر شاخه از when پیچیده می‌شود می‌توانید از بلاک‌ها استفاده کنید که با دو آکولاد مشخص می‌شود.

fun evalWithLogging(e: Expr): Int =
    when (e) {
        is Num -> {
            println("num: ${e.value}")
            e.value
        }
        is Sum -> {
            val left = evalWithLogging(e.left)
            val right = evalWithLogging(e.right)
            println("sum: $left+$right")
            left + right
        }
        else -> throw java.lang.IllegalArgumentException("Unknown expression")
    }

خروجیِ این تابع برایِ مقادیرِ دلخواه به شکلِ زیر است. دقت کنید حتماً باید شاخۀ else وجود داشته باشد تا اگر هیچ یک از شرط‌ها برقرار نبود else اجرا شود.

>>> println(evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))))
num: 1
num: 2
sum: 1 + 2
num: 4
sum: 3 + 4
۷

حلقه‌هایِ while و for

در بینِ تمامِ ویژگی‌هایِ زبانِ کاتلین while و do..while بیشترین شباهت را به جاوا دارند.

while (condition) {
/*...*/
}
do {
/*...*/
} while (condition)

اجرایِ while تا زمانی که یک شرط برقرار باشد تکرار می‌شود. do…while هم به همین صورت. while شرط را ابتدا چک می‌کند و اگر برقرار نباشد چیزی اجرا نمی‌شود ولی do…while شرط را در انتها بررسی می‌کند. یعنی حداقل یکبار اجرا می‌شود. در موردِ این دو حلقه چیزِ جدیدی در کاتلین وجود ندارد. بنابراین به همین توضیح اکتفا می‌کنیم.

بخوانید  آموزشِ پروژه‌محور برنامه‌نویسیِ اندروید (درس 2: ساختِ اولین برنامه)

حلقۀ for

در کاتلین معادلِ مرسومی برایِ for وجود ندارد ولی می‌توانید با استفاده از ranges به چیزی بیشتر از آن دست یابید.

یک رنج به صورتِ ساده فاصلۀ بین دو مقدارِ (معمولاً عددی) را مشخص می‌کند. مثلا:

val oneToTen = 1..10

دقت کنید که range در کاتلین بسته است یعنی؛ مقدارِ آخر نیز جزئی از رنج است.

رایج‌ترین کاری که می‌توان با یک محدودۀ عددی انجام داد iterate کردن بین مقادیرِ آن رنج است. اصطلاحاً؛ progression. مثلاً برای بازیِ Fizz-Buzz می‌توانیم به خوبی از range استفاده کنیم.

این بازی به طرقِ مختلفی انجام می‌گیرد. ما کاری به شیوۀ انجام آن نداریم ولی الگوریتمش به این صورت است که اعداد به ترتیب در یک رنجِ مشخص وارد می‌شوند. اگر عددی بر ۳ قابلِ تقسیم بود خروجی Fizz است. اگر بر ۵ قابلِ تقسیم باشد خروجی Buzz است و بر ۱۵ خروجی FizzBuzz. در صورتی که هیچ یک از این حالت‌ها برقرار نبود خودِ عدد برگردانده می‌شود. این کد را به راحتی می‌توانید در کاتلین پیاده‌سازی کنید. به when دقت کنید که چطور بدونِ آرگومان استفاده شده است.

fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> " FizzBuzz "
    i % 3 == 0 -> " Fizz "
    i % 5 == 0 -> " Buzz "
    else -> " $i "
}

fun main() {
    for (i in 1..100){
        print(fizzBuzz(i))
    }
}

//output: 1  2  Fizz  4  Buzz  Fizz  7  8  Fizz  Buzz  11  Fizz  13  14  FizzBuzz ...
 

می‌توانیم کمی مثال را پیچیده‌تر کنیم. مثلاً به صورت بالعکس از ۱۰۰ تا ۱ حرکت کنیم و هر بار دوتا دوتا جلو برویم. یعنی ۱۰۰ بعد ۹۸ بعد ۹۶ و … . در این حالت رنجِ بالا را باید به صورتِ زیر تغییر دهید:

 for (i in 100 downTo 1 step 2) {
... print(fizzBuzz(i))
... }
//output: Buzz 98 Fizz 94 92 FizzBuzz 88 …

پیش‌تر گفتیم که عملگر .. طیفی از مقادیر تولید می‌کند و همواره مقدارِ آخر را در نظر می‌گیرد. در مواردِ زیادی، به حساب نیاوردنِ مقدارِ پایانی مفید است. برای مثال در یک حلقه که از صفر شروع به شمارش می‌کند و جلو می‌رود معمولاً مقدارِ پایانی را به شمار نمی‌آورند. برایِ این کار از عملگرِ until استفاده می‌شود. برای مثال؛ (for (x in 0 until size معادلِ (for (x in 0..size-1 است.

گفتیم که بیشترین استفاده از for..in پیمایشِ اعضایِ یک کالکشن است. کاربردِ این حالت مانند جاوا است. بنابراین زیاد در این مورد صحبت نمی‌کنیم. به جایش پیمایش در یک map را توضیح می‌دهیم که کاربردهایِ زیادی دارد.

برایٍ مثال برنامه‌ای در نظر بگیرید که معادلِ باینری هر کاراکتر را چاپ می‌کند. برایِ اینکه کارمان جلو برود از روشِ ناکارآمد ذخیرۀ معادلِ عددیِ هر کاراکتر در یک map استفاده می‌کنیم.

import java.util.*

var binaryReps=TreeMap<Char, String>()

fun main(){

    for (c in 'A'..'F') {
        val binary = Integer.toBinaryString(c.toInt())
        binaryReps = binary
    }

    for ((letter, binary) in binaryReps)
        println("$letter=$binary")
}

/*output:
A=1000001
B=1000010
C=1000011
D=1000100
E=1000101
F=1000110 
 */

مشاهده می‌کنید که سینتکسِ به کار رفته در حلقۀ اول نه‌تنها برای اعداد که برای حروف هم کار می‌کند. برنامه از کاراکتر A شروع می‌کند و تا F جلو می‌رود. در این مثال روش‌هایِ ساده‌تری برای مقداردهی map و دسترسی و چاپِ اعضایِ آن به کار رفته که با کمی دقت متوجه می‌شوید. مثلاً به جایِ فراخوانیِ ()get و ()put به راحتی با [map[key مقادیرِ map خوانده و با map[key] = value مقداردهی می‌شود. یعنی؛ به جایِ اینکه بنویسیم (binaryReps.put(c, binary از binaryReps = binary استفاده کرده‌ایم.

اگر به هر دلیلی نیاز داشتید در حینِ iterate کردنِ اعضایِ کالکشن، به ایندکس هم دسترسی داشته باشید نیازی به ساختِ یک متغیرِ جدا نیست.

fun main(){

    var list = arrayListOf("10","11","1001")

    for ((index, element) in list.withIndex())
        println("$index: $element")
}

/*output:
0: 10
1: 11
2: 1001
*/

در موردِ تابعِ withIndex در درسِ بعدی توضیحاتِ بیشتری خواهیم داد. با عملگرِ in می‌توانید وجود یا عدمِ وجودِ یک مقدار در یک range را هم بررسی کرد. برایِ مثال؛ بررسیِ اینکه یک کاراکتر عضوی از کاراکترهایِ لیست هست یا نه.

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'
>>> println(isLetter('q'))
true
>>> println(isNotDigit('x'))
true

یکی از کاربردهایِ جالبِ عملگرِ in و in! استفاده در عبارتِ when است:

fun recognize(c: Char)=when(c){
    in '0'..'9' -> "It's a digit!"
    in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
    else -> "I don't know..."
}

رنج (range)ها محدود به کاراکتر و اعداد نیستند. هر کلاسی که اینترفیسِ java.lang.Comparable را پیاده‌سازی کرده باشد می‌توان برایِ این منظور به کار برد.

استثناها (Exceptions)

مدیریتِ استثنا (Exception Handling) در کاتلین بسیار شبیهِ جاواست. یک تابع می‌تواند به صورتِ طبیعی پایان پذیرد یا بر اثرِ خطا در میانۀ راه استثنایی صادر کند و نیمه‌تمام رها شود. تابعِ فراخوان (caller) می‌تواند اکسپشن را بگیرد و پردازش کند. در غیرِ این صورت استثناء به ترتیب در stack بالا می‌رود تا در لایه‌هایِ بالاتر مدیریت شود. اگر در بالاترین لایه یعنی لایۀ اپلیکیشن یا همان متدِ main استثناء مدیریت نشود، برنامه کرش و بسته خواهد شد.

مدیریت استثناء در ساده‌ترین حالت بسیار شبیهِ جاواست. مثلاً برای پرتابِ یک اکسپشن اینطور عمل می‌کنیم:

if (percentage !in 0..100) {
   throw IllegalArgumentException(
       "A percentage value must be between 0 and 100: $percentage")
}

برخلافِ جاوا، در کاتلین ساختارِ throw یک expression یا عبارت است و می‌توان از آن به عنوان بخشی از یک عبارتِ دیگر استفاده کرد.

var percentage = if (number in 0..100)
        number
    else
        throw IllegalArgumentException("A percentage value must be between 0 and 100: $number")

همانندِ جاوا برای گرفتنِ استثنا از try, catch و finally استفاده می‌شود. در مثالِ پایین می‌توانید طرزِ انجام این کار را ببینید. خطی از یک فایل خوانده می‌شود و سعی می‌شود به عدد تبدیل شود. برایِ مقادیرِ نامعتبر null برگردانده می‌شود.

fun main(){
    val reader=BufferedReader(StringReader("239"))
    println(readNumber(reader));
}

fun readNumber(reader: BufferedReader):Int?{
    try {
        val line= reader.readLine()
        return Integer.parseInt(line)
    }
    catch (e: NumberFormatException){
        return null
    }
    finally {
        reader.close()
    }
}

/*output:
۲۳۹
 */

مهم‌ترین تفاوتِ این کد با جاوا استفاده نکردن از throws است. برایِ تعریفِ این تابع در جاوا حتماً باید throws IOException را در انتهایش قرار دهید. یعنی به این صورت:

fun readNumber(reader: BufferedReader) throws IOException {
//code
}

دلیلش این است که IOException در جاوا یک checked exception است. این نوع اکسپشن‌ها حتماً باید در زمانِ کامپایل تکلیفشان روشن شود. یا باید با try catch هندل شوند یا مثلِ کدِ بالا throws کنید. چنین چیزی مختصِ جاواست و در سی‌شارپ و خیلی از زبان‌هایِ دیگر وجود ندارد. بعضی از استثناها مثلِ خطای تقسیم بر صفر unchecked exception هستند و نیازی به هندل کردنشان در زمانِ کامپایل نیست. این استثناها تا زمانِ اجرا به تعویق می‌افتد. در زبانِ سی پلاس پلاس همۀ استثناها unchecked هستند و توسعه‌دهنده مجبور به هندل کردنِ هیچی استثنایی نیست.

کاتلین همانند بسیاری از زبان‌هایِ مدرنِ JVM، تفاوتی بین استثناها قائل نمی‌شود. بنابراین اجباری به throw کردن تابع یا هندلِ کردن آن‌ها با try…catch نیست. اینکه چرا تصمیم گرفته شده در کاتلین طراحی بدین شکل باشد به تجربیاتِ کاربران در جاوا برمی‌گردد. بسیاری از توسعه‌دهنده‌ها معتقدند که جدا کردن اکسپشن‌ها و اجبار به throw یا هندلِ کردنشان ضمنِ تولیدِ کدهایِ اضافه، تضمینِ خوبی برای مقابله با خطاها نیست.

برایِ مثال در کدِ بالا، NumberFormatException یک checked exception نیست؛ بنابراین کامپایلرِ جاوا شما را وادار به هندلِ کردنِ آن نمی‌کند. از طرفی متدِ ()BufferedReader.Close ممکن است استثنایِ IOException را بفرستد که یک checked exception است و باید آن را هندل کنید؛ ولی اینجا در کاتلین نیازی به این کار نیست. در کدِ بالا صرفاً برای آشنایی با try…catch این کار را انجام داده‌ایم.

تفاوتِ دیگر کاتلین و جاوا این است که try در کاتلین یک expression است. قسمتِ finally را از کدِ قبلی حذف می‌کنیم و از روشِ جدید برای کنترلِ استثنا استفاده می‌کنیم.

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        return
    }
    println(number);
}

همانطور که می‌بینید در اینجا try را به شکلِ یک Expression به همان صورت که در if و when استفاده می‌کردیم نوشته‌ایم؛ یعنی try یک خروجی دارد. البته بر خلافِ if حتی اگر بدنۀ try یک خط داشته باشد باید حتماً داخلِ آکولاد قرار بگیرد. اگر بدنۀ try چند عبارت داشته باشد، آخرین مقدار ملاک است. همین موضوع برای قسمتِ clause هم صدق می‌کند.

در کدِ بالا با بروزِ استثناء، return صورت می‌گیرد و بقیۀ کد اجرا نمی‌شود. اگر بخواهید کد ادامه پیدا کند باید مقداری را در آن قرار دهید. مثلاً به جایِ return از null استفاده کنید. در این صورت اگر چیزی غیر از عدد به تابع فرستاده شد مقدارِ null در خروجی نمایش داده می‌شود.

خلاصه

  • کلمۀ کلیدیِ fun برای تعریفِ تابع استفاده می‌شود و کلماتِ val و var به ترتیب برایِ تعریف متغیرهایِ read-only و mutable به کار می‌روند؛
  • String template ها به شما کمک می‌کند تا از شرِ اتصالِ رشته‌ها خلاص شوید. نامِ متغیرها را با $ صدا می‌زنید تا مقدارشان درونِ رشته قرار گیرد؛
  • کلاس‌هایِ داده‌محور را به راحتی می‌توان در کاتلین تعریف کنید؛
  • دستورِ if در کاتلین اکنون یک expression با مقدارِ بازگشتی است؛
  • when جایگزینِ قدرتمندترِ switch در جاوا است؛
  • بعد از چک کردنِ نوعِ متغیرها نیازی به cast کردنِ نوعِ آن‌ها نیست. کامپایلر خودش این کار را انجام می‌دهد؛
  • for, while و do-while بسیار شبیه جاوا است ولی for انعطافِ بیشتری از جاوا دارد. به‌خصوص اگر لازم باشد بینِ اعضایِ یک map یا کالکشن پیمایش کنید؛
  • سینتکسِ سادۀ ۵٫٫۱ رنجی از اعداد ۱,۲,۳,۴,۵ ایجاد می‌کند. از رنج‌ها در for استفادۀ زیادی می‌شود و در کنارِ عملگرِ in می‌توان کارهایِ پیچیده‌ای را با for انجام داد؛
  • مدیریتِ استثناء در کاتلین بسیار شبیهِ جاواست به استثنایِ اینکه کاتلین شما را مجبور نمی‌کند اکسپشنی را از یک متد thrown کنید.

سید ایوب کوکبی

نویسنده و مترجم...

0 دیدگاه

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *