کاتلین : زبان نوپای محبوب

توسط مجتبی بنائی - جمعه 26 مرداد 1397
گروه : تخصصی
برچسب‌ها: #کاتلین #معرفی

کاتلین به عنوان یک زبان برنامه نویسی نوپا که از سال ۲۰۱۰ به بازار عرضه شده است، در چند سال اخیر پله‌های ترقی را به قدری سریع پیموده است که سال میلادی گذشته شاهد آن بودیم که گوگل، رسماً کاتلین را به عنوان زبان رسمی توسعه اندروید معرفی کرد. در این نوشتار به دلایل محبوبیت این زبان و بررسی امکانات متنوع آن و مقایسه موردی برخی ساختارهای آن با جاوا خواهیم پرداخت.

کاتلین و افق روشن آینده <>کاتلین به عنوان طلایه دار تولید برنامه های اندروید و برنامه نویسی چند منظوره (منبع)

قبل از هر چیز باید به این نکته اشاره کنم که انتخاب کاتلین به عنوان زبان رسمی توسعه اندروید، برای بسیاری از حرفه‌ای های این حوزه، بسیار تعجب برانگیز بوده است و تنها دلیل موجهی که برای حرکت گوگل به سمت کاتلین با وجود هزینه‌های زیادی که به گوگل بابت توسعه ابزار و کتابخانه‌ها تحمیل خواهد شد، دعواهای حقوقی بین گوگل و اوراکل (مالک فعلی جاوا بعد از خرید شرکت سان‌مایکروسیستمز) و نیاز گوگل به رهایی از این مباحث و دعواهای حقوقی و ادعاهای خسارتی است که گاه و بیگاه از سمت اوراکل برای استفاده گوگل از اندروید بر روی بستر جاوا مطرح می‌شود (به این مقاله مراجعه کنید). بنابراین تمامی مزایای کاتلین نسبت با جاوا در توسعه اندروید را با این دید بررسی کنید که تمام اینها‌ می‌تواند تنها بهانه باشد و دلیل اصلی حمایت گوگل در ابتدای امر، دلیل دیگری بوده است.

انتخاب کاتلین به عنوان زبان رسمی توسعه اندروید توسط گوگل <>زلف در دست صبا، گوش به فرمان رقیب (منبع)

کاتلین نام جزیره‌ای است در روسیه نزدیک خلیج فنلاند

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

نکته دیگری که درباره این زبان باید بدانید این است که این زبان توسط شرکت معروف جت برینز که مالک و توسعه‌دهنده بهترین محیط‌های برنامه‌نویسی دنیا مانند IntelliJ، Android Studio، PyCharm، WebStorm و PHPStorm است، ایجاد و توسعه داده شده است که این امر باعث می‌شود برخلاف زبانهایی که از دانشگاه‌ها و آزمایشگاه‌های علمی برخاسته‌اند، تناسب بیشتری با نیاز بازار و صنعت برنامه‌نویسی و توسعه نرم‌افزار داشته باشد. آخرین نکته‌ای که اشاره به آن را لازم می‌دانم این است که کاتلین تنها برای توسعه برنامه‌های جاوا یا اندروید، استفاده نمی‌شود و یکی از دلایل محبوبیت آن، خروجی‌های متنوعی است که می‌توان با آن تولید کرد یعنی کد نوشته شده با آنرا در آینده ای نزدیک هم ‌می‌توان به یک برنامه تحت وب تبدیل کرد و هم یک برنامه اندروید و هم یک برنامه IOS که این موضوع هم برای سرمایه‌گذاری دراز مدت بر روی این زبان، پارامتر مهمی محسوب می‌شود.

خروجی های متنوع کاتلین <> تولید انواع برنامه‌ها با کاتلین (منبع) در هر صورت، انتخاب کاتلین برای درس شی‌گرایی، برای بنده از چند جهت حائز اهمیت بود :

  1. زبانی ساده اما در عین حال قدرتمند بر روی بستر جاوا

  2. تولید برنامه‌های اندروید به عنوان خروجی درس (و در آینده نزدیک برنامه‌های موبایل برای اپل)

  3. عادت به کدنویسی منظم با خطاهای متنوع زمان کامپایل

  4. آشنایی با پارادایم برنامه‌نویسی تابعی در کنار شی‌گرایی

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

مزایای کاتلین

  • تعامل کامل با جاوا) : همانطور که اشاره شد، کدهای کاتلین و جاوا به راحتی می‌توانند کنار هم استفاده شوند. این امر باعث می‌شود بتوان برای نوسازی بسیاری از پروژه‌های موجود، در کنار کدهای قدیمی جاوا از کاتلین استفاده کرد و به تدریج، برنامه‌ها را بازنویسی کرد. از طرفی به هزاران کتابخانه موجود جاوا هم در تمامی برنامه‌های کاتلین دسترسی مستقیم خواهیم داشت.
  • گرامر آشنا : با توجه به اینکه زادگاه کاتلین، یک شرکت نرم افزاری بوده است و نه یک آزمایشگاه و تیم تحقیقاتی دانشگاهی، کاملاً با دید صنعتی و با هدف سهولت در عین افزایش کارآیی نوشته شده است. بنابراین به جای خلق گرامر و زبانی کاملاً جدید، از ساختارهای موجود جاوا و اسکالا و مانند آن استفاده شده است که باعث می‌شود برای اکثر برنامه‌نویسان کدهای آن به سادگی قابل درک و استفاده باشد و زمان کمی برای شروع به کار با آن، نیاز باشد. کد زیر که کد یک کلاس در کاتلین است، برای شما بسیار آشنا به نظر خواهد رسید :
    class Foo {
            val b: String = "b"     // val means unmodifiable
            var i: Int = 0          // var means modifiable
            fun hello() {
            val str = "Hello"
            print("$str World")
        }
        fun sum(x: Int, y: Int): Int {
            return x + y
        }
        fun maxOf(a: Float, b: Float) = if (a > b) a else b
      }
  • عملیات درون رشته‌ای : کار با رشته‌ها و جایگذاری متغیرها در آنها برای ایجاد یک خروجی دلخواه و حتی استفاده مستقیم از یک تابع برای تولید خروجی و گنجاندن آن درون رشته، به راحتی در کاتلین میسر شده است. این امر سرعت تولید کد و خوانایی آنرا افزایش می‌دهد. کد زیر خروجی یک عملیات ریاضی را مستقیماً درون رشته نمایش می‌دهد :
    val x = 4
    val y = 7
    _print_("sum of $x and $y is ${x + y}")  // sum of 4 and 7 is 11
  • استنتاج نوع داده : مشابه با بسیاری از زبانهای برنامه‌نویسی مدرن، هر جا که بتوان نوع داده را از روی مقدار قرار گرفته در متغیر به دست آورد (استنتاج) نیاز به ذکر نوع متغیر، نخواهیم داشت. این امر هم سرعت توسعه و کدنویسی را بالا می‌برد.
    val a = "abc"                         // type inferred to String     
    val b = 4                             // type inferred to Int
    val c: Double = 0.7                   // type declared explicitly
    val d: List<String> = ArrayList()     // type declared explicitly
  • تبدیل نوع هوشمند :عملگر is در این زبان، علاوه بر اینکه چک می‌کند که یک متغیر از یک نوع خاص هست یا نه، اگر بتواند به صورت خودکار آنرا به نوع مورد نیاز تبدیل می‌کند و کدهای لازم برای آنرا تولید می‌کند.
    if (obj is String) {
    print(obj.toUpperCase())     // obj is now known to be a String
    }

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

if (obj instanceof  String) {
    System.out.println(((String)obj).toUpperCase())
}

و خود کاتلین متوجه می‌شود که داخل if، نوع obj ، رشته فرض شده است و می‌توان توابع رشته‌ را مستقیماً روی آن به‌کار برد. + عملگر تساوی آنطور که انتظار داریم : در زبان‌های شی‌گرای رایج مانند جاوا و سی شارپ، عملگر == به جای بررسی ساختار و مقادیر برابر در دو متغیر به بررسی تساوی مکان حافظه آنها می‌پردازد یا به عبارتی بررسی می‌کند که دو متغیر دقیقاً یکی هستند یا نه (به یک جای حافظه اشاره می‌کنند). در کاتلین، عملگر == به بررسی تساوی بودن مقادیر دو متغیر می‌پردازد وعملگر === برای بررسی عیناً یکی بودن دو متغیر کنار گذاشته شده است. همین تغییر کوچک، با توجه به رایج بودن استفاده از این عملگر، خوانایی برنامه و حجم آنرا کاهش می‌دهد.

    val john1 = Person("John")
    val john2 = Person("John")
    john1 == john2    // true  (structural equality)
    john1 === john2   // false (referential equality)
  • آرگومان‌های پیش‌فرض : مشابه با سایر زبان‌های نشات گرفته از زبان C این زبان هم امکان تعریف آرگومان‌هایی با مقادیر پیش‌فرض را به ما می‌دهد که باعث می‌شود نیاز به نوشتن توابع مختلف با پارامترهای گوناگون، به حداقل برسد.
fun build(title: String, width: Int = 800, height: Int = 600) {
    Frame(title, width, height)
}
  • آرگومان‌های نامدار :از دیگر امکانات بسیار مفید اضافه شده به کاتلین، نام‌دار کردن آرگومانها و استفاده از این نام، هنگام فراخوانی تابع است . با این روش، می‌توان هر ترکیبی از پارامترها را هنگام فراخوانی تابع داشته باشیم :‌
build("PacMan", 400, 300)                           // equivalent
build(title = "PacMan", width = 400, height = 300)  // equivalent
build(width = 400, height = 300, title = "PacMan")  // equivalent
  • عبارت منعطف When :یکی از تغییرات بسیار پرکاربرد و مفید در این زبان، جایگزینی دستور سوئیچ در جاوا با دستور when است که به کمک آن می‌توان انواع تصمیمات را براساس مقدار یک متغیر اتخاذ نمود. به جای دستور when بهتر است عبارت when به کار رود چون مانند عبارات ریاضی و منطقی، می‌توان آنرا هر جایی که یک متغیر را استفاده می‌کنیم، به کار برد. به مثال زیر توجه کنید :
when (x) {
    1 -> println("x == 1")
    2 -> println("x == 2")
    3,4 -> {
        println("x == 3")
        println("x == 4")
    }
    in 5..10 -> println("x >= 5 and x <= 10")
    is String -> println("x is actually a string")
    else -> {
        println("this is the else block")
    }
}

val res: Boolean = when {
    obj == null -> false
    obj is String -> true
    else -> throw IllegalStateException()
}
  • خصوصیات ساده شده (Properties) : ساده‌سازی دیگری که در کاتلین نسبت به جاوا و سایر زبانهای شی‌گرا ایجاد شده است، این است که برای فیلدها و خصوصیات عمومی یک کلاس، نیاز به تعریف جداگانه setter و getter نداریم و تنها در صورت نیاز، این توابع را می‌توانیم برای هر فیلد کلاس، تعریف کنیم.
class Frame {
    var width: Int = 800
    var height: Int = 600

    val pixels: Int
        get() = width * height
}
  • کلاس‌های داده : برای ساخت یک کلاس داده مثلاً کلاسی که قرار است اطلاعات یک شخص (Person) را در آن ذخیره کنیم، نیاز به ده‌ها خط کد نداریم و تنها با تعریف کلاس از نوع data و دادن فیلدهای اصلی به آن، تمامی توابع مورد نیاز مانند توابع toString(), equals, hashCode و copy به صورت خودکار ساخته خواهند شد.
class User(
        var firstName: String = "",
        var lastName: String = "",
        var email: String = "") 

همین کد در جاوا به صورت زیر باید نوشته می‌شد :

public class User {
    private String firstName;
    private String lastName;
    private String email;
    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;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
}

همانطور که می‌بینید چندین getter و setter برای فیلدهای خصوصی کلاس نوشته شده است که در کاتلین این کار به صورت اتومات انجام می‌گیرد. ویژگی جذاب دیگر کاتلین، نحوه فراخوانی کلاس فوق است که سه حالت مختلف آنرا در زیر مشاهده می‌کنید :

    val user1 = User()
    user1.firstName = "Graham"
    user1.lastName = "Spencer"
    val user2 = User("Graham", "Spencer")
    var user3 = User(lastName = "Spencer", firstName = "Graham")

در فراخوانی سوم، نام فیلدها را به سادگی هنگام ساختن یک متغیر از نوع User در سازنده کلاس آورده‌ایم، امکانی که کدها را خواناتر و ساده‌تر می‌کند.

  • سربازگذاری عملگرها : سربارگذاری یا تغییر نوع رفتار عملگرها هم در این زبان به سادگی قابل انجام است که باعث نوشتن و تولید برنامه‌های با خوانایی بالاتر می‌گردد. البته این قابلیت را بسیاری از زبانهای دیگر هم دارند. در کد زیر عملگر + برای یک کلاس بردار، با یک خط کد، بازنویسی شده و در ادامه دو بردار با هم جمع شده‌اند :
data class Vec(val x: Float, val y: Float) {
    operator fun plus(v: Vec) = Vec(x + v.x, y + v.y)
}

val v = Vec(2f, 3f) + Vec(4f, 1f)
  • سهولت حذف ساختار داده‌ها در صورت نیاز : از دیگر امکانات جذاب کاتلین، تبدیل یک ساختار داده مانند یک کلاس یا یک نگاشت به داده های تشکیل دهنده آن است که در مواردی که قرار است در یک حلقه تکرار، تک تک مقادیر یک شی را بررسی کنیم، می‌تواند بسیار مفید باشد. به مثال زیر توجه کنید که چگونه به راحتی یک ساختار نگاشت یا map به اجزای تشکیل دهنده خود یعنی زوج کلید و مقدار تبدیل شده است :
for ((key, value) in map) {
    print("Key: $key")
    print("Value: $value")
}
  • محدوده‌ها : از دیگرامکانات اضافه شده به این زبان، امکان ایجاد و استفاده از محدوده‌ها است. هر محدوده، بازه‌ای از اعداد یا کاراکترها را نشان می‌دهد. می‌توان عناصر درونی محدوده راهم فیلتر کرد و مثلاً فقط اعداد فرد بین ابتدا و انتهای بازه را تولید کرد . مثال‌های زیر این قابلیت را بدون نیاز به توضیح خاصی، نمایش می‌دهد:
for (i in 1..100) { ... } 
for (i in 0 until 100) { ... }
for (i in 2..10 step 2) { ... } 
for (i in 10 downTo 1) { ... } 
if (x in 1..10) { ... }
  • توابع توسعه‌دهنده : از امکانات بسیار جذاب و به دردبخور کاتلین، امکان افزودن توابع دلخواه به هر کلاس موجود از این زبان است. مثلاً فرض کنید می‌خواهید تابعی بنویسید بر روی کلاس رشته که تمام فضاهای خالی را با کاراکتر - جایگزین کند . تابع replaceSpaces را مستقیماً برای کلاس String می‌نویسیم و در ادامه کد از آن استفاده م‌کنیم :
    fun String.replaceSpaces(): String {
         return this._replace_(' ', '_')
    }
    val formatted = str.replaceSpaces()
    }
  • امنیت مقادیر پوچ (Null Safety) : یکی دیگر از امکانات مهم و کاربردی افزوده شده به کاتلین، مدیریت حرفه‌ای مقادیر پوچ یا هیچ‌مقدار یا همان Null است که منشا اکثر خطاهایی است که می‌تواند در هنگام اجرا پیش بیاید. در کاتلین، یک متغیر در حالت عادی، قابلیت پذیرش هیچ‌مقدارها را ندارد یعنی هیچ‌ناپذیر است و نمی‌توان به آن یک مقدار Null نسبت داد. با این ترتیب، هر جا که کاتلین احساس کند که ممکن است این متغیر مقدار هیچ به خود بگیرد به ما خطا می‌دهد تا نوع آنرا به هیچ‌پذیر تغییر دهیم و اقدامات لازم را برای حالتی که هیچ مقدار وارد می‌شود هم در کد لحاظ کنیم. در کد زیر متغیر a یک متغیر هیچ‌ناپذیر و متغیر b که با علامت ? در انتهای نوع آن مشخص شده است یک متغیر هیچ‌پذیر است :
    var a: String = "abc"
    a = null                // compile error

    var b: String? = "xyz"
    b = null                // no problem

خط زیر به دلیل اینکه ممکن است متغیر b که در بالا هیچ‌پذیر تعریف شده است، خطای کامپایل تولید می‌کند و باید اصلاح شود :

    val x = b.length        // compile error: b might be null

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

    if (b == null) return
    val x = b.length        // no problem

و راه حرفه‌ای تر برای این موضوع، استفاده از عملگر ? است که به کامپایلر اعلام می‌کند این متغیر ممکن است هیچ باشد و حواسمان به این موضوع هست :

    val x = b?.length       // type of x is nullable Int

در کد زیر به کامپایلر گفته‌ایم که متغیر ship ممکن است هیچ باشد یا داخل آن، متغیر کاپیتان ممکن است هیچ باشد و نهایتاً متغیر نام کاپیتان هم ممکن است هیچ باشد و اگر هر کدام از این‌ها هیچ بود، به جای آن، رشته "unknown" را برگرداند و در متغیر name ذخیره کند :

    val name = ship?.captain?.name ?: "unknown"

اگر هم می‌خواهید در صورت هیچ بودن مقادیر متغیرهای سمت راست انتساب، خطای NPE یا همان اشاره‌گر به هیچ را تولید کنید، از هر دو کد زیر می‌توانید استفاده کنید :

    val x = b?.length ?: throw NullPointerException()  // same as below
    val x = b!!.length                                 // same as above
  • توابع لامبدای ارتقا یافته : توابع لامبدا یا توابع ناشناس و بدون نام، امروزه بسیار رواج پیدا کرده‌اند. بخصوص زمانی که قصد انجام پردازش سریعی بر روی داده‌ها داریم و نمی‌خواهیم وقت زیادی صرف نوشتن و صدا زدن تابع کنیم و بهتر است درجا یک تابع ناشناس و بدون نام نوشته و استفاده شود. کاتلین علاوه بر پشتیبانی از توابع لامبدا، باعث بهبودهایی در آنها نسبت به جاوا شده است. در کد زیر یک متغیر sum از نوع تابع تعریف شده است و در خط زیر از آن استفاده شده است :
    val sum = { x: Int, y: Int -> x + y }   // type: (Int, Int) -> Int
    val res = sum(4,7)                      // res == 11

در کد زیر بر روی آرایه numbers سه فیلتر اعمال کرده‌ایم که هرفیلتر خود یک تابع ناشناس (لامبدا) است :

    numbers.filter({ x -> x.isPrime() })
    numbers.filter { x -> x.isPrime() }
    numbers.filter { it.isPrime() }

یا برروی آرایه persons همزمان چهار کار مختلف را از قبیل فیلتر کردن، مرتب سازی، استخراج ایمیل و چاپ آن، انجام داده‌ایم که به مدد ارتقای عملکرد توابع امکان پذیر شده است :

    persons
        .filter { it.age >= 18 }
        .sortedBy { it.name }
        .map { it.email }
        .forEach { print(it) }

منابعی برای مطالعه بیشتر

  1. وبلاگ سبحان عطار در ویرگول که به آموزش این زبان پرداخته است

  2. وبلاگ سجاد یوسف‌نیا در ویرگول که مباحث کمی پیشرفته‌تر کاتلین را مطرح می‌کند

  3. سایت رسمی زبان کاتلین

  4. بخش اجرای زنده دستورات کاتلین در سایت مرجع زبان

  5. کاتلین: چیزهایی هست که باید بدانی

  6. آموزش کاتلین از پایه - tutorialspoint

  7. ده نکته جالب راجع به کاتلین

  8. منابعی بر شروع برنامه نویسی اندروید با کاتلین

  9. معایب کاتلین

  10. کورس‌های رایگان آموزشی کاتلین (انگلیسی)

  11. یادگیری کاتلین با انجام یک پروژه اندروید

  12. کانال mskm.ir در آپارات - آموزش کاتلین

  13. کانال آپارات آرکادمی - آموزش ویدیویی کاتلین

  14. آموزش‌های کاتلین سایت فرانش

  15. آموزش کاربردی کاتلین - انگلیسی اما ساده و عملی

  16. آموزش گام به گام کاتلین - انگلیسی

  17. آموزش کاتلین - سایت tutorialkart

  18. آموزش فارسی کاتلین سایت اسمارت لب

منابع این نوشتار

  1. چرا من جاوا را به خاطر کاتلین کنار گذاشتم

  2. چرا باید کاملا به کاتلین سوئیچ کنیم ؟

نوشته‌های تازه

خلاصه ای از 15 اصل فلسفه زندگی آندره ژید

خلاصه ای از 15 اصل فلسفه زندگی آندره ژید

سه شنبه 03 مهر 1397 توسط مجتبی بنائی / پرواز اندیشه

15 اصل زندگی اصیل در مقاله در ستایش وارستگی مصطفی ملکیان

ادامه مطلب
دین و دغدغه‌های وجودی انسان

دین و دغدغه‌های وجودی انسان

سه شنبه 20 شهریور 1397 توسط مجتبی بنائی / پرواز اندیشه

دین و دغدغه‌های وجودی انسان : تنهایی / مرگ / آزادی / معنای زندگی

ادامه مطلب
به دنبال معنای زندگی

به دنبال معنای زندگی

سه شنبه 20 شهریور 1397 توسط مجتبی بنائی / پرواز اندیشه

زندگی / معنا / اهداف / ارزشها

ادامه مطلب
به دنبال زندگی اصیل

به دنبال زندگی اصیل

سه شنبه 20 شهریور 1397 توسط مجتبی بنائی / پرواز اندیشه

اندر معایب زیستن عاریتی

ادامه مطلب
دانشگاه‌ چه کاری انجام نمی‌دهد؟

دانشگاه‌ چه کاری انجام نمی‌دهد؟

جمعه 02 شهریور 1397 توسط مجتبی بنائی / فرهنگ و جامعه

تاثیر ناچیز دانشگاه در رشد اخلاقی دانشجو

ادامه مطلب
می‌توان با این خدا پرواز کرد

می‌توان با این خدا پرواز کرد

دوشنبه 29 مرداد 1397 توسط مجتبی بنائی / بر بلندای شعر و عرفان

شعر زیبای پیش از این ها فکر می ‌کردم خدا ... قیصر امین‌پور

ادامه مطلب
آیا شما شیعه هستید؟

آیا شما شیعه هستید؟

دوشنبه 29 مرداد 1397 توسط مجتبی بنائی / پرواز اندیشه

پاسخ زیبای امیر بهادری به سوال آیا شما شیعه هستید

ادامه مطلب
کاتلین : زبان نوپای محبوب

کاتلین : زبان نوپای محبوب

جمعه 26 مرداد 1397 توسط مجتبی بنائی / تخصصی

انتخاب کاتلین برای آموزش مفاهیم شی‌گرایی.

ادامه مطلب