برنامه نویسی

خطا به عنوان مقدار در مقابل استثناها

Summarize this content to 400 words in Persian Lang
در 20 سال گذشته، استثناها روش غالب برای رسیدگی به خطاها در برنامه نویسی بوده اند. جاوا، سی شارپ، سی پلاس پلاس، پایتون و بسیاری دیگر این ویژگی را ارائه می دهند و مفهوم در همه آنها تقریباً مشابه است. با ورود زبان های جدیدتر مانند Go و Rust به مرحله، ما شاهد ظهور روش متفاوتی برای مدیریت خطا هستیم: خطاها به عنوان مقادیر. در این مقاله، ما قصد داریم نگاهی دقیق تر به این پارادایم جدید داشته باشیم و نظرات خود را در مورد آن به شما ارائه خواهم کرد. لطفا توجه داشته باشید که این قطعه متن یک نظر و نه یک حقیقت جهانی، و ممکن است در طول زمان تغییر کند.

“خطا به عنوان مقدار” چیست؟

ایده اصلی پشت الگوی Error as Value این است که هر زمان که این تابع با وضعیت نامعتبر مواجه شد، مقادیر خطای اختصاصی را از یک تابع برگرداند. برخلاف گردش کار استثنا، Error as Value پیشنهاد می کند که خطا را به صورت a برگردانید نتیجه از تابع به این ترتیب، خطا بخشی از امضای تابع می شود که منجر به دو اثر عمده می شود:

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

جزئیات اجرای این الگو متفاوت است. به عنوان مثال، توابع Go اغلب جفت ها را برمی گرداند – اولین ورودی جفت نتیجه واقعی است (در صورت وجود) در حالی که ورودی دوم خطای رخ داده است (در صورت وجود):

f, err := os.Open(“filename.ext”)
if err != nil {
log.Fatal(err)
}
// do something with the open *File f

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کامپایلر Go به شما اجازه نمی دهد که متغیرهای استفاده نشده داشته باشید، بنابراین مجبور هستید آن را بررسی کنید err ارزش.

در Rust، همه چیز کمی متفاوت است، اما در نهایت به یک اثر. زنگ یک ژنریک دارد Result enum که دو پیاده سازی مشخص دارد: Ok و Err. هر تابعی که a را برمی گرداند Result بنابراین می تواند یک نتیجه واقعی یا یک خطا ایجاد کند و تماس گیرنده باید بین این دو تفاوت قائل شود:

let fileResult = File::open(“hello.txt”);

let f = match fileResult {
Ok(file) => file,
Err(error) => panic!(“Problem opening the file: {error:?}”),
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مزایای خطا به عنوان ارزش

طرفداران این رویکرد اغلب به مزایای زیر اشاره می کنند:

خطاها به بخشی از امضای تابع تبدیل می شوند
کامپایلر برنامه نویس را مجبور می کند تا با خطاها مقابله کند
خطاها به عنوان مقادیر دارای عملکرد برتر نسبت به استثناها هستند
خطاها به عنوان مقادیر جریان کنترل واضح تری را ارائه می دهند

افکار من در مورد خطا به عنوان ارزش

لطفاً توجه داشته باشید که هر آنچه در ادامه می آید نظر آگاهانه من به عنوان یک توسعه دهنده سمت سرور و به تنهایی است. این یک برداشت داغ است و ممکن است به شدت با آن مخالفت کنید.

خطاها نباید اتفاق بیفتد.

این استدلال که خطا به عنوان ارزش برای عملکرد بهتر است، در حالی که از نظر فنی درست است1، در عمل برای من اهمیت صفر دارد. اگر برنامه ما به دلایلی در حالت خطا قرار گیرد، ما قبلاً از مسابقه عملکرد محروم شده ایم، زیرا برنامه ما از مسیر خارج شده است. دلیلی برای بهینه سازی عملکرد در حالت خطا وجود ندارد زیرا در ابتدا باید به ندرت رخ دهند.

اگر برنامه شما از خطاهایی برای جریان کنترل منظم استفاده می کند، در وهله اول کار اشتباهی انجام داده اید.

خطاهای قابل بازیابی واقعی یک افسانه است.

من اغلب، در زمینه های مختلف، تمایز بین “خطای قابل جبران” و “خطای غیر قابل جبران” را شنیده ام. برای من، این همیشه عجیب و مصنوعی بوده است. فلسفه در جاوا اولیه این بود که استثناهای بررسی شده باید برای خطاهای قابل بازیابی استفاده شوند. با این تعریف، چگونه درخواست API من از یک بازیابی می شود SQLException که به من می گوید که commit من نمی تواند ادامه یابد زیرا یک محدودیت داده در سرور پایگاه داده را نقض می کند؟ این نمی شود. بهترین کاری که می‌توانیم انجام دهیم این است که آن را لاگین کرده و به یک جمع‌آورنده مرکزی خطا ارسال کنیم تا با تغییر کد منبع، مشکل برطرف شود. را برنامه (یعنی سرور به عنوان یک کل) ممکن است “بازیابی” شود (یعنی خطا در پردازش درخواست واحد کل سرور را خاتمه نمی دهد) اما درخواست در این مرحله قابل نجات نیست تلاش برای مقابله با یک SQLException در سطح روشی که فراخوانی کرد commit() بنابراین بیهوده است، زیرا راه دیگری برای ادامه ندارد.

جاوا اولین زبانی بود که “استثناهای بررسی شده” را معرفی کرد که مجبورید:

یا به صراحت در امضای متد خود اعلام کنید
یا مجبور هستند catch و در خود روش پردازش کنید

تا به امروز، جاوا است فقط زبانی که می دانم این مفهوم را دارد. C# آن را به ارث نبرد و حتی بیگانگان JVM مانند Groovy و Kotlin آن را به طور کامل کنار گذاشتند. این واقعیت که هر چارچوب یا کتابخانه مدرن جاوا نیز استثناهای بررسی شده را به نفع موارد بررسی نشده کنار می گذارد باید به شما بگوید: استثناهای بررسی شده (در حالی که در مقالات تحقیقاتی و گفتگوهای کنفرانس خوب بود) ایده واقعاً غیرعملی بود.

برای Rust and Go، Error as Value برای «خطاهای قابل بازیابی» و وحشت (که در ادامه به آن خواهیم پرداخت) برای خطاهای غیرقابل جبران توصیه می‌شود. آیا الگو را می بینید؟ این استثناها را دوباره بررسی می کند.

مغالطه تابع unfailable

متدولوژی Values ​​as Errors خطاها را در امضای تابع کدگذاری می کند. با این حال، اگر به کتابخانه های استاندارد Go و Rust نگاه کنید، همه توابع در این متدولوژی دارای خطا در امضای خود نیستند. این بدان معناست که توابعی وجود دارد که نمی تواند شکست بخورد.

در یک دنیای کامل، ما قادر خواهیم بود توابعی بنویسیم که هرگز نمی توانند از کار بیفتند. با این حال، در عمل چنین نیست. این موارد را در نظر بگیرید:

یک جمع صحیح ساده a + b اگر مقادیر به اندازه کافی بالا باشند تا از محدوده اعداد صحیح قابل نمایش فراتر بروند، می توانند شکست بخورند.
شما در حال فراخوانی تابع دیگری در تابع خود هستید. این تماس اضافی ممکن است از حداکثر اندازه پشته پلتفرمی که روی آن اجرا می‌کنید بیشتر باشد. این برنامه شما را از کار می‌اندازد، اما خود عملکرد “اشتباهی” انجام نداد.
شما در حال ارسال داده از طریق شبکه هستید، اما درست در وسط آن، اتصال قطع می شود. شاید شما در یک شبکه تلفن همراه هستید و وارد منطقه ای شده اید که دریافت آن بد است. شاید کسی کابل اترنت را از نظر فیزیکی جدا کرده باشد. هیچ کدام از اینها تقصیر برنامه نویس نیست، اما باعث از کار افتادن یک تابع در برنامه می شود.
تو زنگ میزنی malloc (هر برنامه غیر پیش پاافتاده ای این کار را انجام می دهد) و در زمان اجرا هیچ حافظه ای برای شما باقی نمانده است.

پیام کلیدی در اینجا این است: برنامه شما اراده در مکان‌های دلخواه، به دلایل دلخواه، مهم نیست که چقدر خوب مهندسی شده باشد، شکست بخورد. خارج از مرزهای دانشگاهی، هیچ راهی برای محافظت از ما در برابر آن وجود ندارد. چگونه ما معامله با این موقعیت ها سوال کلیدی است.

برگردیم به Rust and Go، اگر در مورد Error as Value جدی هستیم، پس هر تابع باید خروجی خطا داشته باشد، از محاسبات ساده گرفته تا داده های پایگاه داده از طریق شبکه. نیازی به گفتن نیست که این بسیار غیر عملی است (به همین دلیل است که Rust and Go با وحشت می کند; ما در یک لحظه در مورد آنها صحبت خواهیم کرد).

نتیجه ای که من از این موضوع می گیرم عبارتند از:

هر تابع می توان و شکست خواهد خورد، مهم نیست که کدام وظیفه را انجام دهد.
ما نمی‌توانیم همه دلایل مختلف شکست آن را برشماریم و تلاش بیهوده است.
تلاش برای رمزگذاری خطاها به عنوان بخشی از امضاهای تابع، در عین شجاعت، غیرعملی و در نهایت بیهوده است.

این مزیت‌های «جریان کنترل واضح‌تر» (اینطور نیست) و «شامل اشتباهات در امضا» را از بین می‌برد (حداقل نمی‌توانید، نه به طور کامل).

وحشت نکنید

Rust and Go، جدای از Error as Values، “حالت شکست” دیگری نیز دارد که به آن “panic” می گویند. هنگامی که یک وحشت رخ می دهد، کد دارای جریان کنترل عادی است، از تابع فعلی خارج می شود، تابع فراخوانی وجود دارد، و غیره، تا زمانی که برنامه به طور کلی خاتمه یابد. یا (و این قسمت سرگرم کننده است) به یک می رسد مرز خطا. در Rust به این می گویند catch_unwind، در برو نامیده می شود recover.

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

Rust and Go استثناهایی دارد. آنها فقط به شما نمی گویند.

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

شایستگی تلاش برای گرفتن

استثناها و try-catch بلوک ها محاسن زیادی دارند که افراد تمایل دارند آنها را فراموش کنند:

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

نکته آخر بسیار مهم است. مطمئناً در Rust می توانید از آن استفاده کنید ? عملگر روی هر تابعی که a را برمی گرداند Result و در صورت وجود خطا از تابع فعلی خارج می شود، در غیر این صورت نتیجه ساده را به شما می دهد. جالب است، اما… اگر در نهایت تصمیم گرفتید به یک خطا نگاه کنید، هیچ سرنخی نخواهید داشت که از کجا آمده است. ردیابی این موضوع می تواند واقعا سخت باشد. بله، راه‌هایی در Rust وجود دارد که می‌توان اطلاعات ردیابی را به صورت دستی به اشیاء خطا پیوست کرد، اما در این مرحله من احساس می‌کنم که Rust فقط تلاش می‌کند مشکلاتی را که برای خودش ایجاد کرده برطرف کند. در Go، تا آنجا که من می دانم چنین چیزی وجود ندارد. این است if err != nil در تمام راه به وفور

معایب واقعی استثناها

از نظر من، چند مشکل با استثنا وجود دارد:

استثناها را می توان نادیده گرفت. تابع کتابخانه ای که شما فراخوانی می کنید ممکن است یک استثنا ایجاد کند، و شما به عنوان یک برنامه نویس از آن آگاه نیستید.
هیچ چیز شما را مجبور نمی کند در نهایت با استثناها برخورد کنید
استثناها و try-catch می تواند برای جریان کنترل منظم مورد سوء استفاده قرار گیرد.

مطمئناً، هر سه این موارد توسط Errors as Values ​​حل می شوند. اما برای من بیشتر از اینکه حل کند مشکلات ایجاد می کند. مسائلی که در بالا به آن اشاره کردم، مربوط به رشته برنامه نویس و کد پاک است. من متوجه شدم که ممکن است شبیه یک برنامه نویس C باشد که در مورد آن صحبت می کند malloc و free زمانی که اولین زباله جمع کن ها معرفی شدند. اما Error as Value آنقدر به ساختار کد تهاجمی است که نمی توانم تصور کنم که با آن به شکل جدی کار کنم.

خطا به عنوان مقدار در Kotlin

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

fun String.toIntOrNull(): Int?

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کتابخانه استاندارد در کاتلین a را تعریف می کند toIntOrNull() روش برای String. رشته را تجزیه می کند و اگر قابل تجزیه به عدد صحیح باشد، عدد صحیح برگردانده می شود. در غیر این صورت، null بازگردانده خواهد شد. همانطور که کاتلین یک است nullزبان امن، برنامه نویس است مجبور شد برای رسیدگی به null مورد جداگانه این بسیار شبیه به Error as Value است، اما خطا به مقدار محدود شده است null ارزش. نکته منفی این است که مورد خطا نمی تواند اطلاعات معنایی داشته باشد (چرا تجزیه شکست خورد؟ در کدام موقعیت؟). مزیت آن این است که از نظر نحوی بسیار دلپذیر و بسیار واضح است. به عنوان یک توسعه دهنده که این API را فراخوانی می کند، نمی توانید از آن به روش اشتباه استفاده کنید (کامپایلر به شما اجازه نمی دهد) و نمی توانید چیزی را فراموش کنید. من نسبتاً به این رویکرد علاقه مند شده ام. یک اخطار این است که فقط برای روش هایی کار می کند که دارای مقادیر بازگشتی واقعی هستند و این null نباید یک مقدار بازگشتی “عادی” برای متد باشد (در غیر این صورت نمی توانید بین آنها تمایز قائل شوید null مانند “اوه که خطا است” یا null به عنوان یک مقدار برگشتی معمولی بدون خطا).

نتیجه

این تمام چیزی است که امروز باید بگویم. امیدوارم برای شما جالب بوده باشد، این فقط موضوعی است که مدت زیادی است در ذهن من بوده است. و حتی با وجود اینکه در حال حاضر کاملاً روی «استثناهای تیمی» هستم، این احتمال را رد نمی‌کنم که اگر استدلال‌ها به اندازه کافی قانع‌کننده باشند، نظرم در آینده تغییر کند. و من فکر می کنم ما حداقل می توانیم توافق کنیم که هر دو راه حل بهتر از این هستند در سی.

استثناها در مقایسه با جریان کنترل معمولی بسیار کند هستند. دلیل اصلی آن این است که بیشتر زبان‌هایی که از استثناها استفاده می‌کنند، یک stack trace را در استثناء قرار می‌دهند و جمع‌آوری این اطلاعات پرهزینه است. ↩

در 20 سال گذشته، استثناها روش غالب برای رسیدگی به خطاها در برنامه نویسی بوده اند. جاوا، سی شارپ، سی پلاس پلاس، پایتون و بسیاری دیگر این ویژگی را ارائه می دهند و مفهوم در همه آنها تقریباً مشابه است. با ورود زبان های جدیدتر مانند Go و Rust به مرحله، ما شاهد ظهور روش متفاوتی برای مدیریت خطا هستیم: خطاها به عنوان مقادیر. در این مقاله، ما قصد داریم نگاهی دقیق تر به این پارادایم جدید داشته باشیم و نظرات خود را در مورد آن به شما ارائه خواهم کرد. لطفا توجه داشته باشید که این قطعه متن یک نظر و نه یک حقیقت جهانی، و ممکن است در طول زمان تغییر کند.

“خطا به عنوان مقدار” چیست؟

ایده اصلی پشت الگوی Error as Value این است که هر زمان که این تابع با وضعیت نامعتبر مواجه شد، مقادیر خطای اختصاصی را از یک تابع برگرداند. برخلاف گردش کار استثنا، Error as Value پیشنهاد می کند که خطا را به صورت a برگردانید نتیجه از تابع به این ترتیب، خطا بخشی از امضای تابع می شود که منجر به دو اثر عمده می شود:

  • همه کسانی که این روش را فراخوانی می کنند در نگاه اول متوجه خواهند شد که می تواند خطا ایجاد کند

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

جزئیات اجرای این الگو متفاوت است. به عنوان مثال، توابع Go اغلب جفت ها را برمی گرداند – اولین ورودی جفت نتیجه واقعی است (در صورت وجود) در حالی که ورودی دوم خطای رخ داده است (در صورت وجود):

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کامپایلر Go به شما اجازه نمی دهد که متغیرهای استفاده نشده داشته باشید، بنابراین مجبور هستید آن را بررسی کنید err ارزش.

در Rust، همه چیز کمی متفاوت است، اما در نهایت به یک اثر. زنگ یک ژنریک دارد Result enum که دو پیاده سازی مشخص دارد: Ok و Err. هر تابعی که a را برمی گرداند Result بنابراین می تواند یک نتیجه واقعی یا یک خطا ایجاد کند و تماس گیرنده باید بین این دو تفاوت قائل شود:

    let fileResult = File::open("hello.txt");

    let f = match fileResult  {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {error:?}"),
    };
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

مزایای خطا به عنوان ارزش

طرفداران این رویکرد اغلب به مزایای زیر اشاره می کنند:

  1. خطاها به بخشی از امضای تابع تبدیل می شوند

  2. کامپایلر برنامه نویس را مجبور می کند تا با خطاها مقابله کند

  3. خطاها به عنوان مقادیر دارای عملکرد برتر نسبت به استثناها هستند

  4. خطاها به عنوان مقادیر جریان کنترل واضح تری را ارائه می دهند

افکار من در مورد خطا به عنوان ارزش

لطفاً توجه داشته باشید که هر آنچه در ادامه می آید نظر آگاهانه من به عنوان یک توسعه دهنده سمت سرور و به تنهایی است. این یک برداشت داغ است و ممکن است به شدت با آن مخالفت کنید.

خطاها نباید اتفاق بیفتد.

این استدلال که خطا به عنوان ارزش برای عملکرد بهتر است، در حالی که از نظر فنی درست است1، در عمل برای من اهمیت صفر دارد. اگر برنامه ما به دلایلی در حالت خطا قرار گیرد، ما قبلاً از مسابقه عملکرد محروم شده ایم، زیرا برنامه ما از مسیر خارج شده است. دلیلی برای بهینه سازی عملکرد در حالت خطا وجود ندارد زیرا در ابتدا باید به ندرت رخ دهند.

اگر برنامه شما از خطاهایی برای جریان کنترل منظم استفاده می کند، در وهله اول کار اشتباهی انجام داده اید.

خطاهای قابل بازیابی واقعی یک افسانه است.

من اغلب، در زمینه های مختلف، تمایز بین “خطای قابل جبران” و “خطای غیر قابل جبران” را شنیده ام. برای من، این همیشه عجیب و مصنوعی بوده است. فلسفه در جاوا اولیه این بود که استثناهای بررسی شده باید برای خطاهای قابل بازیابی استفاده شوند. با این تعریف، چگونه درخواست API من از یک بازیابی می شود SQLException که به من می گوید که commit من نمی تواند ادامه یابد زیرا یک محدودیت داده در سرور پایگاه داده را نقض می کند؟ این نمی شود. بهترین کاری که می‌توانیم انجام دهیم این است که آن را لاگین کرده و به یک جمع‌آورنده مرکزی خطا ارسال کنیم تا با تغییر کد منبع، مشکل برطرف شود. را برنامه (یعنی سرور به عنوان یک کل) ممکن است “بازیابی” شود (یعنی خطا در پردازش درخواست واحد کل سرور را خاتمه نمی دهد) اما درخواست در این مرحله قابل نجات نیست تلاش برای مقابله با یک SQLException در سطح روشی که فراخوانی کرد commit() بنابراین بیهوده است، زیرا راه دیگری برای ادامه ندارد.

جاوا اولین زبانی بود که “استثناهای بررسی شده” را معرفی کرد که مجبورید:

  • یا به صراحت در امضای متد خود اعلام کنید
  • یا مجبور هستند catch و در خود روش پردازش کنید

تا به امروز، جاوا است فقط زبانی که می دانم این مفهوم را دارد. C# آن را به ارث نبرد و حتی بیگانگان JVM مانند Groovy و Kotlin آن را به طور کامل کنار گذاشتند. این واقعیت که هر چارچوب یا کتابخانه مدرن جاوا نیز استثناهای بررسی شده را به نفع موارد بررسی نشده کنار می گذارد باید به شما بگوید: استثناهای بررسی شده (در حالی که در مقالات تحقیقاتی و گفتگوهای کنفرانس خوب بود) ایده واقعاً غیرعملی بود.

برای Rust and Go، Error as Value برای «خطاهای قابل بازیابی» و وحشت (که در ادامه به آن خواهیم پرداخت) برای خطاهای غیرقابل جبران توصیه می‌شود. آیا الگو را می بینید؟ این استثناها را دوباره بررسی می کند.

مغالطه تابع unfailable

متدولوژی Values ​​as Errors خطاها را در امضای تابع کدگذاری می کند. با این حال، اگر به کتابخانه های استاندارد Go و Rust نگاه کنید، همه توابع در این متدولوژی دارای خطا در امضای خود نیستند. این بدان معناست که توابعی وجود دارد که نمی تواند شکست بخورد.

در یک دنیای کامل، ما قادر خواهیم بود توابعی بنویسیم که هرگز نمی توانند از کار بیفتند. با این حال، در عمل چنین نیست. این موارد را در نظر بگیرید:

  • یک جمع صحیح ساده a + b اگر مقادیر به اندازه کافی بالا باشند تا از محدوده اعداد صحیح قابل نمایش فراتر بروند، می توانند شکست بخورند.

  • شما در حال فراخوانی تابع دیگری در تابع خود هستید. این تماس اضافی ممکن است از حداکثر اندازه پشته پلتفرمی که روی آن اجرا می‌کنید بیشتر باشد. این برنامه شما را از کار می‌اندازد، اما خود عملکرد “اشتباهی” انجام نداد.

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

  • تو زنگ میزنی malloc (هر برنامه غیر پیش پاافتاده ای این کار را انجام می دهد) و در زمان اجرا هیچ حافظه ای برای شما باقی نمانده است.

پیام کلیدی در اینجا این است: برنامه شما اراده در مکان‌های دلخواه، به دلایل دلخواه، مهم نیست که چقدر خوب مهندسی شده باشد، شکست بخورد. خارج از مرزهای دانشگاهی، هیچ راهی برای محافظت از ما در برابر آن وجود ندارد. چگونه ما معامله با این موقعیت ها سوال کلیدی است.

برگردیم به Rust and Go، اگر در مورد Error as Value جدی هستیم، پس هر تابع باید خروجی خطا داشته باشد، از محاسبات ساده گرفته تا داده های پایگاه داده از طریق شبکه. نیازی به گفتن نیست که این بسیار غیر عملی است (به همین دلیل است که Rust and Go با وحشت می کند; ما در یک لحظه در مورد آنها صحبت خواهیم کرد).

نتیجه ای که من از این موضوع می گیرم عبارتند از:

  • هر تابع می توان و شکست خواهد خورد، مهم نیست که کدام وظیفه را انجام دهد.
  • ما نمی‌توانیم همه دلایل مختلف شکست آن را برشماریم و تلاش بیهوده است.
  • تلاش برای رمزگذاری خطاها به عنوان بخشی از امضاهای تابع، در عین شجاعت، غیرعملی و در نهایت بیهوده است.

این مزیت‌های «جریان کنترل واضح‌تر» (اینطور نیست) و «شامل اشتباهات در امضا» را از بین می‌برد (حداقل نمی‌توانید، نه به طور کامل).

وحشت نکنید

Rust and Go، جدای از Error as Values، “حالت شکست” دیگری نیز دارد که به آن “panic” می گویند. هنگامی که یک وحشت رخ می دهد، کد دارای جریان کنترل عادی است، از تابع فعلی خارج می شود، تابع فراخوانی وجود دارد، و غیره، تا زمانی که برنامه به طور کلی خاتمه یابد. یا (و این قسمت سرگرم کننده است) به یک می رسد مرز خطا. در Rust به این می گویند catch_unwind، در برو نامیده می شود recover.

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

Rust and Go استثناهایی دارد. آنها فقط به شما نمی گویند.

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

شایستگی تلاش برای گرفتن

استثناها و try-catch بلوک ها محاسن زیادی دارند که افراد تمایل دارند آنها را فراموش کنند:

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

نکته آخر بسیار مهم است. مطمئناً در Rust می توانید از آن استفاده کنید ? عملگر روی هر تابعی که a را برمی گرداند Result و در صورت وجود خطا از تابع فعلی خارج می شود، در غیر این صورت نتیجه ساده را به شما می دهد. جالب است، اما… اگر در نهایت تصمیم گرفتید به یک خطا نگاه کنید، هیچ سرنخی نخواهید داشت که از کجا آمده است. ردیابی این موضوع می تواند واقعا سخت باشد. بله، راه‌هایی در Rust وجود دارد که می‌توان اطلاعات ردیابی را به صورت دستی به اشیاء خطا پیوست کرد، اما در این مرحله من احساس می‌کنم که Rust فقط تلاش می‌کند مشکلاتی را که برای خودش ایجاد کرده برطرف کند. در Go، تا آنجا که من می دانم چنین چیزی وجود ندارد. این است if err != nil در تمام راه به وفور

معایب واقعی استثناها

از نظر من، چند مشکل با استثنا وجود دارد:

  1. استثناها را می توان نادیده گرفت. تابع کتابخانه ای که شما فراخوانی می کنید ممکن است یک استثنا ایجاد کند، و شما به عنوان یک برنامه نویس از آن آگاه نیستید.
  2. هیچ چیز شما را مجبور نمی کند در نهایت با استثناها برخورد کنید
  3. استثناها و try-catch می تواند برای جریان کنترل منظم مورد سوء استفاده قرار گیرد.

مطمئناً، هر سه این موارد توسط Errors as Values ​​حل می شوند. اما برای من بیشتر از اینکه حل کند مشکلات ایجاد می کند. مسائلی که در بالا به آن اشاره کردم، مربوط به رشته برنامه نویس و کد پاک است. من متوجه شدم که ممکن است شبیه یک برنامه نویس C باشد که در مورد آن صحبت می کند malloc و free زمانی که اولین زباله جمع کن ها معرفی شدند. اما Error as Value آنقدر به ساختار کد تهاجمی است که نمی توانم تصور کنم که با آن به شکل جدی کار کنم.

خطا به عنوان مقدار در Kotlin

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

fun String.toIntOrNull(): Int?
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کتابخانه استاندارد در کاتلین a را تعریف می کند toIntOrNull() روش برای String. رشته را تجزیه می کند و اگر قابل تجزیه به عدد صحیح باشد، عدد صحیح برگردانده می شود. در غیر این صورت، null بازگردانده خواهد شد. همانطور که کاتلین یک است nullزبان امن، برنامه نویس است مجبور شد برای رسیدگی به null مورد جداگانه این بسیار شبیه به Error as Value است، اما خطا به مقدار محدود شده است null ارزش. نکته منفی این است که مورد خطا نمی تواند اطلاعات معنایی داشته باشد (چرا تجزیه شکست خورد؟ در کدام موقعیت؟). مزیت آن این است که از نظر نحوی بسیار دلپذیر و بسیار واضح است. به عنوان یک توسعه دهنده که این API را فراخوانی می کند، نمی توانید از آن به روش اشتباه استفاده کنید (کامپایلر به شما اجازه نمی دهد) و نمی توانید چیزی را فراموش کنید. من نسبتاً به این رویکرد علاقه مند شده ام. یک اخطار این است که فقط برای روش هایی کار می کند که دارای مقادیر بازگشتی واقعی هستند و این null نباید یک مقدار بازگشتی “عادی” برای متد باشد (در غیر این صورت نمی توانید بین آنها تمایز قائل شوید null مانند “اوه که خطا است” یا null به عنوان یک مقدار برگشتی معمولی بدون خطا).

نتیجه

این تمام چیزی است که امروز باید بگویم. امیدوارم برای شما جالب بوده باشد، این فقط موضوعی است که مدت زیادی است در ذهن من بوده است. و حتی با وجود اینکه در حال حاضر کاملاً روی «استثناهای تیمی» هستم، این احتمال را رد نمی‌کنم که اگر استدلال‌ها به اندازه کافی قانع‌کننده باشند، نظرم در آینده تغییر کند. و من فکر می کنم ما حداقل می توانیم توافق کنیم که هر دو راه حل بهتر از این هستند در سی.


  1. استثناها در مقایسه با جریان کنترل معمولی بسیار کند هستند. دلیل اصلی آن این است که بیشتر زبان‌هایی که از استثناها استفاده می‌کنند، یک stack trace را در استثناء قرار می‌دهند و جمع‌آوری این اطلاعات پرهزینه است. ↩

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا