تایپ‌های Generic در کلاس‌ها

Generic Types in Classes

13 مرداد 1399
تایپ های Generic در کلاس ها

حالا که با استفاده از تایپ های Generic در کلاس ها آشنا شدیم باید با نحوه کار این تایپ ها در کلاس ها نیز آشنا شویم. فرض کنید کلاسی به شکل زیر داشته باشیم:

class DataStorage {
  private data = [];

  addItem(item) {
    this.data.push(item);
  }

  removeItem(item) {
    this.data.splice(this.data.indexOf(item), 1);
  }

  getItems() {
    return [...this.data];
  }
}

این کلاس مخصوص ذخیره داده های برنامه فرضی ما است و سه متد addItem و removeItem و getItems را دارد که به ترتیب مسئول اضافه کردن عضو جدید، حذف یک عضو و برگرداندن یک کپی از داده ها می باشد. توجه کنید که پیدا کردن ایندکس داده (data.indexOf) در متد splice روش حذف عناصر از یک آرایه است. در حال حاضر کد بالا به ما چندین خطا می دهد. اولین خطا در مورد data.push است و می گوید هیچ تایپی را برای data خود انتخاب نکرده اید. از آنجایی که ما به مقدار و تایپ دقیق data اهمیت نمی دهیم می توانیم از تایپ های Generic استفاده کنیم:

class DataStorage<T> {
  private data: T[] = [];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    this.data.splice(this.data.indexOf(item), 1);
  }

  getItems() {
    return [...this.data];
  }
}

همانطور که می بینید مثل جلسات قبل از یک generic type ساده استفاده کرده ام و نامش را T گذاشته ام. حالا خطاها برطرف شده و می توانم به شکل زیر از آن استفاده کنم:

const textStorage = new DataStorage<string>();
textStorage.addItem('Max');
textStorage.addItem('Manu');
textStorage.removeItem('Max');
console.log(textStorage.getItems());

در این کد ابتدا یک متغیر به نام textStorage را ایجاد کرده ام و تایپ آن را روی string گذاشته ام تا به کلاس DataStorage بگوییم در textStorage فقط مقادیر رشته ای را ذخیره می کنیم. سپس دو عضو جدید به نام های Max و Manu اضافه کرده ام ولی بعدا Max را حذف کرده ام. در نهایت با log کردن داده ها (متد getItems) مقدار Manu در کنسول مرورگر نمایش داده می شود که مقدار صحیح است. مسئله استفاده از تایپ های Generic نیز همین است، ممکن است ما نخواهیم فقط رشته داشته باشیم بلکه عددها نیز بتوانند از این کلاس استفاده کنند. مثلا:

const numberStorage = new DataStorage<number>();

حتی می توانیم با استفاده از union type ها از هر دو استفاده کنیم:

const numberStorage = new DataStorage<number | string >();

البته کد ما دارای خطایی مخفی است! فرض کنید بخواهیم به جای رشته ها و اعداد از اشیاء استفاده کنیم:

const objStorage = new DataStorage<object>();
objStorage.addItem ({name: 'Max'});
objStorage.addItem({name: 'Manu'});
objStorage.removeItem({name: 'Manu'});
console.log(objStorage.getItems());

در اینجا می خواهیم از تایپ object استفاده کنیم و دو شیء بالا (نام های Max و Manu) را به آن اضافه کنیم. در نهایت نام Manu را حذف کرده و داده های فعلی را console.log می کنیم. کد بالا بدون هیچ مشکلی اجرا شده و فقط نام Max در کنسول مرورگر نمایش داده می شود اما اگر به جای Manu مقدار Max را حذف کنیم به مشکل  می خوریم:

const objStorage = new DataStorage<object>();
objStorage.addItem ({name: 'Max'});
objStorage.addItem({name: 'Manu'});
objStorage.removeItem({name: 'Max'});
console.log(objStorage.getItems());

مشکل اینجاست که اگر کد بالا را اجرا کنید به جای Manu مقدار Max نمایش داده می شود در حالی که ما Max را حذف کرده ایم! مشکل کجاست؟ مشکل اینجاست که اشیاء و آرایه ها در جاوا اسکریپت از نوع referenced-type هستند و متد ما برای حذف (data.splice در متد removeItem که از indexOf استفاده می کند) فقط روی انواع primitive کار می کند. به قسمت زیر نگاه کنید:

objStorage.removeItem({name: 'Max'});

از نظر فنی، شیء ای که به عنوان پارامتر removeItem پاس داده شده است یک شیء جدید می باشد بنابراین به یک مکان جدید در مموری اشاره می کند (مبحث primitive type ها و reference type ها) نه به مکان شیء قبلی. بنابراین متد removeItem سعی می کند index آن را پیدا کند اما ممکن نیست بنابراین آخرین عنصر آرایه را حذف می کند. امیدوارم یادتان باشد که اگر indexOf نتواند مقدار مورد نظر را پیدا کند، 1- را برمی گرداند که یعنی splice از آخر آرایه شروع می کند و آن عضو را حذف می کند. بدین صورت فقط Max باقی می ماند.

برای جلوگیری از حذف شدن اعضای آرایه بهتر است یک شرط به متد removeItem اضافه کنیم:

  removeItem(item: T) {
    if (this.data.indexOf(item) === -1) {
      return;
    }
    this.data.splice(this.data.indexOf(item), 1); // -1
  }

این شرط می گوید اگر data.indexOf برای item برابر 1- شد (یعنی index را پیدا نکردیم) فقط return کن یا به عبارتی کاری انجام نده و از متد خارج شو تا ناخواسته چیزی را حذف نکرده باشیم. البته هنوز مشکل را حل نکرده ایم چرا که متد ما هنوز هم برای اشیاء معتبر نیست و نمی تواند آن ها را حذف کند. راه حل این مورد این است که مطمئن شویم دقیقا همان شیء را پاس داده ایم! آیا متوجه منظور من می شوید؟ به کد زیر نگاه کنید:

const objStorage = new DataStorage<object>();
const maxObj = {name: 'Max'};
objStorage.addItem(maxObj);
objStorage.addItem({name: 'Manu'});
objStorage.removeItem(maxObj);
console.log(objStorage.getItems());

من شیء خودم را در یک ثابت به نام maxObj ایجاد کرده ام تا یک نمونه واحد از آن داشته باشم. در واقع با این کار مطمئن می شویم که شیء ما همیشه به یک نقطه در مموری اشاره می کند و هیچ تداخلی در کد های ما به وجود نمی آورد. با این حال این روش هم آنچنان چنگی به دل نمی زند بنابراین بهتر است کاری کنیم که کلاس ما فقط با primitive value ها کار کند (رشته ها، اعداد، بولین و ...). این کار را با همان Generic type ها انجام می دهیم:

class DataStorage<T extends string | number | boolean> {
  private data: T[] = [];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    if (this.data.indexOf(item) === -1) {
      return;
    }
    this.data.splice(this.data.indexOf(item), 1); // -1
  }

  getItems() {
    return [...this.data];
  }
}

با ویرایش کلاس به کد بالا سریعا خطایی برای کد زیر می گیرید:

const objStorage = new DataStorage<object>();

به دلیل اینکه گفته ایم کلاس ما فقط مقادیر رشته، عدد و بولین را قبول می کند بنابراین اشیاء مجاز نخواهد بود. بعدا می توانیم کلاسی مخصوص به اشیاء داشته باشیم که مثلا در آن برای هر شیء یک id تعریف کنیم تا بر اساس همان id خاص شیء مورد نظر را حذف کنیم و یا از روش های دیگری استفاده کنیم.

در نهایت به یاد داشته باشید که می توانیم برای متد های خودمان generic type های دیگری را تعریف کنیم:

  addItem<U>(item: T) {
    this.data.push(item);
  }

مثلا متد بالا از U استفاده می کند. چنین حالتی در زمینه هایی مفید است که درون متد به تایپ خاصی احتیاج داشته باشیم، نه در کل کلاس. امیدوارم مسئله generic type ها در کلاس ها را نیز درک کرده باشید و فهمیده باشید که مزیت اصلی generic type ها ارائه انعطاف پذیری در عین type checking است.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش جامع تایپ اسکریپت توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.

ما را دنبال کنید
اینستاگرام روکسو تلگرام روکسو ایمیل و خبرنامه روکسو