День семьсот пятнадцатый. #TypesAndLanguages
Довольно интересной показалась серия статей Types and Programming Languages от Adam Furmanek. Интересные наблюдения и тонкости языков программирования. Сегодня первая часть.
1. Не используйте return в блоке finally
Многие языки предоставляют конструкцию обработки исключений, обычно в виде блоков
Некоторые языки поддерживают дополнительный блок
Некоторые языки позволяют возвращать результат из блока
Результат кажется простым и логичным. Но что произойдет, если вы выбросите исключение, а затем используете return? Например, в Java:
Источник: https://blog.adamfurmanek.pl/2021/01/09/types-and-programming-languages-part-1/
Довольно интересной показалась серия статей Types and Programming Languages от Adam Furmanek. Интересные наблюдения и тонкости языков программирования. Сегодня первая часть.
1. Не используйте return в блоке finally
Многие языки предоставляют конструкцию обработки исключений, обычно в виде блоков
try и catch, хотя детали реализации различаются. Программисты обычно предполагают, что эти конструкции работают одинаково для разных языков. К сожалению, это неправда, и нюансы часто бывают сложны для понимания, когда дело доходит до крайних случаев.Некоторые языки поддерживают дополнительный блок
finally, который должен выполняться «несмотря ни на что» - независимо от того, было сгенерировано исключение или нет. Это, правда, не совсем так: есть много ситуаций, когда этот блок не может быть вызван, например, аварийное завершение приложения, ошибки нарушения доступа и т. д. Но сейчас не об этом.Некоторые языки позволяют возвращать результат из блока
finally. В следующем примере на Java последний return «побеждает» другие:public static void main (String[] args)Результат - 42, потому что это последнее возвращаемое значение. Такое же поведение можно видеть в Python, JS, возможно, и на других платформах. Примечательным исключением здесь является C#, который не позволяет использовать return в блоке finally, просто чтобы избежать этой путаницы.
throws java.lang.Exception {
System.out.println(foo());
}
public static int foo(){
try{
return 5;
}finally{
return 42;
}
}
Результат кажется простым и логичным. Но что произойдет, если вы выбросите исключение, а затем используете return? Например, в Java:
public static void main (String[] args)Результат – всё равно 42. Исключение «проглатывается». То же самое в JS:
throws java.lang.Exception {
System.out.println(foo());
}
public static int foo() {
try {
throw new RuntimeException(
"Это сообщение пропадает");
}finally{
return 42;
}
}
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. Исключение при Обработке Исключения
В прошлый раз мы рассмотрели, что не стоит использовать
Результат различается в разных языках. Например, C# теряет исключение, что указано в спецификации:
Если блок finally выбрасывает другое исключение, обработка текущего исключения прекращается.
Стоит отметить, что некоторые языки предоставляют поле в классе исключения, которое должно хранить предыдущее. Но, если внутреннее исключение не устанавливается автоматически платформой, то это поле нам никак не поможет.
Это важно при работе с ресурсами. Некоторые языки предоставляют конструкцию типа
Источники:
- 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/
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Мы теряем исключение "Using failed" из-за нового исключения "Disposing failed" в блоке finally. Кстати, в Java это реализовано правильно, и сохраняет оба исключения.
Disposing
Exception: System.Exception: Disposing failed
at Resource.Dispose()
at Program.Main()
Источники:
- 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 при Прекращении Работы
Рассмотрим следующий код:
Внутри обработанного исключения гарантируется выполнение связанного блока
Поэтому блок
Дальше больше, потому что всё может зависеть от типа исключения. Например, в .NET есть атрибут HandleProcessCorruptedStateExceptions:
Исключения повреждённого состояния процесса - это исключения, которые указывают, что состояние процесса было повреждено. Дальнейшее выполнение приложения в этом состоянии не рекомендуется.
По умолчанию среда CLR не передаёт эти исключения управляемому коду, и блоки
Таким образом, ваше приложение может продолжить работу, но не все блоки
Аналогичный вопрос возникает, когда вместо создания исключения вы выходите из приложения, вызывая
Зачем это нужно? Потому что мы обычно освобождаем ресурсы в блоке finally. Если эти ресурсы являются локальными для процесса, тогда это не имеет большого значения, но как только вы начнете использовать межпроцессные вещи (например, общесистемные мьютексы), важно освободить их, иначе другой пользователь может не знать, повреждено защищённое состояние или нет.
Это уже не говоря о том, что необработанное исключение может (.NET) или не может (JVM) привести к остановке всего приложения.
Вывод
Всегда помещайте в поток исполнения глобальный обработчик
Источник: https://blog.adamfurmanek.pl/2021/01/23/types-and-programming-languages-part-3/
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/
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).
Важно понимать, что это не имеет ничего общего с технической реализацией. Это логическая проблема (какой из членов выбрать), а не техническая.
Рассмотрим наследование сигнатуры:
Что же с реализацией интерфейса по умолчанию?
Источник: https://blog.adamfurmanek.pl/2021/02/06/types-and-programming-languages-part-4/
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();Вызовет ошибку компиляции в обоих языках. В Java с сообщением, что интерфейсы
c.foo();
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/
День восемьсот девяностый. #TypesAndLanguages
4. Проблема Алмаза. Окончание
Начало
Продолжение
Так в чём же состоит Проблема Алмаза? Не в наследовании несовместимого, а в выборе, какой унаследованный член использовать. Ситуация усложняется, когда мы вводим состояние в базовый класс. Нам нужно решить, хотим ли мы иметь независимые состояния для каждого подкласса (обычное наследование) или разделять его между подклассами (виртуальное наследование). Если мы делимся состоянием, оно может легко сломаться (поскольку две разные реализации используют одни и те же переменные). Если нет, нам нужно указать, какие переменные мы имеем в виду в самом низком подклассе.
В C# и Java проблема решается ошибкой времени компиляции. Но в других языках есть другие решения, например в Scala приоритет получает член «крайнего правого» унаследованного класса.
Проблема Алмаза в Java и C# с первого дня
По сути Проблема Алмаза была в Java и C# с самого начала. Как мы уже говорили, проблема состоит в том, чтобы решить, какому из членов отдать приоритет. Например:
По сути, это та же Проблема Алмаза, что и раньше. У нас есть две вещи, и мы не можем решить, какую из них использовать. При реализациях интерфейса по умолчанию языки выдают ошибку компиляции, но при перегрузке метода они просто выбирают один метод вместо другого.
Это, кстати, может привести к нарушению совместимости. Представьте, что кто-то добавит ещё один метод
Итого
Хотя правильно сказать, что в Java и C# нет множественного наследования, лучше иметь в виду, что существует много уровней наследования, и мы должны быть конкретными. Мы можем унаследовать реализацию начиная с C# 8 — это множественное наследование или нет?
Хотя правильно сказать, что до 8 версии языка не было Проблемы Алмаза, по сути, проблема существует в перегрузке методов. И это имеет те же последствия.
Стоит помнить, как, казалось бы, совершенно разные языковые элементы приводят к аналогичным проблемам. Мы все «боимся» Проблемы Алмаза, но не боимся перегрузки методов. Более того, считаем что это классная функция, пока однажды не нарушим совместимость.
Источник: https://blog.adamfurmanek.pl/2021/02/06/types-and-programming-languages-part-4/
4. Проблема Алмаза. Окончание
Начало
Продолжение
Так в чём же состоит Проблема Алмаза? Не в наследовании несовместимого, а в выборе, какой унаследованный член использовать. Ситуация усложняется, когда мы вводим состояние в базовый класс. Нам нужно решить, хотим ли мы иметь независимые состояния для каждого подкласса (обычное наследование) или разделять его между подклассами (виртуальное наследование). Если мы делимся состоянием, оно может легко сломаться (поскольку две разные реализации используют одни и те же переменные). Если нет, нам нужно указать, какие переменные мы имеем в виду в самом низком подклассе.
В C# и Java проблема решается ошибкой времени компиляции. Но в других языках есть другие решения, например в Scala приоритет получает член «крайнего правого» унаследованного класса.
Проблема Алмаза в Java и C# с первого дня
По сути Проблема Алмаза была в Java и C# с самого начала. Как мы уже говорили, проблема состоит в том, чтобы решить, какому из членов отдать приоритет. Например:
class A {
public string foo(long l) => "Long";
public string foo(double d) => "Double";
}
Что выведет следующий код? A a = new A();У нас есть два метода с разными сигнатурами. Мы хотим вызвать метод и передать недопустимое значение - значение другого типа. Однако Java и C# достаточно «умны», и просто приводят значение к типу, который им больше нравится (в данном случае -
Console.WriteLine(a.foo(123));
long).По сути, это та же Проблема Алмаза, что и раньше. У нас есть две вещи, и мы не можем решить, какую из них использовать. При реализациях интерфейса по умолчанию языки выдают ошибку компиляции, но при перегрузке метода они просто выбирают один метод вместо другого.
Это, кстати, может привести к нарушению совместимости. Представьте, что кто-то добавит ещё один метод
foo(int i). Что будет с вашим кодом? Раньше int приводился к long, но после добавления нового метода приведение не требуется, и будет вызван новый метод. Это нарушает совместимость.Итого
Хотя правильно сказать, что в Java и C# нет множественного наследования, лучше иметь в виду, что существует много уровней наследования, и мы должны быть конкретными. Мы можем унаследовать реализацию начиная с C# 8 — это множественное наследование или нет?
Хотя правильно сказать, что до 8 версии языка не было Проблемы Алмаза, по сути, проблема существует в перегрузке методов. И это имеет те же последствия.
Стоит помнить, как, казалось бы, совершенно разные языковые элементы приводят к аналогичным проблемам. Мы все «боимся» Проблемы Алмаза, но не боимся перегрузки методов. Более того, считаем что это классная функция, пока однажды не нарушим совместимость.
Источник: https://blog.adamfurmanek.pl/2021/02/06/types-and-programming-languages-part-4/