داستان Null در دارت(دادگاه رسیدگی به اتهامات دارت-1)
توی قسمت قبلی دیدیم که برنامه نویس ها یه شورش علیه Dart به راه انداختن و سرانجام جناب Dart دستگیر شد و قرار شد که یه دادگاه برای رسیدگی به اتهاماتش تشکیل بشه.
از اون جایی که اتهامات خیلی زیاد بود چند جلسه دادگاه برای رسیدگی به این اتهامات لازم بود و توی این قسمت داستان جلسه اول دادگاه رو با هم میخونیم.
قاضی دادگاه : از جناب Dart تقاضا میشود که به جایگاه تشریف بیاورن و پاسخگوی اتهامات مطرح شده باشند...
اما اولین اتهام چی بود؟
داستان قسمت قبلی یادته؟ که میرزا قلی کلاس آنلاین برگزار کرد و توی جلسه اول یه اتفاقاتی افتاد...
نتیجه ای که میرزا قلی گرفته بود و به بچها گفت این بود:
- اگه یک variable رو از نوع nullable تعریف کنیم(یعنی با ?), میتونیم همون اول کار بهش مقدار ندیم و مقدار اون به صورت پیش فرض برابر با null میشه.
- و اگه یه variable رو از نوع معمولی یا non-nullable تعریف کنیم (یعنی بدون ?), چون که دیگه نمیتونه به هیچ وجه مقدار null رو بگیره باید همون اول اول که تعریفش کردیم بهش یه مقدار هم بدیم.
ولی قل مراد گفتش که من یه کد اینجوری زدم :
و این کد داره دومین نتیجه ای که تو گرفتی رو نقض میکنه.
آخرش به این نتیجه رسیدن که حتمن Dart مشکل داره و باگ داره...
حالا Dart باید به این اتهام پاسخ میداد؟
از اینجا به بعد داستان رو از زبان Dart بشنویم. قبلش بگم که آقای Dart کتابی صحبت میکنه و دیگه این موضوع دست من نیست...
در ابتدا به عنوان مقدمه باید عرض کنم که در من (منظورش از من همون Dart هست) هر موقع از { } استفاده می کنید یک Scope جدید ایجاد می شود. برای مثال وقتی که یک class جدید مینویسید :
با قرار دادن { } یک Scope جدید ایجاد می شود.یا مثلن وقتی که یک function جدید تعریف میکنید :
همین طور وقتی که یک شرط if مینویسید یا یک حلقه for ایجاد میکنید :
همه این ها به دلیل استفاده از { } یک Scope جدید ایجاد میکنن.
میرزا قلمدون که همراه میرزا قلی از قل آباد به نیویورک رفته بود که در دادگاه شرکت کنه یهو بلند شد و گفت : حالا اینایی که گفتین چه ربطی به موضوع داره؟
قاضی دادگاه : تق تق تق ... نظم جلسه را رعایت کنید. جناب Dart ادامه دهید.
جناب Dart : حال در هر Scope به variable های تعریف شده در Scope های بالاتر از خودش (یا به عبارتی به Scope های پدر) دسترسی داریم ولی به Variable های تعریف شده در Scope های فرزند دسترسی نداریم.
همین طور باید اضافه کنم میتوانیم variable ها را براساس Scope ای که در آن تعریف می شوند به سه دسته کلی تقسیم کنیم:
- Global variables
دسته اول variable هایی هستن که خارج از هر Scope یعنی خارج از هر class یا function یا ... تعریف می شوند و در واقع در بدنه اصلی برنامه تعریف می شوند.
- Class Instance fields
دسته دوم instance field های مربوط به class ها هستن.
میرزا قلمدون باز پرید وسط و گفت : instance field چیه ؟
قاضی دادگاه : تق تق تق ... نظم جلسه را حفظ کنید...
میرزا قلمدون رو بخاطر به هم ریختن نظم جلسه از اتاق بیرون کردن...
- Local variables
دسته سوم variable هایی هستند که داخل بدنه یک function یا داخل Scope های مربوط به if یا ... تعریف می شوند و به آن ها Local می گوییم.
برای مثال :
در کد بالا نوع Global و Local را مشاهده میکنید.نوع Global در بدنه اصلی برنامه و نوع Local داخل function(function Scope) تعریف شده اند.
نوع اول یعنی Global ها داخل function قابل دسترسی و استفاده هستن ولی نوع دوم یعنی Local ها فقط و فقط داخل همون scope ای که تعریف شده اند قابل دسترسی هستن و خارج از آن قابل دسترسی نیستن.
دلیل این امر هم این هست که function Scope در واقع فرزند Global Scope محسوب میشود و میتواند به variable های آن دسترسی داشته باشد ولی Global Scope نمی تواند به variable های فرزند خودش یعنی function Scope دسترسی داشته باشد.
مثالی دیگر :
خروجی کد بالا به صورت زیر هست :
میبینید که در کد بالا یک تابع با نام myFunction تعریف کرده ام که برای خودش یک scope ایجاد میکند و داخل این scope به همه variable هایی که در scope خودش و همچنین scope بالاتر (global) تعریف شده اند دسترسی داریم.
همچنین داخل تابع myFunction یک تابع دیگر با نام myInnerFunction تعریف کرده ام که این تابع هم برای خودش یک scope ایجاد میکند({ }) و داخل این scope به همه variable های scope بالاتر (یعنی global و myFunction scope) دسترسی داریم.
ولی داخل globla scope نمیتوانیم به varibale هایی که داخل این دو function تعریف شده اند دسترسی داشته باشیم.همچنین از داخل myFunction نمیتوانیم به variable هایی که داخل myInnerFunction تعریف شده اند دسترسی داشته باشیم.
و یک مثال دیگر :
داخل Instance method های یک کلاس به همه local variable ها (که داخل همان method یا همان scope تعریف شده اند ) و همه instance filed های کلاس به علاوه همه global variable ها ( که خارج از کلاس تعریف شده اند) دسترسی داریم.
و به عنوان مثال آخر :
در کد بالا یک function داریم و داخل آن از یک if استفاده کرده ایم که استفاده از if باعث ایجاد یک Scope جدید می شود ( { } ) و داخل Scope مربوط به myFunction به variable هایی که در if Scope تعریف شده اند دسترسی نداریم ولی داخل Scope مربوط به if به همه ی variable های تعریف شده در Scope های بالاتر دسترسی داریم. در واقع در اینجا if Scope فرزند function Scope محسوب میشود و به variable های آن دسترسی دارد ولی function Scope به variable های فرزند خودش دسترسی ندارد.
پس :
- هر { } یک Scope جدید ایجاد می کند. و بدنه اصلی برنامه که شامل هیچ { } نیست را می توانیم Global Scope بنامیم.
- هر Scope به variable های تعریف شده در Scope های اجداد خود دسترسی دارد ولی به variable های تعریف شده در Scope های نوادگان خود دسترسی ندارد.
- براساس Scope های مختلف می تواینم variable ها را به سه دسته کلی Global و Local و Class instance field تقسیم کنیم.
بعد از این مقدمه باید عرض کنم که :
وقتی که یک non-nullable variable را به صورت Local تعریف میکنید نیازی نیست که همان ابتدا آن را مقدار دهی کنید. چون که آنالایزر من (منظورش آنالایزر دارت هست چون خودش دارته) هوشمند هست و میتواند کل بدنه مربوط به آن Scope(مثلن کل بدنه function) را بررسی کند.در این حالت اگر از variable تعریف شده در هیچ کجا استفاده نکنید کار بدون مشکل پیش می رود ولی به محض این که برای اولین بار می خواهید از آن استفاده کنید آنالایزر من کل بدنه مربوط به ان Scope را بررسی و تحلیل می کند و اگر ببینید که شما آن variable را قبل از استفاده مقدار دهی نکرده اید به شما خطا می دهد.
برای مثال :
در کد بالا یک variable از نوع non-nullable تعریف کرده ایم و آن را مقدار دهی نکرده ایم ولی چون که از آن استفاده نکرده ایم هیچ مشکلی رخ نمی دهد.
حالا کد زیر را ببینید :
نوع بازگشتی function رو String کردم و بدون مقداردهی variable آن را return کردم. و یک خطا گرفتم :
همان طور که میبینید این خطا می گوید که که یک non-nullable variable باید قبل از استفاده حتمن مقدار دهی بشود.
به عنوان مثالی دیگر :
میبینید که یک non nullable String با نام StringStatus تعریف کرده ام در موقع return خطا گرفته ام.چون آنالایزر مطمین نیست که در هر شرایطی این variable قبل از استفاده (یعنی قبل از return )مقدار میگیرد.که با افزودن یک else به آنالایزر اطمینان میدهم که در هرصورت این variable قبل از استفاده مقدار دهی میشود و خطا برطرف میشود.
ولی این نکته فقط و فقط برای Local variables صادق است و برای Global variables و Class instance fields نمی توانیم از این نکته استفاده کنیم.چرا که آنالایزر من نمی تواند کل برنامه یا کل یک کلاس را بررسی و تحلیل کند و تشخیص دهد که آیا یک variable که در این Scope ها تعریف شده است قبل از استفاده مقدار دهی شده است یا خیر پس variable هایی از این دو نوع که از نوع non-nullable هستند باید همان ابتدا مقدار دهی شوند.
برای مثال :
در کد بالا یک Global variable داریم که از نوع non-nullable است و باید در همان لحظه تعریف مقداردهی شود, در غیر این صورت با خطا مواجه خواهید شد.
این قضیه برای Field های یک کلاس هم صادق هست :
پس :
- اگر یک variable از جنس nullable باشد میتواند موقع تعریف مقدارهی نشود و مقدار آن برابر با null خواهد شد.
- اگر یک varibale از جنس non-nullable باشد و به صورت Local تعریف شود, می تواند موقع تعریف مقدار نگیرد و خود آنالایزر هشدارهای لازم برای مقدار دهی را در صورت لزوم میدهد.(موقع اولین استفاده آنالایزر چک می کند که ایا آن variable مقدار دهی شده است یا نه)
- اگر یک variable از جنس non-nullable باشد و داخل global scope تعریف شده باشد و یا داخل class scope (class field) باید همان موقع تعریف مقدار دهی شود.
ولی اگر یک non-nullable variable از نوع class instance field باشد در شرایط خاصی میتوانیم آن را همان موقع تعریف مقدار دهی نکنیم.
معمولن class instance field ها را می خواهیم در constructor مقدار دهی کنیم ولی آنالایزر باید این امکان را داشته باشد که بفهمد که آیا non-nullable variable تعریف شده, مقدار دهی شد یا خیر؟
در واقع وقتی که یک Local variable تعریف میکینم چون که آنالایزر باید یک سطح کوچک از کد (مثلن بدنه یک function) را بررسی کند و اطمینان حاصل کند که non-nullable variable تعریف شده قبل از اولین استفاده مقدار دهی شده است یا خیر. بررسی و آنالیز این سطح کوچک برای آنالایزر کار دشواری نیست و به همین دلیل به ما اجازه می دهد که در همان ابتدای تعریف non-nullable variable آن را مقدار دهی نکنیم.
ولی در مورد Global variable ها یا class instance field ها چون که آنالایزر باید یک سطح بزرگ از کد را بررسی کند, این عمل راحت نیست و به همین دلیل وقتی non-nullable variable هایی داخل این Scope ها تعریف میکنید شما را اجبار میکند که در همان ابتدا آن رو مقدار دهی کنید.
ولی در مورد class instance field ها میتوانیم دو حالت استثنا را در نظر بگیریم و در آن موارد اجازه داریم که non-nullable variable را همان اول مقدار دهی نکنیم.
قبل از گفتن این دو مورد استثنا باید بگوییم که سه روش برای initialize کردن یک instance field داخل constructor وجود دارد.
- روش اول: استفاده از this در آرگومان ها ورودی سازنده.
?این قسمت توسط آنالایزر قابل رصد شدن هست و آنالایزر میتواند آرگومان های ورودی سازنده های کلاس ها را چک کند.
روش دوم : استفاده از لیست مقدار دهی(initialize list).
?این قسمت هم توسط آنالایزر قابل رصد هست.
- روش سوم : استفاده از constructor body.
?این قسمت یعنی constructor body توسط آنالایزر قابل رصد نیست.
پس با توجه به موارد بالا میتوان گفت که میتوانیم یک instance field از نوع non-nullable تعریف کنیم و آن را همان ابتدا مقداردهی نکنیم به شرط اینکه از روش های اول یا دوم برای مقدار دهی آن استفاده کنیم.
روش اول :
روش دوم :
ولی در صورت استفاده از روش سوم آنالایزر خطا میدهد:
ولی در صورتی که خواستید :
- یک non-nullable variable را داخل Global Scope تعریف کنید و آن را همان ابتدای تعریف مقدار دهی نکنید.
- یا یک non-nullable class instance field تعریف کنید و آن را همان ابتدا یا با استفاده از یکی از دو روش بالا مقداردهی نکنید.
می توانید از کلمه کلیدی late استفاده کنید.
مثال :
یک non-nullable variable در Global Scope که همان ابتدا مقدار دهی نشده است و آنالایزر هم هیچ خطایی اعلام نکرده است.
یک non-nullable instance field داخل یک class که همان ابتدا مقدار دهی نشده است و آنالایزر هم هیچ خطایی اعلام نکرده است.
و اما کلمه کلیدی late چه کار میکند؟
تا این جا دیدیم که بعد از اضافه شدن قابلیت null safety به Dart یک سری از خطاهایی که قبلن در زمان اجرای کد(runtime) دریافت میکردیم به زمان compile منتقل شدند و همان موقعی که در حال کدنویسی هستیم آنالایزر به ما هشدار میدهد.
ولی کلمه کلیدی late به compiler میگوید که دیگر با این variable در زمان compile کد کاری نداشته باش و در واقع برنامه نویس به compiler اعلام میکند که من خودم مدیریت این variable و مقداردهی آن را به عهده میگیرم و لازم نیست که تو در این مورد دخالت کنی.
پس با استفاده کردن از late دیگر compiler کاری به مقداردهی یا عدم مقداردهی این non-nullable variable ندارد و مدیریت آن را به عهده برنامه نویس میگذارد.
حال با این تفاصیر دو اتفاق میوفتد :
- دیگر آنالایزر در مورد این variable و عدم مقدار دهی آن به ما اعلام خطا نمیکند.
- چون مدیریت مقداردهی این variable به عهده برنامه نویس هست اگر برنامه نویس آن را مقدار دهی نکند و از آن در جایی استفاده کند, موقع اجرای کد (runtime) خطا رخ میدهد.
مثال :
میبینید که یک non-nullable global variable با استفاده از late تعریف کردم و بدون مقدار دهی از آن استفاده کردم ولی آنالایزر هیچ خطایی مبنی بر اینکه این variable را مقداردهی نکرده ای اعلام نکرد (به دلیل استفاده از late).
پس در زمان نوشتن کد و compile هیچ خطایی نداریم.ولی اگر کد را اجرا کنیم :
همان طور که میبینید یک خطای زمان اجرا یا runtime error دریافت میکنیم.
مثال :
در یک class یک non-nullable field را با late تعریف کردم و بدون مقدار از آن استفاده کردم ولی به دلیل استفاده از late هیچ خطایی در زمان compile نداریم.ولی با اجرای کد :
ولی اگر همین variable را در بدنه constructor مقدار دهی کنم مشکل برطرف میشود :
نکته خیلی مهم :
نیازی نیست برای Local variable از late استفاده کنید.
همچنین نیازی نیست برای nullable variable ها از late استفاده کنید چون در صورت عدم مقداردهی, مقدار پیش فرض یعنی null را میگیرند.
استفاده از late فقط برای non-nullable global variable که میخواهید همان ابتدای تعریف آن را مقدار دهی نکنید یا non-nullable class field که می خواهید ان را همان ابتدای تعریف یا با یکی از دو روش گفته شده برای مقدار دهی در constructor مقداردهی نکنید, مناسب است.
این نکته را هم باید اضافه کنم که نکاتی که در مورد class instance field ها گفتیم در مورد static field های یک class هم صادق است برای تعریف یک non-nullable static field داخل یک class میتوان از late استفاده کرده و مقدار دهی را به بعدن موکول کرد.
و اما نکته پایانی در مورد final variable ها است.تا قبل از null safety اگر یک final variable تعریف میکردیم باید آن را همان ابتدا مقدار دهی میکردیم یا در مورد class instance field باید آن را به یکی از دو روش گفته شده که توسط آنالایزر قابل تحلیل است مقداردهی میکردیم.
مثال :
در مثال بالا یک global variable داریم که از نوع nullable است پس تا اینجا نیازی نیست که آن را همان ابتدا مقدار دهی کنیم چون که میتواند مقدار پیش فرض null را بگیرد. همچنین نیازی به استفاده از کلمه late نیست.
اما به دلیل اینکه این variable از نوع final است باید همان ابتدای تعریف مقدار دهی شود و به همین دلیل آنالایزر خطا میدهد.
ولی میتوان با استفاده از late این مشکل را حل کرد :
پس از کلمه کلیدی late را علاوه بر کاربرد های قبلی میتوان :
- برای final variable هایی که در Global Scope تعریف می شوند و نمیخواهیم آن ها را همان ابتدا مقدار دهی کنیم استفاده کرد.
- برای class instance field هایی که به صورت final تعریف میشوند و نمیخواهیم آن ها را همان ابتدا یا با استفاده از دو روش مقدار دهی در constructor که توسط آنالایزر قابل تحلیل است مقدار دهی کنیم.
استفاده کرد.
عرایض بنده تمام شد و از دادگاه محترم و حاضرین در جلسه تقاضا دارم در صورتی که سوال یا ابهامی دارند مطرج کنند.
با توجه به توضیحات کامل Dart کسی از حاضرین سوالی نبپرسی و همه قانع شدن و دادگاه هم اولین اتهام رو از Dart رد کرد.
خب جلسه اول دادگاه به این صورت به پایان رسید و مثل اینکه Dart خیلی خوب تونست اتهامات وارد شده را از خودش رد کنه. ولی هنوز جلسات دیگه ای برای بررسی سایر اتهامات باقی مونده که داستان اون ها رو توی قسمت های بعدی با هم میخونیم.
ولی حالا من خلاصه مواردی که Dart گفت رو یه بار دیگه براتون میگم :
خلاصه Scope :
- با استفاده از هر { } یک Scope جدید ایجاد میشه مثل تعریف یه class یا یه function یا یه if و ... .
- اون variable هایی که داخل بدنه اصلی برنامه هستن رو بهشون میگیم Global .
- اون هایی که داخل function یا if و ... تعریف میشن رو میگیم Local.
- اون هایی که داخل class ها هستن رو میگیم class fields.
- هر Scope فقط و فقط به variable هایی که داخل خودش و Scope های اجدادش تعریف شدن دسترسی داره و به variable های Scope های نوادگانش دسترسی نداره.
خلاصه nullable variables:
این ها چون که میتونن مقدار null رو بگیرن.
توی هر Scope ای که تعریف بشن میتونن همون اول مقدار دهی نشن و در این صورت مقدار پیش فرض null رو میگیرن.
این مورد یک استثنا داره و اون هم این که این variable خودش final باشه که توی بخش final میگم.
خلاصه non-nullable variables:
این ها نمیتونن مقدار null رو بگیرن.
- حالا اگه به صورت Local تعریف شدن میتونی همون اول مقدارشون ندی و خود کامپایلر یا آنالایزر در صورت لزروم بهت هشدار میده(توی اولین استفاده بررسی میکنه که آیا مقدار دارن یا نه و اگه نداشتن بهت خطا میده (compile time error))
- اگه به صورت Global تعریف کردی مجبوری همون اول مقدارشون بدی.
- اگه یه class field بودن باید یا همون اول مقدارشون بدی یا با استفاده از دو روش مقدار دهی توی constructor که compiler میتونه رویتش کنه.یعنی با استفاده از this یا initialize list.
خلاصه final variables :
این ها چه nullable باشن و چه non-nullable اگه :
- به صورت Local تعریف شدن میتونی همون اول مقدارشون ندی و خود کامپایلر یا آنالایزر در صورت لزروم بهت هشدار میده(توی اولین استفاده بررسی میکنه که ایا مقدار دارن یا نه و اگه نداشتن بهت خطا میده (compile time error))
- اگه به صورت Global تعریف کردی مجبوری همون اول مقدارشون بدی.
- اگه یه class field بودن باید یا همون اول مقدارشون بدی یا با استفاده از دو روش مقدار دهی توی constructor که compiler میتونه رویتش کنه.یعنی با استفاده از this یا initialize list.
خلاصه late :
- از این کلمه کلیدی فقط برای Global variable ها یا class field ها استفاده کن.
- برای Local variable ها نیازی نیست که ازش استفاده کنی.
شرط استفاده ازش هم اینه که :
- یه Global variable ای داشته باشی که مجبور باشی همون ابتدای تعریف مقدارش بدی یعنی یا non-nullable باشه و یا اینکه final باشه.
- یه class field داشته باشی که مجبور باشی همون اول یا با استفاده از دو روش مقدار دهی در constructor مقدارش بدی یعنی یا non-nullable باشه یا final.
و به هر دلیلی نخوای که توی این شرایط بهش مقدار بدی.
در این صورت از late استفاده کن و با این کار به compiler میگی که برو وایسا کنار و دخالت نکن و compiler هم گوش میکنه و میگه من میرم کنار و کاری به کارت ندارم ولی مسیولیتش با خودت و اگه اینو مقدار ندادی و ازش استفاده کردی موقع اجرای برنامه خطا میگیری و آبروت جلوی کاربر میره.
پس اگه از late استفاده کردی مراقب باش که آبرو ریزی نشه.
درواقع با استفاده از late قابلیت compile time error رو از دست میدیم و موقع کدنویسی هیچ error ای دریافت نمیکنیم و در صورت بی دقتی موقع اجرا خطا میگیریم(runtime)
حالا در پایان این قسمت یه چالش مطرح میکنم و ازت میخوام که با دلیل و منطق و تحلیل درست و حسابی اون رو حلش کنی و جوابش رو بدی...
کد بالا چرا خطا داره و چکار کنیم که درست شه؟
امیدوارم که لذت برده باشی و کلی نکته جدید یاد گرفته باشی.
لایک یادت نره....
تا قسمت بعدی و ادامه ماجرای های null در Dart خدافظ.
مطلبی دیگر از این انتشارات
داستان Null در دارت (نال فرزند ناخلف)
مطلبی دیگر از این انتشارات
Dart Const (const constructor)
مطلبی دیگر از این انتشارات
Git dependencies in Flutter pubspec