توابع خطی (inline) در کاتلین

توابع خطی

تابع خطی

از توابع خطی برای بهبود کیفیت برنامه و یا تعریف پارامتر های جنریک نوع reified استفاده میشه.

فرایند اجرای کد های کاتلین در jvm

هنگام کامپایل کد های کاتلین ابتدا تبدیل به کد های جاوا شده و سپس با کامپایلر جاوا به کد های خوانا توسط jvm تبدیل میشن.

مراحل اجرای کد های کاتلین در jvm

تعریف تابع خطی

هنگامی که یک تابع مرتبه بالا تعریف میکنیم لامبدا هایی که به عنوان پارامتر تابع تعریف کردیم هرکدوم یک Object هستند که میتونن فضای حافظه رو اشغال کنن.

برای بهبود کیفیت برنامه میتونیم از توابع خطی (inline) در کاتلین استفاده کنیم تا از دست ابجکت هایی که حافظه رو اشغال کردن خلاص بشیم.

به کد زیر دقت کنید

fun foo(block: () -> Unit){ println("Before") block() println("After") }

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

public void foo(Function block){ System.out.println("before"); block.invoke(); System.out.println("after"); }

وقتی بخوایم تابعو صدا بزنیم:

foo(){ println("Do something here") }

به چیزی شبیه زیر تو جاوا تبدیل میشه:

foo(new Function(){ @Override public void invoke(){ System.out.println("Do something here") } });

همینطور که ملاحظه کردید اگه بخوایم تابع foo رو به صورت معمولی ایجاد کنیم یک آبجکت از Function ایجاد میشه که حافظه رو اشغال میکنه.

حالا تابع foo رو با استفاده از کلید واژه ی inline تعریف میکنیم:

inline fun foo(block: () -> Unit){ println("Before") block() println("After") }

وقتی تابعو مثل قبل صدا کنیم کامپایلر کد ها رو مثل زیر از تابع میندازه بیرون:

System.out.println("Before"); block() System.out.println("After");

دیگه خبری از Function نیست و عبارت block به صورت تابع ایجاد شد؛ همین در بهبود کیفیت مخصوصا زمانی که حافظه رو با آبجکت های زیادی اشغال کرده باشیم میتونه کمک کنه.

نکات قابل توجه هنگام تعریف تابع خطی

۱- از تعریف توابع بزرگ (به لحاظ زیاد بودن کد های داخل تابع) باید خودداری کرد.

۲- اگه عبارت لامبدا رو به صورت inline تعریف کنیم فقط میتونیم در توابع خطی صداش بزنیم یا به صورت پارامتر به توابع خطی پاس بدیم.

۳- توابع خطی نمیتونن به توابع و متغیر های private دسترسی داشته باشن، در مورد internal ها باید با ‎@PublishedApi علامتگذاری بشن.

کلیدواژه ی noinline

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

مثال:

inline fun foo(bar: (String) -> Unit, noinline baz: (Int) -> Unit){ //Do something here }

در بالا bar خطی و baz غیر خطی است.

استفاده از return غیر محلی

هنگام تعریف عبارت لامبدا به طور اشکار از return نمیشه استفاده کرد مگه با تعریف برچسب برای عبارت اما اگه لامبدا خطی باشه میتونیم داخل لامبدا مستقیم از return (فقط برای متوقف کردن تابع) استفاده کنیم.

به این return غیر محلی گفته میشه چون تابع بیرون لامبدا هم متوقف میکنه.

مثال:

در زیر از return یک بار به صورت غیر محلی استفاده کردیم و یک بار به صورت محلی با تعریف برچسب op برای operator.

fun main(){ val a = 5.0 val b = 0.0 val operationResult = operateSquareNumbers(a, b) op@{ m , n -> if(n == 0.0){ println("Can't divide by 0") return } return@op m / n } println("Result of Operation on square of $a and $b is $operationResult") } inline fun operateSquareNumbers(a: Double, b: Double, operator: (Double, Double) -> Double): Double { val sqA = a * a val sqB = b * b return operator(sqA, sqB) }

کلیدواژه ی crossinline

بعضی از پارامتر های لامبدایی که برای تابع خطی تعریف میکنیم، ممکنه به صورت مستقیم داخل تابع صدا زده نشن و در یک تابع یا ابجکت داخل تابع خطی صداشون کنیم؛ اما اجازه نداریم از خطی داخل غیر خطی استفاه کنیم چون نمیشه داخل تابع غیر خطی از return غیر محلی استفاده کرد. در اینجا باید پارامتر تابعو با کلیدواژه ی crossinline تعریف کنیم.

مثال:

فرض کنید تابع foo غیر خطی و تابع bar خطی باشه، میخوایم foo رو داخل bar صدا بزنیم و از پارامتر bar داخل foo استفاده کنیم.

fun foo(executor: () -> Unit){ executor() } inline fun bar(crossinline block: (String) -> Unit){ block("From inline function") foo(){ block("From noinline function") } }

مثال:

inline fun foo(crossinline body: () -> Unit) { val bar = object: Runnable { override fun run() = body() } // ... }

در مثال بالا یک object از نوع Runnable داخل foo تعریف کردیم و body رو داخل آبجکت صدا زدیم.

پارامتر های Reified Type

توابعی که پارامتر های نوع جنریکشون reified است باید خطی باشن.

داخل بدنه ی تابع جنریک نمیشه به پارامتر های جنریک (مثلا T) دسترسی داشت چون فقط T هنگام کامپایل وجود داره و زمان اجرا پاک میشه. اما اگه نوع T با reified تعریف شده باشه میتونیم بهش زمان اجرا دسترسی داشته باشیم.

با بیان یک مثال میخوایم بررسی کنیم یک آبجکت نمونه ای از یک کلاس هست یا خیر؟

اگه یک تابع جنریک برای این کار تعریف کنیم، برنامه دچار خطا میشه:

// Error fun <T> Any.instanceOf() = this is T

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

مثال:

inline fun <reified T> Any.instanceOf() = this is T fun main(){ val myInt: Int = 3 println("myInt is String? ${myInt.instanceOf<String>()}") println("myInt is Int? ${myInt.instanceOf<Int>()}")

در بالا یک پارامتر جنریک نوع T با reified تعریف کردیم.

پراپرتی (property) های خطی

کلیدواژه ی inline میتونه برای getter و setter های property که backing field ندارن استفاده بشه.

مثال:

val foo: Foo inline get() = Foo() var bar: Bar inline set(v) { ... }

همچنین میتونیم کل property رو خطی کنیم:

inline val foo: Foo get() = Foo() set(v){...}

به پراپرتی که در مثال دیدید property های خطی در کاتلین میگیم.

خلاصه

- از توابع خطی برای بهبود کیفیت برنامه و تعریف پارامتر reified استفاده میشه.

- هنگام استفاده از کلیدواژه ی inline تابع و پارامتر هاش خطی میشه.

- پارامتر توابع خطی با استفاده از کلیدواژه ی noinline میتونن غیر خطی بشن.

- در لامبدا های خطی میتونیم از return غیر محلی برای متوقف کردن تابع استفاده کنیم

- با استفاده از کلیدواژه ی crossinline میتونیم پارامتر های خطی رو در غیر خطی های داخل تابع صدا بزنیم.

- میتونیم برای تابع خطی جنریک های reified تعریف کنیم.

- میتونیم getter و setter های پراپرتی ها رو به صورت خطی تعریف کنیم یا کل پراپرتی رو خطی کنیم.

arrow_drop_up
کپی شد!