.NET Разработчик
6.55K subscribers
442 photos
3 videos
14 files
2.12K 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/
День восемьсот восемьдесят восьмой. #TypesAndLanguages
4. Проблема Алмаза. Начало
Проблема Алмаза, иногда называемая Смертельным Алмазом Смерти, возникает, когда мы наследуем одно и то же через несколько базовых сущностей. Если вы думаете, что «проблема есть в C++, но её не существует в Java или C#», то вы слишком сосредотачиваетесь на технической части.

Наследование
Обычно мы говорим, что в Java или C# существует одиночное наследование и множественная реализация интерфейсов. Это правда, но за этим скрывается гораздо более широкая картина.

Наследование позволяет наследовать характеристики и функции от базовой сущности (в большинстве случаев от класса или объекта). Есть много вещей, которые мы можем унаследовать, или много уровней наследования:

1. Наследование сигнатуры
В интерфейсе объявлен метод, мы наследуем его и предоставляем реализацию. Сигнатура здесь означает, что это только «заголовок» метода, без тела. Важно понимать, что эта «сигнатура наследования» не обязательно должна совпадать с «сигнатурой вызова». Например, вы не можете изменить тип возвращаемого значения при реализации интерфейса в C#, но тип возвращаемого значения не является частью «сигнатуры вызова». Java допускает это, но это детали реализации. Когда мы говорим о «наследовании сигнатуры», мы имеем в виду только заголовок метода, который мы получаем от базовой сущности.

2. Наследование реализации
При наследовании реализации мы получаем не только сигнатуру, но и всё тело метода. Это не так давно стало доступно в Java или C# через реализации интерфейса по умолчанию. Это можно рассматривать как типажи (traits). И хотя между наследованием реализации и трейтами есть некоторые различия, они довольно близки друг к другу.

3. Наследование состояния
Это наследование полей. При наследовании состояния мы получаем поле из базовой сущности, которое мы можем использовать в подклассе. В некоторой степени это похоже на примеси (mixins). Также стоит отметить, что у нас может быть наследование состояния без наследования реализации, но в большинстве случаев эти два аспекта объединяются.

4. Наследование идентичности
Можно считать «наследованием конструктора» (не вдаваясь в теорию типов). Разница между миксином и наследованием от класса сводится к конструктору. Вы можете создать экземпляр и получить новую идентичность. Обычно мы получаем идентичность, создавая базовую сущность и «удерживая» её внутри подобъекта.

Наследование в C# и Java
C++ имеет множественное наследование и не делает различий между классом и интерфейсом. В C# и Java запрещено всё, кроме наследования сигнатур. Однако важно понимать, что утверждение, что «в C# и Java не существует множественного наследования», неверно. Существует множественное наследование для сигнатур и одиночное наследование для всего остального.

Однако позже создатели языков поняли, что это может быть не лучшая идея, и была добавлена реализация интерфейса по умолчанию, которая по сути является «наследованием реализации».

Продолжение следует…

Источник:
https://blog.adamfurmanek.pl/2021/02/06/types-and-programming-languages-part-4/
День восемьсот восемьдесят девятый. #TypesAndLanguages
4. Проблема Алмаза. Продолжение
Начало

Википедия определяет проблему алмаза как ситуацию, когда два класса B и C наследуются от класса A, переопределяют что-то, а затем класс D наследуется от классов B и C, не переопределяя член из A. Когда мы теперь хотим использовать член из A в классе D, мы не знаем, какой из них использовать (из B или из C).

Важно понимать, что это не имеет ничего общего с технической реализацией. Это логическая проблема (какой из членов выбрать), а не техническая.

Рассмотрим наследование сигнатуры:
interface A {
void foo();
}
interface B {
void foo();
}
class C : A, B {
public void foo(){
Console.WriteLine("FOO");
}
}

Здесь никаких проблем нет, поскольку сигнатуры одинаковые, и неважно, какую из них использовать. Но что, если мы изменим тип результата:
interface A {
object foo();
}
interface B {
string foo();
}
class C : A, B {
public string foo() {
return "Foo";
}
}

В Java это сработает, в C# компилятор выдаст ошибку, требуя реализации
A.foo() из-за другого типа результата. Здесь «Ситуация Алмаза» решается в Java, но является проблемой в C#.

Что же с реализацией интерфейса по умолчанию?
interface A {
void foo() => Console.WriteLine("A");
}
interface B {
void foo() => Console.WriteLine("B");
}
class C : A, B { }

Следующий код
C c = new C();
c.foo();

Вызовет ошибку компиляции в обоих языках. В Java с сообщением, что интерфейсы A и B несовместимы. В C#, что класс C не содержит реализации метода foo. Всё дело в том, что C# заставляет использовать явную реализацию, и выбор остаётся за клиентским кодом (нужно привести экземпляр класса к одному из интерфейсов):
C c = new C();
((A)c).foo();

Окончание следует…

Источник:
https://blog.adamfurmanek.pl/2021/02/06/types-and-programming-languages-part-4/