اپراتور ها در Swift

قبل از مطالعه بخونید

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

  • نکته اول اینکه اگه بطور کلی با اپراتور آشنایید و دنبال کاربردش در زبان برنامه نویسی سوویفت هستید، پیشنهاد میکنم که از بخش «پیاده سازی اپراتورها در سوویفت» شروع کنین. در غیر این صورت، از همین اول بخونید و در جریان باشید که این مطلب برای همه است، پس در مورد مفاهیم پایه ای هم مجدد بحث کردیم. بررسی این مفاهیم هم کمی طعم زبان Swift رو با خودش داره.
  • نکته دوم اینکه تمام قطعه کد هایی که توی این مقاله هستند، به صورت عکس قرار دادم. ولی در انتها یک فایل Playground براتون گذاشتم که خیلی منظم همه این قطعه کد ها توش وجود داره.

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

اپراتور به چی میگن؟ ?

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

با توجه به توضیحاتی که دادیم، اپراتور ها رو میشه از چند نظر دسته بندی کرد.

از نظر تعداد ورودی

همون طور که گفتیم اپراتور ها حداقل ۱ ورودی دارن. اکثر اپراتور ها ۱ یا ۲ ورودی دارن، یک اپراتور هم هست که ۳ ورودی داره. اگه دقیق تر بخوایم بگیم:

  • یک ورودی یا Unary: اپراتور هایی مثل NOT منطقی، - (منفی) که باعث عکس شدن علامت یک عدد میشه و یا اپراتور های دامنه نیمه باز (Half Open Range) که توی زبان سوویفت تعریف میشن (بطور مثال: ...5)
  • دو ورودی یا Binary: که قطعا معرف حضور هستن، ولی بطور مثال: + که دو عدد رو جمع میزنه، % که باقیمانده تقسیم دو عدد رو حساب میکنه و اپراتور دامنه (Range) که باز هم مخصوص سوویفت هست.
  • ۳ ورودی یا Ternary: توی زبان سوویفت فقط یک اپراتور ۳ تایی تعریف میشه. اون هم :? هست که به نام شرط سه تایی یا Ternary Conditional میشناسنش. یک نمونه اگه ازش بخوایم بگیم:

از نظر محل قرارگیری اپراتور نسبت به ورودی

  • پیشوند یا Prefix: اپراتور هایی که بصورت پیشوندی نوشته میشن فقط به یک ورودی اعمال میشن و پشت اون ورودی نوشته میشن. مثلا، علامت منفی پشت عدد.
  • میانوندی یا Infix: فراگیر ترین تیپ اپراتور ها اپراتور های میانوندی اند که بین دو یا چند ورودی قرار میگیرن. علامت جمع و ضرب و ... میانوندی هستن. شاید بد نباشه بدونید شرط سه تایی هم میانوندی حساب میشه.
  • پسوندی یا Postfix: این گروه هم فقط به یک ورودی اعمال میشن و بعدش نوشته میشن. توی سوویفت بطور مثال، اپراتور دامنه نیمه باز بصورت پسوندی هم تعریف شده. (این اپراتور بصورت پیشوندی هم تعریف میشه. مورد استفادش درواقع دامنه هایی هست که از سمت چپ یا راست به سمت بینهایت یا منفی بینهایت میرن.) علامت های ! و ? هم که به ترتیب برای Force Unwrap و Optional Chaining استفاده میشن هم اپراتور پسوندی به حساب میان.

از نظر تقدم

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

در عبارت بالا اگه به ترتیب اپراتور هارو اعمال کنیم، x برابر با ۲۰ میشه. ام در واقعیت x برابر با ۱۴ میشه، چون ضرب از اولویت بالاتری نسبت به جمع برخورداره. یجوریایی عبارت بالا به شکل زیر توسط سیستم خونده میشه.

درصورتی که علاقه دارید، از این لینک میتونید با این دسته بندی ها آشنا بشین. شاید من هم بعدا در یک مقاله دیگه موردشون صحبت کردم.

از نظر جهت اعمال

یک عبارت ریاضی رو فرض کنید، اگر دو طرف یک عدد، اپراتور یکسان وجود داشته باشه (یا اپراتور هایی با تقدم یکسان)، باید بدونیم که کدوم زودتر اتفاق میفته. برای توضیح این موضوع ،یک اپراتور خیالی تعریف میکنیم با علامت # و اون رو برای هر حالت باز نویسی میکنیم.

همونطور که میبینید عدد ۳ در دو طرفش یک اپراتور یکسان داره، پس باید بدونیم اپراتور # اول روی ۳ و ۴ اعمال میشه یا روی ۳ و ۲.

  • چپ بند (Left-associative): چپ بند یعنی در عبارت بالا، اپراتور سمت چپ ۳ زودتر از اپراتور سمت راستش اعمال میشه. جهت اعمال اپراتور هایی مثل جمع و ضرب و... چپ بند هست.

اگر فرض کنیم # چپ بند هست، نهایتا عبارت ما توسط سیستم به شکل زیر ساده دیده میشه:

  • راست بند (Right-associative): برای این اپراتور ها در شرایطی که گفتیم، اول اپراتور سمت راست اعمال میشه. اپراتور هایی مثل =، =+، شرط سه تایی و ?? (مخصوص سوویفت) راست بند هستند.

حالا اگر فرض کنیم # راست بند هست، داریم:

  • بدون بند (Non-associative): این نوع اپراتور ها به شکلی هستند که اصلا نمیشه ترکیبشون کرد. یعنی برای مثال اپراتور #، اونچه نوشتیم، ارور در زمان کامپایل داره. اما عبارت دوم صحیح هست:

قطعا میپرسید چرا؟ علت اینه که جنس خروجی این ها مثل جنس هیچ کدوم از ورودیاشون نیست. بطور مثال: اپراتور هایی مثل == یا < (کوچکتر/بزرگتر) و اپراتور های منطقی. جنس ورودی این اپراتور ها متنوع هست اما همیشه جنس خروجیشون Bool هست.

الان که دانش دقیقتری نسبت به اپراتور به دست آوردیم وقتشه که از دانشمون در سوویفت استفاده کنیم.??‍?


پیاده سازی اپراتورها در سوویفت

Operator Overloading ?

در همه زبان ها اپراتور هایی وجود داره که برای Type های اون زبان قابل استفادست. در سوویفت-و البته خیلی از زبان های دیگه که ما اینجا کاری باهاشون نداریم-شما میتونید برای Type دلخواه خودتون، توضیح بدید که اون اپراتور ها چجوری کار میکنند.

ساعت رو فرض کنید.

چیزی به نام ساعت در زبان سوویفت، به صورت پیشفرض، تعریف نشده. پس اگر بخواهیم از همچین چیزی رو داشته باشیم باید خودمون تعریفش کنیم. همینطور هم اگه کاربردی بخواد داشته باشه، باز هم باید خودمون تعریفش کنیم. به طور مثال ما نیاز داریم که ۲ تا ساعت رو با هم جمع کنیم. دقت کنید که ساعت ما فقط میتونه ۲۴ ساعت رو نمایش بده، پس اگه ساعتاش بیشتر شه، اصطلاحا لبریز یا Overflow میشه.

به همین سادگی میشه معنی یک اپراتور رو برای Type های جدید به اضافه کرد. و ازین به بعد عبارت زیر برای کامپایلر یک عبارت معنی دار هست.

تعریف اپراتور جدید ?

جذابیت اپراتور ها در سوویفت همینجا تموم نمیشه، شما میتونید اپراتور دلخواهتون رو حتی فقط برای یک شرایط ساده و یک کاربرد تعریف کنید تا کارتون رو راحت‌تر کنه. به عنوان مثال در سوویفت اپراتور ++ وجود نداره، اما شما میتونید اون رو تعریف کنید.

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

به عنوان نمونه، اپراتور ++ که توی زبان هایی مثل C هست، اما در سوویفت نیست رو میشه براش به این شکل تعریف کرد:

اپراتور ++ روی خود ورودی اعمال میشه، و نتیجش برمیگرده.

همون طور که نتایج هم نشون میدن، ترتیب عملیات این هست که اول number بعلاوه ۱ میشه، و بعد عبارت ریاضی محاسبه میشه.

نکته: انتخاب ورودی ++ به صورت inout این کمک رو به ما میکنه که تغییر روی خود ورودی یا همون number هم اعمال بشه. (مطالعه بیشتر)

حتی حرفه ای تر... ?

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

شما بعد از تعریف علامت اپراتور (میانوندی)، باید گروه تقدمش رو مشخص کنید. اگر این کار رو نکنید خود سوویفت به صورت پیشفرض این کار رو میکنه و اون رو برابر با DefaultPrecedence میذاره. میتونید بجای PrecedenceGroup از گروه های تقدمی که پیشفرض سوویفت هست استفاده کنید، یا گروه مورد نیاز خودتون رو تعریف کنید. ما اینجا برای توان نیاز داریم که یک گروه جدید تعریف کنیم، چون تقدم توان از ضرب هم بیشتر هست.

همونطور که مشخص هست، گروه تقدم جدید رو PowerPrecedence اسم گذاشتیم و تقدمش بالاتر از ضرب، MultiplicationPrecedence، و پایین تر از شیفتِ بیت، BitwiseShiftPrecedence، در نظر گرفته شده. اپراتور توان رو با علامت ^^ تعریف کردیم، چون علامت ^ بصورت پیشفرض، برای XOR در نظر گرفته شده. نکته مهم دیگه این هست که associativity رو راست در انتخاب کردیم، چون اولا توان میتونه با بقیه اپراتور ها ترکیب بشه، دوما، وقتی چند توان پشت هم نوشته بشن، از راست به چپ دسته بندی میشن.

برای اینکه ببینید چه گروه های تقدم و چه اپراتور هایی توی سوویفت وجود دارن، این لینک رو دنبال کنید.

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

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

اپراتور هایی که مفهوم ریاضی ندارند ?

اپراتور ها فقط برای اعمال ریاضی طراحی نشدن، یکی از بهترین مثال های این موضوع در ReactiveCocoa وجود داره که یکی از کارهاش این هست که یک سیگنال رو به یک property وصل میکنه. ما خودمون یک کاربرد جدید براشون طراحی میکنیم.

توی سوویفت شی Date وجود داره. Date به خودی خود فقط تاریخ رو نگه میداره، برای نمایش Date به فرمت دلخواه ما، لازم هست که از یک DateFormatter استفاده کنیم و Date رو بهش بدیم تا یک ‌String به ما برگردونه که شکل دلخواه ما رو داره.

این پروسه میتونه هر بار چند خط طول بکشه ولی با تعریف یک اپراتور میتونیم کارمون رو راحت کنیم. ما اپراتورمون رو به شکل <~ انتخاب میکنیم. به معنی اینکه Date که سمت چپ هست رو بریز در قالبی که به فرم String و در سمت راست قرار داره.

همونطور که میبینید یک گروه تقدم براش تعریف کردم که طبق تعریف، تقدمش از جمع بالاتره. علت اینکه تقدم رو بالاتر از جمع گرفتیم این بود که چون خروجی از جنس String هست، شاید بخواهیم با String های دیگه جمعش بزنیم، این بالاتر بودن تقدم باعث میشه که اول اپراتور <~ اعمال بشه، و بعد خروجی جمع زده بشه. نکته بعدی این هست که associativity رو none درنظر گرفتیم. یعنی با اپراتور های هم نوع خودش ترکیب نمیشه علت هم این هست که جنس ورودی ها و خروجی ها چیز های متفاوتی هستن.

حالا میبینیم که توی یک خط میشه یک Date رو به شکل های مختلف بیان کرد.

صحبت پایانی ?

? همونطور که قول دادم، فایل Playground مربوط به این مقاله از این لینک قابل دانلود هست.

و ممنون که همراه من بودید در این مقاله. امیدوارم خوشتون اومده باشه. حتما نظراتتون رو زیر همین پست بذارید یا بصورت ایمیل به آدرس alireza.asadi.36@gmail.com ارسال کنید. همینطور اگه مبحث دیگه ای هست که براتون جذابه و دوست دارید در بارش یه آموزشی بذارم، حتما بهم بگید. ??

? مرجع اصلی این مقاله کتاب The Swift Programming Language یا همون سایت Swift.org هست. برای بیان بهتر بحث Associativity یا جهت ترکیب شدن با اپراتور ها با تقدم یکسان، از Wikipedia.org استفاده شده.