C# 9.0: Records - کار با داده‌های تغییر ناپذیر کلاس‌ها

در نوشته‌های قبلی در مورد ویژگی‌های مختلف C# ۹.۰ یاد گرفتیم:

در این نوشته، بیایید به یکی دیگر از ویژگی‌های بسیار جالب C# ۹.۰ که انواع record یا records نامیده می‌شود نگاه کنیم.​

کار کردن با داده‌های تغییر ناپذیر بسیار قدرتمند است، اغلب منجر به باگ‌های کمتری می‌شود، و شما را مجبور می‌کند تا اشیا را تبدیل به اشیای جدید کنید به جای اینکه اشیای موجود را اصلاح کنید. توسعه دهندگان #F به این عادت کرده اند، زیرا #F همه چیز را به طور پیش‌فرض به عنوان تغییرناپذیر در نظر می‌گیرد. حالا شما انواع تغییرناپذیر را در C# ۹.۰ نیز دارید، یا به اصطلاح انواع record، یا فقط records نامیده می‌شود. Records کار با داده‌های تغییرناپذیر در #C را برای شما آسان‌تر می‌کند. قبل از این که نگاهی به records در این نوشته بیاندازیم...

بیایید با یک کلاس شروع کنیم

در نوشته قبلی شما در مورد ویژگی‌های init-only در C# ۹ یاد گرفتید. من کلاس Firend را با دو خصوصیت FirstName و LastName با ویژگی init-only ایجاد کردم. اگر نمی‌دانید ویژگی init-only چیست، لطفا نوشته قبلی را بخوانید:

با ویژگی‌های init-only، شما خصوصیت‌های تغییرناپذیر را به دست می‌آورید. در کلاس Firend در تکه کد بالا، تمام ویژگی‌های آن init-only و بنابراین تغییرناپذیر هستند، شما نمی‌توانید آن‌ها را تغییر دهید. این بدان معناست که هنگام کار با این کلاس، شما مقدار خصوصیت را نمی‌توانید تغییر دهید. ​اگر لازم است چیزی را تغییر دهید، با داده‌های به روز شده یک شی جدید Friend ایجاد می‌کنید. این کاری است که شما هنگام کار با داده‌های تغییر ناپذیر انجام می‌دهید. به جای تغییر یک شی در طول زمان، شما یک شی جدید را در زمان نیاز به تغییر ایجاد می‌کنید. این به این معنی است که شی شما نشان‌دهنده وضعیت داده‌ها در یک نقطه زمانی خاص است.

اجازه دهید مثالی از نحوه انجام این کار برایتان بزنم. بیایید یک شی Friend مانند زیر ایجاد کنیم:

حالا بیایید فرض کنیم که در نقطه‌ای از برنامه شما نیاز دارید که lastname شی friend را به Mueller تغییر دهید، که یک نام بسیار رایج در آلمان است (‏شما ممکن است گلزنان مشهور فوتبال، Gerd Mueller و Thomas Mueller را بشناسید)‏. همانطور که با داده‌های تغییرناپذیر کار می‌کنید، نمی‌توانید خصوصیت LastName را تغییر دهید. به جای انجام این کار، شما یک شی Friend جدید ایجاد می‌کنید که نشان‌دهنده وضعیت جدید است. شما ممکن است آن شی Friend جدید را مانند تکه کد زیر بسازید. توجه داشته باشید که من چگونه مقدار خصوصیت FirstName را از شی Firend اول به مقدار خصوصیت FirstName شی Friend دوم نسبت می‌دهم:

اما زمانی که شما ویژگی‌های بیشتری داشته باشید این رویکرد آزاردهنده است. بیایید یک خصوصیت Middlename را به کلاس Friend اضافه کنیم:

حالا شما در تکه کد زیر می‌بینید که ایجاد شی Firend دوم با lastname جدید نیز یک خط کد اضافی برای آن خصوصیت Middlename جدید در پی دارد. به این دلیل است که شما باید این خصوصیت را از شی Firend قدیمی نیز کپی کنید:

این بدان معناست که هرچه خصوصیات شما بیشتر باشد، این مسئله سخت‌تر می‌شود. البته شما می‌توانید برخی از منطق کپی با reflection یا serialization پیاده سازی کنید، همچنین می‌توانید از کتابخانه auto-mapping نیز استفاده کنید. اما C# ۹.۰ راه بهتری برای کار با کلاس‌های داده تغییرناپذیر دارد: Records.

اولین Record خود را ایجاد کنید

برای تغییر کلاس Friend به یک record، شما از کلمه‌کلیدی record به جای کلمه‌کلیدی class استفاده می‌کنید. در زیر شما نوع متناظر را به عنوان یک نوع record می‌بینید:

ایجاد کپی‌هایی با عبارت With

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

عبارت with به شما این امکان را می‌دهد که یک شی جدید را با کارایی بیشتری ایجاد کنید. شما آن را در عمل در تکه کد زیر می‌بینید، و آن کد منجر به همان نتیجه‌ای می‌شود که در تکه کد بالا می‌بینید. آخرین دستور در تکه کد زیر از عبارت with برای ایجاد یک شی Friend جدید از شی Friend موجود ذخیره شده در متغیر friend استفاده می‌کند. شما می‌توانید این دستور را اینطور بخوانید: از مقدار خصوصیت‌های شی Friend موجود ذخیره شده در متغیر friend برای ایجاد شی Friend جدید، و تنظیم خصوصیت LastName شی Friend جدید به Mueller استفاده کنید. شی Friend جدیدی که توسط عبارت with با مقدار خصوصیت‌های نشان‌داده‌شده در commentها، متغیر newFriend تولید می‌شود، ذخیره کنید.​

همانطور که در تکه کد بالا می‌بینید، عبارت with از یک ترکیب جالب در آغازگر شی برای ایجاد مقادیر جدید خصوصیت‌های مشخص استفاده کرده است. به این معنی است که اگر با آغازگر شی‌ها آشنا باشید، به سرعت با این ترکیب جدید سرعت خواهید گرفت. اما به یاد داشته باشید، عبارت with تنها با انواع record کار می‌کند و نه با کلاس‌های معمولی. ​

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

بررسی کنید که Recordهایتان برابر هستند یا خیر

انواع recordها از reference type هستند و value type مانند structها نیستند. اما متد Equals آن‌ها به طوری اجرا می‌شود که تمام مقادیر خصوصیت‌ها را برای برابری مقایسه می‌کند. در واقع کامپایلر #C، متد Equals را برای شما ایجاد می‌کند.​ و کامپایلر همچنین overloadهایی برای اپراتورهای == و =! را ایجاد می‌کنید، بنابراین این اپراتورها از متد Equal استفاده می‌کنند. این ویژگی دیگری از انواع record است. این بدان معنی است که شما می‌توانید دو record را با ارزش خصوصیت‌های آن‌ها برای برابری مقایسه کنید. کد در زیر این را در عمل نشان می‌دهد. ابتدا یک شی Friend ایجاد شده و در متغیر friend ذخیره می‌شود. سپس عبارت with برای ایجاد شی Friend دیگر از شی firend موجود استفاده می‌کند. مقدار خصوصیت LastName شی Friend جدید، Mueller قرار داده شده ‌است. سپس دو شی Friend با عملگر == مقایسه می‌شوند. نتیجه false است چون خصوصیت LastName شی جدید Huber ،Friend نیست و مقدار آن Mueller می‌باشد.

بعد از دستور Console.WriteLine شی Friendسوم از شی newFriend ایجاد می‌شود. از عبارت with برای تنظیم مقدار خصوصیت LastName به Huber استفاده می‌شود؛ و شی Friend ایجاد شده در متغیر anotherFriend ذخیره می‌شود. این بدان معنی است که شی ذخیره‌شده در متغیر anotherFriend مقادیر خصوصیت‌ها، مشابه با اولین شی Friend‌ای است که در متغیر friend ذخیره شده است. سپس آن شی Friendاول با شی Friendسوم ذخیره‌شده در متغیر anotherFriend با عملگر == مقایسه می‌شود. نتیجه دوباره در کنسول با دستور Console.WriteLine نوشته می‌شود. در این حالت، نتیجه true است، زیرا خصوصیت‌های دو شی Friend شامل مقادیر یکسانی هستند.

بررسی برابری یکی دیگر از ویژگی‌های قدرتمند انواع record است. با فراخوانی متد Equals یا با استفاده از عملگر == تمامی مقادیر خصوصیت‌ها را مقایسه می‌کنیم. در واقع، نوع IEquality<T> ،record را پیاده‌سازی می‌کند، در مورد نوع IEquality<Friend> ،Friend را پیاده‌سازی می‌کند.

کامپایلر چه چیزی تولید می‌کند؟

زمانی که فایل dll. را در برنامه Intermediate Language Disassembler باز کنید (ILDASM.exe - آموزش)، می‌توانید نوع Friend را بررسی کنید تا همه چیزهایی را که کامپایلر #C تولید کرده ببینید. در تصویر زیر می‌توانید نوع رکورد Friend را در Intermediate Language Disassembler ببینید. در واقع کامپایلر #C یک کلاس برای نوع رکورد Friend با خصوصیت‌های LastName ،FirstName و MiddleName ایجاد می‌کند. آنچه که در اینجا مشاهده می‌کنید پیاده‌سازی <IEquatable<Friend می‌باشد. چیز‌های دیگری هم می‌توانید ببینید، برای مثال یک کپی از سازنده که یک شی Friend را دریافت می‌کند، در تصویر زیر در بخش ctor : void(class Friend). نمایش داده شده است.

زمانی که بر روی کپی سازنده دو بار کلیک می‌کنید، می‌توانید کد زبان میانی (Intermediate Language Code) را ببینید. کلمه family در خط اول بیانگر این است که سازنده Protected است. سازنده تمام مقدارهای انتقال داده شده به شی Friend را در خصوصیات LastName ،FirstName و MiddleName شی جدید Friend کپی می‌کند.

بنابراین، شما می‌توانید فکر کنید که کپی سازنده چیزی شبیه به این در #C است:

هنگامی که از عبارت with همانند تکه کد زیر استفاده می‌کنید، کپی سازنده برای ایجاد یک کپی جدید متناظر با شی Friend که با عبارت with مشخص می‌کنید، فراخوانی می‌شود. پس از آن خصوصیت LastName با توجه به آغاز کننده شی که با عبارت with مشخص کردید، مقداردهی می‌شود.

در کنار کپی سازنده، کد‌های بیشتری نیز برای انواع record ایجاد می‌شود. آن کد ایجاد شده توسط کامپایلر قسمتی است که تمام جادوها اتفاق می‌افتد. یکی دیگر از ویژگی های مفید تولید شده توسط کامپایلر، متد محافظت شده PrintMembers است. یک StringBuilder را به عنوان یک پارامتر در نظر می‌گیرد و نام و مقدار تمام خصوصیت‌ها را به شی StringBuilder اضافه می‌کند. متد PrintMembers با استفاده از متد بازنویسی شده ToString فراخوانی می‌شود که این هم توسط کامپایلر ایجاد می‌شود. تکه کد زیر، خصوصیت‌های متعلق به شی Friend را در کنسول چاپ می‌کند، Console.WriteLine متد ToString را روی شی Friend فراخوانی می‌کند.

خروجی کنسول مانند زیر است. همانطور که مشاهده می کنید، ابتدا نوع چاپ شده و سپس همه خصوصیت‌ها:

خلاصه

در این نوشته انواع record که با C# 9.0 معرفی شدند را یاد گرفتید. آن‌ها کار با شی داده‌های تغییرناپذیر در #C را به یک لذت تبدیل می‌کنند. برای توسعه دهندگان #F این چیز جدیدی نیست، اما برای توسعه دهندگان #C پیشرفت بزرگی در این زبان است. with یک دستور قوی و زیبا است که تنها برای records در دسترس است، همچنین records موقعیتی عالی برای تولید خصوصیت‌ها و سازندگان/پایان‌دهندگان هستند.



Source: Thomas Claudius Huber - C# 9.0: Records – Work With Immutable Data Classes


مقالات بیشتر در دات نت زوم

https://t.me/DotNetZoom