تفاوت data class و normal class در کاتلین


بسم الله الرحمن الرحیم

کلاس داده(data class) تو کاتلین به کلاس ای گفته می شه که وظیفه اصلی اون نگه داشتن داده ها یا اطلاعات است. بنابراین موقعی که قصد ایجاد کلاسی رو دارید که فقط میخواید اطلاعاتی رو داخلش نگه دارید از data class استفاده کنید. ولی قبل اش باید بدونید که زبان برنامه نویسی کاتلین بین کلاس های داده و کلاس معمولی چه تفاوتی قائل می شه.


تفاوت کلاس های داده و کلاس های معمولی

برای اینکه تفاوت این دو نوع کلاس رو بهتون نشون بدم دو کلاس با نام های NormalClassUser(که نقش کلاس معمولی من رو به عهده داره) و DataClassUser(که نقش کلاس داده من رو به عهده داره) ساختم. این دو کلاس دارای دو property یکسان با نام های name و age هستند.

1) نحوه اعلان

اولین تفاوت در نحوه تعریف کلاس داده و کلاس معمولی است. کلاس های معمولی با کلیدواژه class و کلاس های داده با کلیدواژه data class تعریف می شوند. مثال:

class NormalClassUser (val name : String, val age : Int) // کلاس معمولی
- - - - - - -
data class DataClassUser(val name: String, val age: Int) // کلاس داده

2) تابع ()toString

تفاوت بعدی در مقداری است که متد ()toString برای هر یک بر می گرداند. به کد زیر توجه کنید:

fun main() {
    val dataClassUser = DataClassUser(&quothadi&quot, 21)
    val normalClassUser = NormalClassUser(&quothadi&quot, 21)

    println(dataClassUser.toString())
    println(normalClassUser.toString())
}

تو قطعه کد بالا من اومدم دو نوع شی از جنس DataClassUser و NormalClassUser ایجاد کردم و مقادیر hadi و 21 رو تو constructor بهشون پاس دادم. بعدش اومدم متد ()toString رو ابتدا برای شی dataClassUser و بعدش برای شی normalClassUser صدا زدم و خروجی این متد ها رو چاپ کردم. اگر قطعه کد بالا رو اجرا کنیم خروجی ای مشابه زیر رو خواهیم دید:

DataClassUser(name=hadi, age=21)
com.example.myapplication.NormalClassUser@610455d6

همون طور که می بینید متد ()toString وقتی روی یک کلاس داده فراخوانی میشه، به جای اینکه آدرس اون شی رو تو حافظه بهمون بده، مقادیر داخل اون شی رو برامون برمیگردونه. ولی هنگامی که روی یک کلاس معمولی فراخوانی میشه آدرس اون شی رو در حافظه برامون برمیگردونه.

3) تابع ()copy

هنگامی که از کلاس داده استفاده می کنیم، کاتلین به صورت اتوماتیک یک تابع به نام ()copy رو برامون تولید می کنه. کار اصلی این تابع ایجاد یک کپی از شی کلاس داده است(کار مشابه دیزاین پترن Prototype رو انجام میده). برا اینکه متوجه این مطلب بشید به قطعه کد زیر دقت کنید:

fun main() {
    val dataClassUser = DataClassUser(&quothadi&quot, 21)
    val copy = dataClassUser.copy()
    println(copy.toString())
}

تو کد بالا من اومدم و متد copy رو برای شی dataClassUser صدا زدم و نتیجه اش رو داخل متغیر copy ریختم. و بعد مقدار برگشتی حاصل از صدا زدن تابع ()toString روی شی copy رو چاپ کردم. خروجی به صورت زیر است:

DataClassUser(name=hadi, age=21)

همون طور که می بینید مقادیر داخل شی copy دقیقا برابر مقادیر داخل شی dataClassUser هست. لازم به ذکره که این دو شی از همدیگر مستقل هستند و آدرس حافظه متفاوتی دارند. بنابراین تغییرات هر یک روی دیگری تاثیر نخواهد گذاشت.

البته این همه ی کار تابع ()copy نیست. بلکه ما میتونیم هنگامی که این تابع رو صدا می زنیم مقادیری رو نیز روی شی جدید ست کنیم. برای درک این مطلب به قطعه کد زیر توجه کنید:

fun main() {
    val dataClassUser = DataClassUser(&quothadi&quot, 21)
    val copy = dataClassUser.copy(age = 40)
    println(copy.toString())
}

این قطعه کد خروجی زیر رو چاپ می کنه:

DataClassUser(name=hadi, age=40)

همون طور که می بینید مقدار name در شی copy دقیقا برابر مقدار name داخل شی dataClassUser است ولی age تغییر کرده است، چرا که هنگام صدا زدن تابع ()copy خودمان آن را عوض کردیم.

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

4) توابع ()componentX

هنگام استفاده از کلاس های داده، به صورت خودکار به تعداد property هایی که داخل کلاس داده مون داریم توابع ()component1(), component2 و ... تولید میشه. به این صورت که ()component1 مقدار اولین property داخل کلاس داده، ()component2 مقدار دومین property و به همین صورت ()componenX مقدار x امین property کلاس داده رو برمیگردونه. برای درک این مطلب به کد زیر دقت کنید:

fun main() {
    val dataClassUser = DataClassUser(&quothadi&quot, 21)
    println(dataClassUser.component1())
    println(dataClassUser.component2())
}

خروجی کد بالا به صورت زیر خواهد بود:

hadi
21

همون طور که می بینید تابع ()component1 مقدار hadi رو برگردونده و تابع ()component2 مقدار 21 رو که مطابق چیزی هست که بالا تر اشاره کردم.

قابلیت جالبی تو کاتلین هست به اسم Destructuring Declarations که به ما این امکان رو میده که یک کلاس رو به یک یا چند متغیر بشکنیم. اگر برای شی ای توابع ()componentX تولید بشه، می تونیم اون رو به متغیر هایی بشکنیم. به این کد توجه کنید:

fun main() {
    val dataClassUser = DataClassUser(&quothadi&quot, 21)
    val (x, y) = dataClassUser
    println(&quotx: $x y: $y&quot)
}

تو این کد من دو متغیر x و y داخل پرانتر تعریف کردم و مقدار اون ها رو برابر کلاس داده خودم قرار دادم. خروجی کد بالا به صورت زیر است:

x: hadi y: 21

همون طور که می بینید مقدار متغیر x برابر ویژگی name و مقدار y برابر ویژگی y از شی dataClassUser شدند. یعنی مقادیر property های موجود در شی dataClassUser نظیر به نظیر داخل متغیر های x و y ریخته شدند.

مشابه این کار رو اگر با map ها کار کرده باشید احتمالا دیدید. به طور مثال:

fun main() {
    val map = mapOf(&quotname&quot to 21, &quotmehdi&quot to 24)
    for((name, age) in map){
        println(&quotname: $name age: $age&quot)
    }
}

تو قطعه کد بالا ابتدا یک map تعریف کردم که رشته هایی رو به عدد هایی map می کنه. بعد گفتم به ازای هر name و age از داخل این map بیا و name و age رو چاپ کن. همون طور که میدونید هر map مجموعه ای از Map.Entry ها رو داخل خودش نگه می داره. بنابراین name و age باید به مقادیر داخل یک Map.Entry اشاره کنند. نحوه تعریف Map.Entry به صورت زیر است:

public interface Entry<out K, out V> {
    public val key: K
    public val value: V
}

همون طور که می بینید دو property به نام های key و value داره. دلیل اینکه ما تونستیم از قابلیت Destructuring Declarations برای Map.Entry های موجود در map مون استفاده کنیم این بود که توابع componentX برای این interface تولید میشه.

5) تابع ()equals و ()hashCode

تابع ()equals که در کلاس Any وجود دارد، به منظور مقایسه مقادیر داخل دو شی به کار می رود. تو کاتلین اگر دو شی رو به صورت a == b مقایسه کنیم، مقدار حاصل از این مقایسه برابر حاصل عبارت a.equals(b) خواهد بود(کارکرد عملگر == تو کاتلین با جاوا فرق داره). پیاده سازی پیش فرض تابع ()equals چک می کند که آیا دو شی به یک شی اشاره می کنند یا خیر، به عبارت دیگر چک می کند که آیا مکان دو شی در حافظه یکی است یا نه. اگر خواستیم که این پیاده سازی را تصحیح کنیم و مثلا بگوییم که اگر مقادیر داخل دو شی یکی بود، true بشود و در غیر اینصورت false، در کلاس های معمولی مجبوریم که این تابع را override کنیم.

اما در کلاس های داده، کاتلین به صورت خودکار دو تابع ()hashCode و ()equals را پیاده سازی می کند. در پیاده سازی خودکار تابع ()equals، کاتلین تک تک عناصر دو کلاس داده را(اگر از یک جنس بوند) بررسی می کند و اگر همه ی آن ها برابر بودند، مقدار حاصل از فراخوانی تابع equals برابر true خواهد شد. در غیر اینصورت false برگردانده خواهد شد. به کد زیر توجه کنید:

fun main() {
    val dataClassUser1 = DataClassUser(&quothadi&quot, 21)
    val dataClassUser2 = DataClassUser(&quothadi&quot, 21)

    val normalClassUser1 = NormalClassUser(&quothadi&quot, 21)
    val normalClassUser2 = NormalClassUser(&quothadi&quot, 21)

    println(&quotdataClassUser1==dataClassUser2: ${dataClassUser1 == dataClassUser2}&quot)
    println(&quotnormalClassUser1==normalClassUser2: ${normalClassUser1 == normalClassUser2}&quot)
}

نتیجه قطعه کد بالا به صورت زیر خواهد بود:

dataClassUser1==dataClassUser2: true
normalClassUser1==normalClassUser2: false

اگر به کد decompile شده کلاس DataClassUser نگاهی بیندازیم می توانیم پیاده سازی تابع equals را ببینیم.

public boolean equals(@Nullable Object var1) {
   if (this != var1) {
      if (var1 instanceof DataClassUser) {
         DataClassUser var2 = (DataClassUser)var1;
         if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
            return true;
         }
      }

      return false;
   } else {
      return true;
   }
}


سخن آخر

خب اولین مقاله من در ویرگول به پایان رسید. خوشحال میشم اگر این مقاله براتون مفید بود لایک کنید و همچنین اگر انتقاد یا پیشنهادی جهت بالابردن کیفیت مقاله های آتی(انشاءالله) داشتید با من در میون بذارید.