در این مقاله میخوایم با Extension type در زبان Dart آشنا بشیم و کاربردهاش رو با مثال بررسی کنیم. اگر به خوندن خلاصه این مقاله علاقه دارید، پست من در لینکدین رو ببینید.
در یک تعریف کوتاه، Extension type یک انتزاع زمان کامپایل برای یک نوع داده است. به این صورت که یک نوع داده رو با یک واسط متفاوت و ایستا wrap میکنه. با استفاده از Extension type میتونیم بر حسب نیاز، یک سری محدودیتها و قوانین روی یک نوع داده موجود (مثلاً int) اعمال کنیم. از اینجا به بعد، به نوع دادهای که Extension type رو روی اون تعریف میکنیم، نوع داده اصلی (representation type) میگیم. با تعریف Extension type برای یک نوع داده میتونیم بعضی متدهای اون نوع داده رو استفاده یا برخی رو حذف کنیم و یا با عملکردهای جدیدی جایگزینشون کنیم.
برای اینکه موضوع واضحتر بشه با یک مثال ادامه میدم. در این مثال، نوع داده اصلی int هست و میخوایم بر مبنای int یک Extension type برای شماره ID بسازیم که عملکردهای اصلی int رو با یک سری تغییرات در اختیارمون میذاره. میخوایم نوع داده int رو با استفاده از Extension type به صورتی محدود کنیم که تنها عملکردهای معنیدار برای شماره ID رو داشته باشه. مثلا برای شماره ID عمل جمع معنی نداره پس این عملکرد رو نیاز نداریم.
extension type IdNumber(int id) { // Wraps the 'int' type's '<' operator: operator <(IdNumber other) => id < other.id; // Doesn't declare the '+' operator, for example, // because addition does not make sense for ID numbers. } void main() { // Without the discipline of an extension type, // 'int' exposes ID numbers to unsafe operations: int myUnsafeId = 42424242; myUnsafeId = myUnsafeId + 10; // This works, but shouldn't be allowed for IDs. var safeId = IdNumber(42424242); safeId + 10; // Compile-time error: No '+' operator. myUnsafeId = safeId; // Compile-time error: Wrong type. myUnsafeId = safeId as int; // OK: Run-time cast to representation type. safeId < IdNumber(42424241); // OK: Uses wrapped '<' operator. }
در مثال بالا یک Extension type به نام IdNumber تعریف کردیم که عملگر < رو برای مقایسه شماره Id تعریف کرده. عملگرهای دیگه برای int مثل جمع و تفریق رو نداریم چون برای نوع داده اصلی که در این مثال int هست بی معنی هستند و نیازی بهشون نیست.
در متد main ابتدا متغیر myUnsafeId رو تعریف و از نوع داده int برای id استفاده کردیم تا نشون بدیم چطور عملگر + رو میشه به اشتباه روی شماره Id اعمال کرد و هیچ محدودیتی در این زمینه وجود نداره. اما وقتی با استفاده از IdNumber متغیر safeId رو تعریف میکنیم هنگام استفاده از عملگر + با خطای زمان کامپایل مواجه میشیم چون این عملگر برای IdNumber تعریف نشده.
به دلیل محدودیتهایی که برای IdNumber تعریف شده، نمیتونیم متغیری از نوع داده اصلی رو به متغیری از نوع IdNumber تخصیص (Assign) بدیم و نیاز هست که قبل از تخصیص، متغیر نوع IdNumber رو به نوع اصلی تبدیل (Cast) کنیم.
خب تا اینجای کار شاید بگید که این کار رو میتونستیم با wrapper class هم انجام بدیم. یعنی یک کلاس به نام IdNumber ایجاد کنیم و قوانین و محدودیتهای مورد نظرمون رو اعمال کنیم. اما تفاوت اینجاست که با استفاده از Extension type نیازی به ایجاد آبجکتهای (Object) اضافه (از نوع کلاس wrapper) در زمان اجرا (که در مواردی میتونه هزینهبر هم باشه) نیست. چون Extension type ها ایستا هستند و کامپایل میشن بنابراین در زمان اجرا هیچ هزینهای تحمیل نمیکنند.
چطور یک Extension type تعریف کنیم؟
تعریف Extension type با کلمات کلیدی extension type شروع میشه و بعد نام رو مشخص میکنیم و به دنبالش نوع داده اصلی داخل پرانتز درج میشه:
extension type E(int i) { // Define set of operations. }
کد (inti) : مشخص میکنه که نوع داده اصلی که Extension type روی اون تعریف شده int هست و i ارجاعی به یک آبجکت از نوع داده اصلیه. دقت کنید که اینجا یک سازندهی ضمنی به صورت E(int i) : i = i داریم.
کاربردهای Extension type
در مورد Extension type اینطور فکر کنید که بهتون اجازه میده خودتون روی یک نوع داده یک type safety پیاده کنید، البته دقت داشته باشید که نمیتونید رفتار و خصوصیات نوع داده مدنظر رو تغییر بدید.
در ادامه میخوایم دو کاربرد مهم Extension type رو در قالب مثال بررسی کنیم:
extension type NumberE(int value) { NumberE operator +(NumberE other) => NumberE(value + other.value); NumberE get next => NumberE(value + 1); bool isValid() => !value.isNegative; } void testE() { var num1 = NumberE(1); int num2 = NumberE(2); // Error: Can't assign 'NumberE' to 'int'. num1.isValid(); // OK: Extension member invocation. num1.isNegative(); // Error: 'NumberE' does not define 'int' member 'isNegative'. var sum1 = num1 + num1; // OK: 'NumberE' defines '+'. var diff1 = num1 - num1; // Error: 'NumberE' does not define 'int' member '-'. var diff2 = num1.value - 2; // OK: Can access representation object with reference. var sum2 = num1 + 2; // Error: Can't assign 'int' to parameter type 'NumberE'. List<NumberE> numbers = [ NumberE(1), num1.next, // OK: 'next' getter returns type 'NumberE'. 1, // Error: Can't assign 'int' element to list type 'NumberE'. ]; }
این نوع Extension type به عنوان یک نوع دادهی جدید در نظر گرفته میشه و از نوع داده اصلی متمایزه بنابراین نمیتونید مقداری از نوع داده اصلی به متغیری از نوع این Extension type تخصیص بدید و به عملکردهای نوع داده اصلی هم دسترسی ندارید.
از این روش میشه برای جایگزین کردن واسط یک نوع داده موجود استفاده کرد. به این ترتیب میتونیم واسطی ایجاد کنیم که روی یک نوع دادهی جدید، محدودیت و قوانین معنیدار اعمال کنه، در حالی که از مزیتهایی مثل کارایی و سهولت استفاده که نوع دادههای موجود مانند int برخوردارند، استفاده میکنه.
کاربرد دوم: ایجاد یک واسط گسترشیافته (extended) برای یک نوع دادهی موجود:
در این حالت، Extension type، نوع دادهی اصلی مورد نظر رو implement میکنه. به این ترتیب، میتونه نوع داده اصلی رو ببینه و اعضای نوع داده اصلی رو فراخوانی کنه. مثال زیر رو در نظر بگیرید تا بیشتر توضیح بدم:
extension type NumberT(int value) implements int { // Doesn't explicitly declare any members of 'int'. NumberT get i => this; } void main () { // All OK: Transparency allows invoking `int` members on the extension type: var v1 = NumberT(1); // v1 type: NumberT int v2 = NumberT(2); // v2 type: int var v3 = v1.i - v1; // v3 type: int var v4 = v2 + v1; // v4 type: int var v5 = 2 + v1; // v5 type: int v2.i; // Error: Extension type interface is not available to representation type }
در این نوع Extension type علاوه بر عملکردهای نوع دادهی اصلی، به اعضای کمکی که خودتون تعریف میکنید هم دسترسی دارید. در نتیجه یک واسط گسترشیافته برای یک نوع داده موجود دارید. از این واسط جدید، برای متغیرهایی که از نوع Extension type تعریف میکنید استفاده میشه. در مثال بالا نوع داده اصلی int هست و همونطور که میبینید، متغیرهای نوع NumberT به عملیات تفریق و جمع که مربوط به نوع داده اصلیه دسترسی دارند. علاوه بر این، یک getter با نام i در NumberT تعریف شده که متغیرهای نوع NumberT بهش دسترسی دارند.
تفاوت Extension type با Extension method چیه؟
در واقع Extension method که بهش extension هم میگیم، مشابه Extension type یک انتزاع ایستاست. هرچند یک Extension method عملکردی رو مستقیما به هر نمونه (instance)، از نوعی که extension روی اون تعریف شده، اضافه میکنه. اما Extension type متفاوته: واسطی که با استفاده از Extension type تعریف میکنیم فقط روی متغیرهایی اعمال میشه که از نوع همون Extension type تعریف شده باشن. در واقع به صورت پیشفرض از واسط نوع داده اصلی خودشون متمایز هستند.
تفاوت extension method و extension type رو به طور خلاصه اینطور میشه گفت: Extension method برای افزودن قابلیتهای ساده به نوع داده موجود مناسبه در حالی که Extension type برای ایجاد API های سفارشی و بهینهسازی کارایی ایدهآله.
مزایای Extension type
تا اینجا به صورت ضمنی به مزایا اشاره کردم اما بطور خلاصه مهمترین مزایای Extension type اینهاست:
1- بهبود کارایی: نیاز به ایجاد آبجکت اختصاصی (wrapper) برای هر instance رو از بین میبرن و به همین دلیل برای مواردی که حساسیت زیادی روی کارایی وجود داره، بهخصوص مواردی که با مجموعه دادههای بزرگ یا دستکاری مکرر دادهها سروکار داریم، ایدهآل هستن.
2- انتزاع واضحتر: میشه APIهای تمیزی مشابه Dart ایجاد کرد تا پیچیدگیهای داده اصلی پنهان بشه و قابلیت نگهداری و خوانایی کد بهبود پیدا کنه.
ممنونم که همراهم بودید. لایک و کامنتهای شما خوشحالم میکنه :)