آشنایی با انواع داده در زبان Dart برای توسعه‌دهندگان جاوا اسکریپت

A Tour of the Dart Language

آشنایی با انواع داده در زبان Dart برای توسعه دهندگان جاوا اسکریپت

Dart یک زبان general-purpose بوده که توسط گوگل ساخته شده است. زبان های general-purpose زبان هایی هستند که به طور خاص برای کاری خاص طراحی نشده اند بلکه می توان با آن ها کار های مختلفی انجام داد و به یک حوزه پایبند نیستند. همچنین Dart یک زبان statically typed است که یعنی تایپ متغیرها و داده ها هنگام نوشتن کد مشخص می شود. مثلا برای تعریف یک رشته حتما باید مشخص کنید که داده از نوع رشته است و نمی توانید بدون تغییر صریح و دستی در کدها داده دیگری را در آن ذخیره کنید. البته اگر بخواهیم دقیق تر بگوییم Dart یک زبان type inferred است که یعنی مشخص کردن تایپ داده ها اجباری نیست.

ما در این مقاله می خواهیم با زبان Dart آشنا شده و آن را با جاوا اسکریپت (یا بهتر بگویم،‌ تایپ اسکریپت) مقایسه کنیم. شاید از خواندن این جمله تعجب کنید اما Dart شباهت های بسیار زیادی با جاوا اسکریپت و مخصوصا تایپ اسکریپت دارد تا حدی که یادگیری آن برای توسعه دهندگان تایپ اسکریپت مانند آب خوردن است. در صورتی که از توسعه دهندگان تایپ اسکریپت یا جاوا اسکریپت نیستید هنوز هم می توانید متوجه موضوعات توضیح داده شده در این مقاله بشوید به شرطی که حداقل با یک زبان برنامه نویسی (PHP یا Python یا Ruby یا Java یا ...) آشنایی داشته باشید.

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

۱. متغیرها (variables)

همانطور که می دانید متغیرها برای ذخیره داده ها استفاده می شوند. برای تعریف یک متغیر در dart دقیقا مانند جاوا اسکریپت از کلیدواژه var استفاده می کنیم:

var name = 'Amir';

در حال حاضر متغیر name در کد بالا نوع داده رشته (String) را دارد (در ادامه درباره نوع داده ها صحبت خواهیم کرد). همچنین دقیقا مانند جاوا اسکریپت باید تمام دستورات را با ; (علامت نقطه ویرگول) ببندید. البته انجام ندادن این کار در جاوا اسکریپت مجاز است اما در dart خطا دریافت خواهید کرد.

dart نوع خاصی از متغیرها را دارد که به آن ها Late variables می گوییم. این متغیرها در ابتدا تعریف می شوند و بعدا مقدار دهی خواهند شد:

late String website;




void main() {

  website = 'roxo.ir';

  print(website);

}

همانطور که می بینید برای تعریف متغیر website از کلیدواژه late و String استفاده کرده ایم. همانطور که گفتم در dart می توانیم نوع داده یک متغیر را مشخص کنیم و از آنجایی که داده را در همین ابتدا نداریم باید نوع آن را مشخص کنیم که با کلیدواژه String مشخص شده است. کلیدواژه late نیز به معنی این است که این متغیر بعدا مقدار دهی خواهد شد. اگر فراموش کنید این متغیر را بعدا مقدار دهی کنید، یک خطا هنگام اجرای برنامه به شما داده خواهد شد.

احتمالا بپرسید تابع main از کجا آمده است؟ ما در جاوا اسکریپت کدهایمان را مستقیما در فایل می نویسیم اما در dart هر برنامه باید یک تابع اصلی وجود داشته باشد که معمولا main نام دارد. void نیز بدین معنی است که تابع main هیچ مقداری را برنمی گرداند. شما می توانید این موضوع را تغییر بدهید اما void به صورت پیش فرض در اکثر برنامه های dart قرار می گیرد. مثلا اگر قرار باشد تابع main یک رشته را برگرداند باید بدین شکل عمل کنیم:

late String website;




String main() {

  website = 'roxo.ir';

  print(website);

  return "Hello roxo";

}

فعلا نگران این موضوع نباشید، در قسمت های بعدی درباره انواع داده صحبت خواهیم کرد. مسئله مهم اینجاست که چنین موردی بسیار نادر است. در ۹۹ درصد برنامه ها تابع main نباید هیچ مقداری را برگرداند چرا که بالاترین نقطه شروع برنامه است بنابراین معمولا چیزی خارج از آن وجود ندارد تا آن را دریافت کند.

در dart علاوه بر var می توانید از const و final نیز برای تعریف متغیرها استفاده کنید:

  • متغیرهای const در زمان کامپایل شدن برنامه همیشه ثابت هستند و تلاش در ویرایش آن ها باعث تولید خطا می شود.
  • متغیرهای final فقط می توانند یک بار مقدار دهی شوند.

با این حساب تمام const ها به طور غیر مستقیم final محسوب می شوند چرا که قابلیت ویرایش ندارند. مثالی از یک متغیر final را ببینید:

final String name = 'Amir';

حالا اگر بخواهیم مقدار متغیر بالا را ویرایش کنیم به خطا برخورد می کنیم. من خطا تولید شده را به صورت یک کامنت قرار داده ام:

name = 'Ahmad'; // Error: a final variable can only be set once.

const نیز معمولا برای ذخیره مقادیری استفاده می شود که هیچ گاه در برنامه شما تغییر نمی کنند:

const bar = 1000000; // واحد فشار در برنامه ما ثابت است

const double atm = 1.01325 * bar; // فشار اتمسفر در برنامه ما ثابت است

const baz = [];

حالا اگر بخواهیم baz را ویرایش کنیم چطور؟ با خطا روبرو می شویم:

baz = [42]; // Error: Constant variables can't be assigned a value.

یک تفاوت ساده بین final و const اینجاست که اگر شیء ای final باشد خودش قابل تغییر نیست اما فیلد های موجود در آن هنوز قابل ویرایش هستند در حالی که اشیاء const به هیچ عنوان نه خودشان و نه فیلد هایشان قابل ویرایش نیستند.

۲. نوع داده ها (data types)

نوع داده ها دقیقا همان چیزی هستند که از نامشان مشخص است: نوع داده یک متغیر خاص را مشخص می کنند. آیا مثال قبلی ما را به یاد دارید؟

late String website;

در این مثال String یک نوع داده است (به طور خاص یک رشته است). در dart انواع داده زیر وجود دارد:

  • برای اعداد نوع داده int (اعداد طبیعی) و double (اعداد اعشاری) وجود دارد.
  • برای رشته ها نوع داده String وجود دارد که حتما باید با S بزرگ نوشته شود.
  • برای boolean ها نوع داده bool وجود دارد.
  • برای لیست ها نوع داده List وجود دارد (معادل آرایه ها در جاوا اسکریپت).
  • برای set ها نوع داده Set وجود دارد.
  • برای map ها نوع داده Map وجود دارد.
  • برای runes نوع داده Runes وجود دارد.
  • برای عدم وجود یک مقدار، نوع داده null وجود دارد.
  • انواع داده Future و Stream برای کدنویسی ناهمگام استفاده می شوند.
  • نوع داده never به معنی این است که یک دستور خاص هیچ وقت با موفقیت تمام نمی شود. معمولا برای توابعی استفاده می شود که همیشه exception پرتاب می کنند.
  • برای عدم اطمینان از یک نوع داده، نوع داده dynamic وجود دارد. dynamic باعث غیر فعال شدن type checking می شوند.
  • نوع داده void به معنی عدم استفاده از یک مقدار است. معمولا به عنوان نوع داده برگردانده شده در توابع استفاده می شود.
  • برای symbol ها نوع داده Symbol وجود دارد. symbolها اشیائی هستند که نماینده یک اپراتور یا شناسه در dart است. تقریبا هیچ وقت از symbol ها استفاده نخواهید کرد.

احتمالا با برخی از این انواع داده آشنا نباشید بنابراین بهتر است نگاهی به آن ها بیندازیم.

اعداد

int ها نماینده اعداد صحیح هستند و حداکثر ۶۴ بیتی خواهند بود. double ها نماینده اعداد اعشاری هستند و باز هم ۶۴ بیتی خواهند بود. هر دوی این تایپ ها زیرمجموعه نوع داده دیگری به نام num هستند بنابراین اگر بخواهید یک متغیر خاص هم اعداد صحیح و هم اعشاری را قبول کند باید از num استفاده کنید.

num x = 1;

x += 2.5;

در ضمن اگر یک عدد صحیح را به متغیری با نوع داده اعشاری بدهید، آن عدد مستقیما بخش اعشار می گیرد. مثال:

double z = 1; // double z = 1.0.

همچنین برای تبدیل اعداد به رشته و تبدیل رشته به عدد متد های زیر را داریم:

// String -> int

var one = int.parse('1');

assert(one == 1);




// String -> double

var onePointOne = double.parse('1.1');

assert(onePointOne == 1.1);




// int -> String

String oneAsString = 1.toString();

assert(oneAsString == '1');




// double -> String

String piAsString = 3.14159.toStringAsFixed(2);

assert(piAsString == '3.14');

رشته ها

رشته ها در dart توالی خاصی از کاراکتر های UTF-16 را ذخیره می کنند و ساخت آن ها نیز دقیقا مانند جاوا اسکریپت با علامت quotation است. همچنین همه می دانید که جاوا اسکریپت قابلیتی به نام template literal را دارد که در آن با استفاده از {}$ می توانید مقادیر پویا مانند یک متغیر را به یک رشته تزریق کنید. dart نیز عینا همین قابلیت را دارد:

var s = 'string interpolation';




assert('Dart has $s, which is very handy.' ==

    'Dart has string interpolation, ' +

        'which is very handy.');

assert('That deserves all caps. ' +

        '${s.toUpperCase()} is very handy!' ==

    'That deserves all caps. ' +

        'STRING INTERPOLATION is very handy!');

همانطور که می بینید برای تزریق رشته کافی است از علامت $ و نام متغیر استفاده کنید (s$) اما اگر می خواهید علاوه بر تزریق رشته عملیات های کدنویسی نیز انجام بدهید باید به جای $ از {}$ استفاده کنید.

در نهایت استایل dart برای ساخت رشته های چند خطی شبیه به استایل زبان پایتون است و از سه علامت quotation استفاده می کند:

var s1 = '''

You can create

multi-line strings like this one.

''';

من در بخش های قبلی به نوع داده runes اشاره کرده اما runes چیست؟ در زبان dart نوع داده runes کد Unicode کاراکتر ها را نمایش می دهد. در Unicode هر کاراکتر یک کد خاص دارد و runes آن را به ما می دهد. مثلا اموجی خنده کد 1f606 را دارد.

boolean ها

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

// Check for an empty string.

var fullName = '';

assert(fullName.isEmpty);




// Check for zero.

var hitPoints = 0;

assert(hitPoints <= 0);




// Check for null.

var unicorn;

assert(unicorn == null);




// Check for NaN.

var iMeantToDoThis = 0 / 0;

assert(iMeantToDoThis.isNaN);

تمام مقایسه های بالا به صورت bool انجام می شود.

لیست ها

نوع داده list معادل نوع داده آرایه در جاوا اسکریپت است. آرایه یا لیست مجموعه ای منظم و مرتب از مقادیر هستند. توجه داشته باشید که «مرتب» به معنی دارای ترتیب است بنابراین ترتیب در لیست ها حفظ می شود.

var list = [1, 2, 3];

dart به صورت خودکار تایپ لیست بالا را تعیین می کند که <List<int است. اگر با تایپ اسکریپت کار کرده باشید می دانید که این تایپ یعنی لیستی از مقادیر int (عدد صحیح). همچنین dart قابلیت استفاده از trailing comma (قرار دادن کاما پس از آخرین عضو لیست) را نیز دارد:

var list = [

  'Car',

  'Boat',

  'Plane',

];

در ضمن اگر می خواهید لیست شما در زمان کامپایل ثابت باشد از const قبل از آن استفاده کنید:

var constantList = const [1, 2, 3];

باز هم مانند جاوا اسکریپت به اپراتور spread نیز دسترسی داریم اما یک اپراتور دیگر به نام اپراتور null-aware spread نیز وجود دارد که می تواند مقادیر null را تشخیص داده و از بهم ریختن برنامه جلوگیری کند. اپرتور spread عادی:

var list = [1, 2, 3];

var list2 = [0, ...list];

assert(list2.length == 4);

اپراتور null-aware spread:

var list;

var list2 = [0, ...?list];

assert(list2.length == 1);

با اینکه متغیر list  خالی است اما خطایی نمی گیریم چرا که از علامت ؟ استفاده کرده ایم.

یکی دیگر از قابلیت های عالی dart قابلیت collection if و collection for است که به ما اجازه می دهند در صورت صحیح بودن یک شرط لیست خود را تولید کنیم یا با استفاده از حلقه های for لیست خود را بسازیم. به مثال زیر توجه کنید:

var nav = [

  'Home',

  'Furniture',

  'Plants',

  if (promoActive) 'Outlet'

];

این لیست سه عنصر دارد اما عنصر چهارم مشروط است یعنی اگر متغیر promoActive صحیح بود عضو outlet نیز به این لیست اضافه می شود. همچنین برای حلقه های for:

var listOfInts = [1, 2, 3];




var listOfStrings = [

  '#0',

  for (var i in listOfInts) '#$i'

];

assert(listOfStrings[1] == '#1');

در اینجا می خواهیم اعضای لیست listOfInts را به لیست listOfStrings اضافه کنیم اما قبل از آن می خواهم رشته 0# را نیز داشته باشیم. برای این کار ابتدا 0# را نوشته و سپس از یک حلقه for استفاده کرده ایم.

یادتان باشد که لیست ها تمام متد های محبوب کار با آرایه ها در جاوا اسکریپت را دارند. از آنجایی که این مقاله برای یادگیری کامل زبان dart نیست من وارد تک تک متد ها و جزئیات نمی شوم اما شما می توانید با مراجعه به وب سایت dart این متد ها را بررسی کنید.

set ها

set ها مجموعه ای نامرتب از داده های یکتا هستند. نامرتب یعنی هیچ ترتیبی در ثبت و دریافت آن ها در مموری نیست و یکتا یعنی هیچ دو عضوی نمی توانند تکراری باشند. ساختار set ها دقیقا مانند لیست ها است اما به جای [] از {} استفاده می شود:

var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};

برای تعریف set های خالی حتما باید صریح باشید تا زبان dart آن را با map ها (بعدا بررسی می کنیم) اشتباه نگیرد. دو راه ساده برای تعریف یک set خالی:

var names = <String>{};

Set<String> names = {};

هر دو دستور بالا یک set خالی را تعریف می کنند اما دستور زیر یک map را تولید می کند:

var names = {}; // این متغیر یک مپ است

با این حساب باید map ها را نیز بررسی کنیم.

map ها

map ها شبیه به اشیاء در جاوا اسکریپت هستند، یعنی مجموعه ای از جفت های key/value می باشند. برخلاف جاوا اسکریپت شما می توانید هر مقداری را به عنوان یک کلید استفاده کنید (حتی کلاس ها و توابع) اما کلید های یک map نمی توانند تکراری باشند. به مثال ساده زیر از map ها دقت کنید:

var gifts = {

  // Key:    Value

  'first': 'partridge',

  'second': 'turtledoves',

  'fifth': 'golden rings'

};




var nobleGases = {

  2: 'helium',

  10: 'neon',

  18: 'argon',

};

در متغیر gifts کلید های map رشته ای هستند اما در nobleGases کلید ها عددی می باشند. همانطور که گفتم نوع کلید ها به سلیقه شما بستگی دارد. البته شما می توانید map ها را با استفاده از constructor خاص map ها نیز ایجاد کنید:

var gifts = Map<String, String>();

gifts['first'] = 'partridge';

gifts['second'] = 'turtledoves';

gifts['fifth'] = 'golden rings';




var nobleGases = Map<int, String>();

nobleGases[2] = 'helium';

nobleGases[10] = 'neon';

nobleGases[18] = 'argon';

احتمالا با خودتان می گویید اگر ()Map یک constructor است پس کلیدواژه new کجاست؟ در زبان dart استفاده از new برای نمونه سازی از کلاس ها کاملا اختیاری است و می توانید آن را حذف کنید.

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

توابع

dart یک زبان کاملا شیء گرا است بنابراین حتی توابع نیز در آن شیء هستند. نوع داده توابع در این زبان، Function است و تعریف آن ها تفاوتی با جاوا اسکریپت ندارد:

bool isNoble(int atomicNumber) {

  return _nobleGases[atomicNumber] != null;

}

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

در جاوا اسکریپت نوع خاصی از توابع به نام arrow function ها را داریم. در زبان dart نیز اگر دستور درون بدنه تابع از یک expression بیشتر نباشد می توانید از همان ساختار استفاده کنید:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

در ضمن می توانید از پارامتر های اسمی (Named parameters) که در جاوا اسکریپت داریم نیز استفاده کنید:

enableFlags(bold: true, hidden: false);

همچنین با قرار دادن پارامتر ها درون علامت های [] آن پارامتر را اختیاری تعریف می کنید:

String say(String from, String msg, [String? device]) {

  var result = '$from says $msg';

  if (device != null) {

    result = '$result with a $device';

  }

  return result;

}

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

Future و کدنویسی ناهمگام

بسیاری از عملیات ها در دنیای برنامه نویسی به صورت ناهمگام یا asynchronous انجام می شوند. عملیات های ناهمگام معمولا عملیات هایی هستند که زمان زیادی را می گیرند و اگر بخواهیم آن ها را به صورت هنگام انجام بدهیم باعث مسدود شدن برنامه تا زمان اتمام آن عملیات خاص می شود. سه مثال ساده از عملیات های ناهمگام دریافت داده از شبکه (مثلا یک API دیگر)، خواندن داده از یک فایل روی هارد دیسک و ثبت داده در پایگاه داده است.

برای انجام عملیات های ناهمگام در dart به سه عنصر اصلی نیاز داریم:

  • Future ها که دقیقا مانند promise های جاوا اسکریپت هستند.
  • async برای تعریف یک دستور ناهمگام
  • await برای اجرا و منتظر ماندن برای یک دستور ناهمگام

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

در مورد تایپ future ها هم می توان به صورت یک قاعده کلی گفت که <Future<T پس از کامل شدن داده ای از نوع T را خواهد داشت. مثلا <Future<String پس از کامل شدن یک داده رشته ای را به ما می دهد یا اگر عملیات ناهمگام چیزی را برنمی گرداند <Future<void تایپ صحیح برای آن است. بیایید با یک مثال ساده از future ها آشنا شویم:

Future<void> fetchUserOrder() {

  // من به صورت مصنوعی زمان انتظار دو ثانیه ای را برای این کد ایجاد کرده ام

  return Future.delayed(Duration(seconds: 2), () => print('Large Latte'));

}




void main() {

  fetchUserOrder();

  print('Fetching user order...');

}

در اینجا با استفاده از تابع delayed و تنظیم آن روی ۲ ثانیه سعی کرده ایم حالتی شبیه به عملیات های ناهمگام را ایجاد کنیم. همچنین از آنجایی که تابع fetchUserOrder هیچ مقداری را به جز خود future برنمی گرداند تایپ <Future<void را می گیرد. به نظر شما با اجرای کد بالا ابتدا رشته Fetching user order نمایش داده می شود یا رشته Large Latte؟ اگر در تایپ اسکریپت/جاوا اسکریپت با promise ها کار کرده باشید حتما می دانید که کد بالا ابتدا رشته Fetching user order را برمی گرداند چرا که تکمیل شدن future دو ثانیه طول می کشد. این مسئله برای پرتاب خطا ها نیز به همین شکل است:

Future<void> fetchUserOrder() {

  return Future.delayed(Duration(seconds: 2),

      () => throw Exception('Logout failed: user ID is invalid'));

}




void main() {

  fetchUserOrder();

  print('Fetching user order...');

}

با اجرای کد بالا ابتدا رشته Fetching user order نمایش داده شده و سپس یک خطا برایتان پرتاب می شود.

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

// این یک مثال کاملا اشتباه از کار با کدهای ناهمگام است




String createOrderMessage() {

  var order = fetchUserOrder();

  return 'Your order is: $order';

}




Future<String> fetchUserOrder() =>

    // تولید وقفه مصنوعی

    Future.delayed(

      Duration(seconds: 2),

      () => 'Large Latte',

    );




void main() {

  print(createOrderMessage());

}

در این کد تابعی به نام fetchUserOrder را داریم که سفارش کاربر را دریافت می کند اما ما ۲ ثانیه وقفه مصنوعی در آن ایجاد کرده ایم تا پس از دو ثانیه رشته large latte را برگرداند. همچنین متدی به نام createOrderMessage را داریم که سفارش کاربر را از تابع fetchUserOrder گرفته و آن را درون یک رشته قرار داده و برمی گرداند. در متد main نیز از تابع print استفاده کرده ایم تا این سفارش را چاپ کنیم. به نظر شما با اجرای کد بالا چه اتفاقی می افتد؟ اگر کد بالا را اجرا کنید نتیجه زیر را می گیرید:

Your order is: Instance of '_Future<String>'

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

راه حل چیست؟ راه حل استفاده از async و await است. شما نمی توانید تک تک بخش های برنامه خود را به صورت async یا ناهمگام تعریف کنید بلکه فقط بخش های خاصی از برنامه به شکل ناهمگام اجرا می شوند. هنگام کار با async و await باید به یاد داشته باشید:

  • برای تعریف یک تابع به صورت ناهمگام،‌ کلیدواژه async را قبل از نام تابع بنویسید.
  • برای منتظر ماندن برای اتمام یک عملیات ناهمگام از کلیدواژه await استفاده کنید. await فقط در بلوک های کد async قابل استفاده است.

مثالی از یک تابع async:

void main() async { ··· }

بیایید به یک مثال واقعی و صحیح از کار با کدهای ناهمگام برویم:

Future<String> createOrderMessage() async {

  var order = await fetchUserOrder();

  return 'Your order is: $order';

}




Future<String> fetchUserOrder() =>

    // ایجاد وقفه مصنوعی

    Future.delayed(

      Duration(seconds: 2),

      () => 'Large Latte',

    );




Future<void> main() async {

  print('Fetching user order...');

  print(await createOrderMessage());

}

همانطور که می بینید من توابع createOrderMessage و main را به صورت async تعریف کرده ام. با انجام این کار می توانیم درون آن ها از await استفاده کنیم. از این به بعد هر جایی که با future ها سر و کار داشته باشیم از await استفاده می کنیم تا کدها منتظر تمام شدن آن باشند. با اجرای این کد نتیجه این بار به شکل زیر خواهد بود:

Fetching user order...

Your order is: Large Latte

امیدوارم این مقاله به شما کمک کرده باشد تا با زبان dart آشنا شده باشید.


منبع: وب سایت dart

نویسنده شوید

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

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