.NET Разработчик
6.69K subscribers
463 photos
4 videos
14 files
2.21K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День семьсот пятнадцатый. #TypesAndLanguages
Довольно интересной показалась серия статей Types and Programming Languages от Adam Furmanek. Интересные наблюдения и тонкости языков программирования. Сегодня первая часть.

1. Не используйте return в блоке finally
Многие языки предоставляют конструкцию обработки исключений, обычно в виде блоков try и catch, хотя детали реализации различаются. Программисты обычно предполагают, что эти конструкции работают одинаково для разных языков. К сожалению, это неправда, и нюансы часто бывают сложны для понимания, когда дело доходит до крайних случаев.

Некоторые языки поддерживают дополнительный блок finally, который должен выполняться «несмотря ни на что» - независимо от того, было сгенерировано исключение или нет. Это, правда, не совсем так: есть много ситуаций, когда этот блок не может быть вызван, например, аварийное завершение приложения, ошибки нарушения доступа и т. д. Но сейчас не об этом.

Некоторые языки позволяют возвращать результат из блока finally. В следующем примере на Java последний return «побеждает» другие:
public static void main (String[] args)
throws java.lang.Exception {
System.out.println(foo());
}
public static int foo(){
try{
return 5;
}finally{
return 42;
}
}

Результат - 42, потому что это последнее возвращаемое значение. Такое же поведение можно видеть в Python, JS, возможно, и на других платформах. Примечательным исключением здесь является C#, который не позволяет использовать return в блоке finally, просто чтобы избежать этой путаницы.

Результат кажется простым и логичным. Но что произойдет, если вы выбросите исключение, а затем используете return? Например, в Java:
public static void main (String[] args)
throws java.lang.Exception {
System.out.println(foo());
}
public static int foo() {
try {
throw new RuntimeException(
"Это сообщение пропадает");
}finally{
return 42;
}
}

Результат – всё равно 42. Исключение «проглатывается». То же самое в JS:
function foo(){
try{
throw "Это сообщение пропадает";
}finally{
return 42;
}
}
console.log(foo());

Python:
def foo():
try:
raise Exception("Это сообщение пропадает")
finally:
return 42

print(foo())

В общем случае, никогда не используйте return в блоке finally. Это ломает механизм обработки исключений.

Источник: https://blog.adamfurmanek.pl/2021/01/09/types-and-programming-languages-part-1/
День семьсот двадцать первый. #TypesAndLanguages
2. Исключение при Обработке Исключения
В прошлый раз мы рассмотрели, что не стоит использовать return в блоке finally. Сегодня исследуем аналогичный случай исключения при обработке исключения:
try{
try{
throw new Exception("Exception 1");
}finally{
throw new Exception("Exception 2");
}
}catch(Exception e){
Console.WriteLine(e);
}

Что будет выведено? Этот вопрос с подвохом. Во-первых, есть два исключения, и мы знаем, что некоторые языки (включая платформу .NET) реализуют двухпроходную систему обработки исключений. Первый проход по стеку ищет обработчик, способный обработать исключение, затем второй проход раскручивает стек и выполняет все блоки finally. Но что, если мы выбросим исключение во втором проходе?

Результат различается в разных языках. Например, C# теряет исключение, что указано в спецификации:
Если блок finally выбрасывает другое исключение, обработка текущего исключения прекращается.

Стоит отметить, что некоторые языки предоставляют поле в классе исключения, которое должно хранить предыдущее. Но, если внутреннее исключение не устанавливается автоматически платформой, то это поле нам никак не поможет.

Это важно при работе с ресурсами. Некоторые языки предоставляют конструкцию типа using в C#:
using(var resource = new Resource()){
//…
}
Концептуально это аналогично следующему коду:
var resource = new Resource();
try{
//…
} finally{
if(resource != null) resource.Dispose();
}

Кажется довольно просто. Но что если Dispose выбросит исключение:
public class Resource : IDisposable {
public void Dispose(){
Console.WriteLine("Disposing");
throw new Exception("Disposing failed");
}
}

Тогда следующий код:
try{
using(var resource = new Resource()){
Console.WriteLine("Using");
throw new Exception("Using failed");
}
}catch(Exception e){
Console.WriteLine("Exception: " + e);
}

выведет
Using
Disposing
Exception: System.Exception: Disposing failed
at Resource.Dispose()
at Program.Main()

Мы теряем исключение "Using failed" из-за нового исключения "Disposing failed" в блоке finally. Кстати, в Java это реализовано правильно, и сохраняет оба исключения.

Источники:
-
https://blog.adamfurmanek.pl/2021/01/16/types-and-programming-languages-part-2/
-
https://blog.adamfurmanek.pl/2020/07/25/net-inside-out-part-21/
День семьсот тридцать шестой. #TypesAndLanguages
3. Finally при Прекращении Работы
Рассмотрим следующий код:
try {
throw new Exception("Exception 1");
}finally{
// очистка
}
Допустим, в этом потоке исполнения нет блока catch. Что произойдёт? Это зависит от платформы. Например, в документации C# по finally говорится:
Внутри обработанного исключения гарантируется выполнение связанного блока finally. Однако если исключение не обработано, то выполнение блока finally зависит от того, как запускается операция развертывания исключения. Это, в свою очередь, зависит от способа настройки компьютера.

Поэтому блок finally может и не выполниться. В то же время, согласно этому документу, JVM гарантирует исполнение блока finally.

Дальше больше, потому что всё может зависеть от типа исключения. Например, в .NET есть атрибут HandleProcessCorruptedStateExceptions:
Исключения повреждённого состояния процесса - это исключения, которые указывают, что состояние процесса было повреждено. Дальнейшее выполнение приложения в этом состоянии не рекомендуется.
По умолчанию среда CLR не передаёт эти исключения управляемому коду, и блоки try-catch (или другие способы обработки исключений) для них не вызываются. Если вы абсолютно уверены, что хотите самостоятельно обрабатывать такие исключения, вы должны применить атрибут HandleProcessCorruptedStateExceptions к методу, в котором вы хотите выполнять блоки обработки таких исключений. CLR предоставляет исключения повреждённого состояния процесса в применимые блоки обработки исключений только в методах, которые имеют атрибуты HandleProcessCorruptedStateExceptions или SecurityCritical.

Таким образом, ваше приложение может продолжить работу, но не все блоки finally могут быть выполнены.

Аналогичный вопрос возникает, когда вместо создания исключения вы выходите из приложения, вызывая Application.Exit(). Будет ли исполнен блок finally?
Зачем это нужно? Потому что мы обычно освобождаем ресурсы в блоке finally. Если эти ресурсы являются локальными для процесса, тогда это не имеет большого значения, но как только вы начнете использовать межпроцессные вещи (например, общесистемные мьютексы), важно освободить их, иначе другой пользователь может не знать, повреждено защищённое состояние или нет.

Это уже не говоря о том, что необработанное исключение может (.NET) или не может (JVM) привести к остановке всего приложения.

Вывод
Всегда помещайте в поток исполнения глобальный обработчик try-catch.

Источник: https://blog.adamfurmanek.pl/2021/01/23/types-and-programming-languages-part-3/