استثناءات Kotlin وميزاتها

تستخدم شركتنا Kotlin في الإنتاج لأكثر من عامين. شخصيا ، صادفت هذه اللغة منذ حوالي عام. هناك العديد من الموضوعات التي يجب التحدث عنها ، ولكن اليوم سنتحدث عن معالجة الأخطاء ، بما في ذلك بأسلوب وظيفي. سأخبرك عن كيفية القيام بذلك في Kotlin.

صورة

(صورة من الاجتماع حول هذا الموضوع ، الذي عقد في مكتب إحدى شركات Taganrog. تحدث أليكسي شافرانوف ، رئيس مجموعة العمل (Java) في Maxilekt)

كيف يمكنك التعامل مع الأخطاء من حيث المبدأ؟


لقد وجدت عدة طرق:

  • يمكنك استخدام بعض قيمة الإرجاع كمؤشر لحقيقة وجود خطأ ؛
  • يمكنك استخدام المعلمة المؤشر لنفس الغرض ،
  • أدخل متغير عالمي
  • التعامل مع الاستثناءات
  • إضافة العقود (DbC) .

دعنا نتحدث بمزيد من التفاصيل عن كل خيار من الخيارات.

قيمة الإرجاع


يتم إرجاع قيمة "سحرية" معينة في حالة حدوث خطأ. إذا سبق لك استخدام لغات البرمجة النصية ، فيجب أن تكون قد شاهدت تصميمات مماثلة.

مثال 1:

function sqrt(x) { if(x < 0) return -1; else return √x; } 

مثال 2:

 function getUser(id) { result = db.getUserById(id) if (result) return result as User else return “Can't find user ” + id } 

المعلمة المؤشر


يتم استخدام معلمة معينة تم تمريرها إلى الوظيفة. بعد إرجاع القيمة حسب المعلمة ، يمكنك معرفة ما إذا كان هناك خطأ داخل الوظيفة.

مثال:

 function divide(x,y,out Success) { if (y == 0) Success = false else Success = true return x/y } divide(10, 11, Success) id (!Success) //handle error 

متغير عالمي


المتغير العالمي يعمل بنفس الطريقة تقريبًا.

مثال:

 global Success = true function divide(x,y) { if (y == 0) Success = false else return x/y } divide(10, 11, Success) id (!Success) //handle error 

استثناءات


نحن جميعا معتادون على الاستثناءات. يتم استخدامها في كل مكان تقريبا.

مثال:

 function divide(x,y) { if (y == 0) throw Exception() else return x/y } try{ divide(10, 0)} catch (e) {//handle exception} 

العقود (DbC)


بصراحة ، أنا لم أر هذا النهج على الهواء مباشرة. من خلال googling الطويل ، وجدت أن لدى Kotlin 1.3 مكتبة تتيح بالفعل استخدام العقود. أي يمكنك ضبط الشرط على المتغيرات التي يتم تمريرها إلى الوظيفة ، الشرط على قيمة الإرجاع ، عدد المكالمات ، حيث يتم الاتصال من ، إلخ. وإذا تم استيفاء جميع الشروط ، يُعتقد أن الوظيفة تعمل بشكل صحيح.

مثال:

 function sqrt (x) pre-condition (x >= 0) post-condition (return >= 0) begin calculate sqrt from x end 

بصراحة ، تحتوي هذه المكتبة على بناء رهيب. ربما لهذا السبب لم أر مثل هذا الشيء على الهواء مباشرة.

استثناءات في Java


دعنا ننتقل إلى جافا وكيف عملت كل شيء من البداية.

صورة

عند تصميم لغة ، تم وضع نوعين من الاستثناءات:

  • فحص - فحص ؛
  • لم يتم التحقق منه - لم يتم التحقق منه.

ما هي الاستثناءات المحددة؟ من الناحية النظرية ، هناك حاجة لذلك يجب على الناس التحقق من الأخطاء. أي إذا كان هناك استثناء محدد ممكن ، فيجب التحقق منه لاحقًا. من الناحية النظرية ، ينبغي أن يكون هذا النهج أدى إلى عدم وجود أخطاء غير المجهزة وتحسين جودة التعليمات البرمجية. ولكن في الممارسة العملية هذا ليس كذلك. أعتقد أن الجميع شاهدوا مرة واحدة على الأقل في حياتهم كتلة صيد فارغة.

لماذا يمكن أن يكون هذا سيئا؟

فيما يلي مثال كلاسيكي مباشرةً من وثائق Kotlin - واجهة من JDK تم تنفيذها في StringBuilder:

 Appendable append(CharSequence csq) throws IOException; try { log.append(message) } catch (IOException e) { //Must be safe } 

أنا متأكد من أنك قد قابلت عددًا كبيرًا من التعليمات البرمجية ملفوفة في try-catch ، حيث يعتبر catch كتلة فارغة ، نظرًا لأن هذا الموقف لا ينبغي أن يحدث ببساطة ، وفقًا للمطور. في كثير من الحالات ، يتم تطبيق معالجة الاستثناءات المحددة بالطريقة التالية: إنها ببساطة تقوم بإلقاء RuntimeException والتقاطها في مكان ما أعلاه (أو عدم التقاطها ...).

 try { // do something } catch (IOException e) { throw new RuntimeException(e); //  - ... 

ما هو ممكن في Kotlin


من حيث الاستثناءات ، يختلف برنامج التحويل البرمجي Kotlin في ذلك:

1. لا يميز بين الاستثناءات المحددة وغير المحددة. جميع الاستثناءات غير محددة فقط ، وتقرر بنفسك ما إذا كنت ستلاحقها ومعالجتها.

2. يمكن استخدام try كتعبير - يمكنك تشغيل block try وإرجاع السطر الأخير منه ، أو إرجاع السطر الأخير من block catch.

 val value = try {Integer.parseInt(“lol”)} catch(e: NumberFormanException) { 4 } //  

3. يمكنك أيضًا استخدام إنشاء مشابه عند الإشارة إلى بعض الأشياء ، والتي قد تكون لاغية:

 val s = obj.money ?: throw IllegalArgumentException(“ , ”) 

توافق جافا


كود Kotlin يمكن استخدامها في جافا والعكس بالعكس. كيفية التعامل مع الاستثناءات؟

  • لا يمكن التحقق من الاستثناءات المحددة من Java في Kotlin أو الإعلان عنها (نظرًا لعدم وجود استثناءات محددة في Kotlin).
  • الاستثناءات الممكنة المحددة من Kotlin (على سبيل المثال ، تلك التي جاءت في الأصل من Java) ليست مطلوبة للتحقق في Java.
  • إذا كان من الضروري التحقق ، يمكن جعل الاستثناء قابلاً للتحقق منه باستخدام التعليق التوضيحيThrows في الطريقة (من الضروري الإشارة إلى الاستثناءات التي يمكن لهذه الطريقة طرحها). التعليق التوضيحي أعلاه مخصص فقط لتوافق Java. لكن في الممارسة العملية ، يستخدمه كثير من الناس لإعلان أن مثل هذه الطريقة ، من حيث المبدأ ، يمكن أن تلقي نوعًا من الاستثناء.

بديل لمحاولة كتلة الصيد


كتلة محاولة الصيد لديه عيب كبير. عندما يظهر ، يتم نقل جزء من منطق العمل داخل المصيد ، ويمكن أن يحدث ذلك بأحد الطرق العديدة المذكورة أعلاه. عندما ينتشر منطق العمل على الكتل أو سلسلة الاتصال بأكملها ، يكون من الصعب فهم كيفية عمل التطبيق. وكتل سهولة القراءة أنفسهم لا تضيف رمز.

 try { HttpService.SendNotification(endpointUrl); MarkNotificationAsSent(); } catch (e: UnableToConnectToServerException) { MarkNotificationAsNotSent(); } 

ما هي البدائل؟

خيار واحد يوفر لنا نهجا وظيفيا لمعالجة الاستثناء. تطبيق مماثل يشبه هذا:

 val result: Try<Result> = Try{HttpService.SendNotification(endpointUrl)} when(result) { is Success -> MarkNotificationAsSent() is Failure -> MarkNotificationAsNotSent() } 

لدينا الفرصة لاستخدام Try monad. في جوهرها ، هذه حاوية تخزن بعض القيمة. flatMap هي طريقة للعمل مع هذه الحاوية ، والتي ، إلى جانب القيمة الحالية ، يمكن أن تأخذ وظيفة ، ومرة ​​أخرى ، تُرجع monad.

في هذه الحالة ، يتم التفاف المكالمة في Try monad (نعود إلى Try). يمكن معالجتها في مكان واحد - حيث نحتاج إليها. إذا كان للمخرجات قيمة ، فنحن نقوم بالإجراءات التالية معها ، إذا تم طرح استثناء ، سنقوم بمعالجته في نهاية السلسلة.

التعامل مع استثناء وظيفي


أين يمكنني أن أحاول؟

أولاً ، هناك عدد لا بأس به من تطبيقات المجتمع لفصول "حاول وإما". يمكنك أن تأخذها أو حتى تكتب التنفيذ بنفسك. في أحد المشاريع "القتالية" ، استخدمنا تطبيق Try "عصامي" - لقد نجحنا مع فئة واحدة وقمنا بعمل ممتاز.
ثانياً ، توجد مكتبة Arrow ، التي تضيف من حيث المبدأ الكثير من الوظائف إلى Kotlin. بطبيعة الحال ، هناك محاولة وإما.

حسنًا ، بالإضافة إلى ذلك ، ظهرت فئة النتائج في Kotlin 1.3 ، والتي سأناقشها بمزيد من التفصيل لاحقًا.

حاول استخدام مكتبة Arrow كمثال


مكتبة Arrow تعطينا صف Try. في الواقع ، يمكن أن يكون ذلك في حالتين: النجاح أو الفشل:

  • النجاح في الانسحاب الناجح سيحتفظ بقيمتنا ،
  • يخزن الفشل استثناء حدث أثناء تنفيذ كتلة التعليمات البرمجية.

الدعوة هي على النحو التالي. وبطبيعة الحال ، يتم لفها في محاولة جرب بانتظام ، ولكن هذا سيحدث في مكان ما داخل الكود الخاص بنا.

 sealed class Try<out A> { data class Success<out A>(val value: A) : Try<A>() data class Failure(val e: Throwable) : Try<Nothing>() companion object { operator fun <A> invoke(body: () -> A): Try<A> { return try { Success(body()) } catch (e: Exception) { Failure(e) } } } 

يجب أن تطبق نفس الفئة طريقة flatMap ، والتي تسمح لك بتمرير وظيفة وإرجاع monad try:

 inline fun <B> map(f: (A) -> B): Try<B> = flatMap { Success(f(it)) } inline fun <B> flatMap(f: (A) -> TryOf<B>): Try<B> = when (this) { is Failure -> this is Success -> f(value) } 

ما هذا؟ من أجل عدم معالجة الأخطاء لكل من النتائج عندما يكون لدينا العديد منها. على سبيل المثال ، حصلنا على عدة قيم من خدمات مختلفة ونريد دمجها. في الواقع ، يمكن أن يكون لدينا حالتان: إما تلقيناها بنجاح وضمناها ، أو وقع شيء ما. لذلك ، يمكننا القيام بما يلي:

 val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { 4 } val sum = result1.flatMap { one -> result2.map { two -> one + two } } println(sum) //Success(value=15) 

إذا نجح كل من المكالمات وحصلنا على القيم ، فإننا ننفذ الوظيفة. إذا لم ينجحوا ، فسوف يعود الفشل مع استثناء.

إليك ما يبدو إذا سقط شيء ما:

 val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { throw RuntimeException(“Oh no!”) } val sum = result1.flatMap { one -> result2.map { two -> one + two } } println(sum) //Failure(exception=java.lang.RuntimeException: Oh no! 

استخدمنا نفس الوظيفة ، ولكن الإخراج هو فشل من RuntimeException.

أيضًا ، تتيح لك مكتبة Arrow استخدام تصميمات في الواقع عبارة عن سكر نحوي ، بشكل خاص ملزمة. كل هذا يمكن إعادة كتابته من خلال flatMap مسلسل ، لكن الربط يسمح لك بقراءته.

 val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { 4 } val result3: Try<Int> = Try { throw RuntimeException(“Oh no, again!”) } val sum = binding { val (one) = result1 val (two) = result2 val (three) = result3 one + two + three } println(sum) //Failure(exception=java.lang.RuntimeException: Oh no, again! 

بالنظر إلى أن إحدى النتائج قد انخفضت ، فقد حصلنا على خطأ في الإخراج.

يمكن استخدام monad مماثل للمكالمات غير المتزامنة. على سبيل المثال ، فيما يلي وظيفتان تعملان بشكل غير متزامن. نجمع نتائجها بالطريقة نفسها ، دون التحقق من حالتها بشكل منفصل:

 fun funA(): Try<Int> { return Try { 1 } } fun funB(): Try<Int> { Thread.sleep(3000L) return Try { 2 } } val a = GlobalScope.async { funA() } val b = GlobalScope.async { funB() } val sum = runBlocking { a.await().flatMap { one -> b.await().map {two -> one + two } } } 

وهنا مثال أكثر "قتالية". لدينا طلب إلى الخادم ، ونقوم بمعالجته ، والحصول على النص الأساسي منه ومحاولة تعيينه إلى فصلنا ، الذي نعيد منه البيانات بالفعل.

 fun makeRequest(request: Request): Try<List<ResponseData>> = Try { httpClient.newCall(request).execute() } .map { it.body() } .flatMap { Try { ObjectMapper().readValue(it, ParsedResponse::class.java) } } .map { it.data } fun main(args : Array<String>) { val response = makeRequest(RequestBody(args)) when(response) { is Try.Success -> response.data.toString() is Try.Failure -> response.exception.message } } 

جرّب الصيد أن يجعل هذه الكتلة أقل قابلية للقراءة. وفي هذه الحالة ، نحصل على response.data في الإخراج ، والتي يمكننا معالجتها وفقًا للنتيجة.

نتيجة Kotlin 1.3


قدم Kotlin 1.3 فئة النتيجة. في الحقيقة ، إنه شيء مشابه لـ Try ، لكن مع عدد من القيود. الغرض منه في الأصل أن يستخدم في عمليات غير متزامنة مختلفة.

 val result: Result<VeryImportantData> = Result.runCatching { makeRequest() } .mapCatching { parseResponse(it) } .mapCatching { prepareData(it) } result.fold{ { data -> println(“We have $data”) }, exception -> println(“There is no any data, but it's your exception $exception”) } ) 

إن لم يكن مخطئا ، هذه الفئة هي حاليا التجريبية. يمكن لمطوري اللغة تغيير توقيعه أو سلوكه أو إزالته تمامًا ، لذلك في الوقت الحالي يُحظر استخدامه كقيمة إرجاع من الأساليب أو المتغير. ومع ذلك ، يمكن استخدامه كمتغير محلي (خاص). أي في الواقع ، يمكن استخدامه كمحاولة من المثال.

النتائج


الاستنتاجات التي خلقتها لنفسي:

  • معالجة الأخطاء الوظيفية في Kotlin بسيطة ومريحة.
  • لا أحد يكترث لمعالجتها من خلال تجربة جذابة في النمط الكلاسيكي (كلاهما وله الحق في الحياة ؛ كلاهما ملائمان) ؛
  • عدم وجود استثناءات محددة لا يعني أنه لا يمكن معالجة الأخطاء ؛
  • استثناءات غير معلومة على الإنتاج تؤدي إلى عواقب محزنة.

مؤلف المقالة: أليكسي شافرانوف ، رئيس مجموعة العمل (جافا) ، Maxilect

ملاحظة: ننشر مقالاتنا على عدة مواقع من Runet. اشترك في صفحاتنا على VK أو FB أو Telegram-channel لمعرفة كل منشوراتنا وغيرها من أخبار Maxilect.

Source: https://habr.com/ru/post/ar447380/


All Articles