.NET Разработчик
6.53K 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/
День восемьсот девяностый. #TypesAndLanguages
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();
Console.WriteLine(a.foo(123));

У нас есть два метода с разными сигнатурами. Мы хотим вызвать метод и передать недопустимое значение - значение другого типа. Однако Java и C# достаточно «умны», и просто приводят значение к типу, который им больше нравится (в данном случае - long).

По сути, это та же Проблема Алмаза, что и раньше. У нас есть две вещи, и мы не можем решить, какую из них использовать. При реализациях интерфейса по умолчанию языки выдают ошибку компиляции, но при перегрузке метода они просто выбирают один метод вместо другого.

Это, кстати, может привести к нарушению совместимости. Представьте, что кто-то добавит ещё один метод foo(int i). Что будет с вашим кодом? Раньше int приводился к long, но после добавления нового метода приведение не требуется, и будет вызван новый метод. Это нарушает совместимость.

Итого
Хотя правильно сказать, что в Java и C# нет множественного наследования, лучше иметь в виду, что существует много уровней наследования, и мы должны быть конкретными. Мы можем унаследовать реализацию начиная с C# 8 — это множественное наследование или нет?

Хотя правильно сказать, что до 8 версии языка не было Проблемы Алмаза, по сути, проблема существует в перегрузке методов. И это имеет те же последствия.

Стоит помнить, как, казалось бы, совершенно разные языковые элементы приводят к аналогичным проблемам. Мы все «боимся» Проблемы Алмаза, но не боимся перегрузки методов. Более того, считаем что это классная функция, пока однажды не нарушим совместимость.

Источник: https://blog.adamfurmanek.pl/2021/02/06/types-and-programming-languages-part-4/
День девятьсот шестьдесят шестой. #TypesAndLanguages
Ключевые различия между C# и F#. Начало
И C#, и F# по-своему уникальны. В чём их различия и что сделать проще в C#, а что в F#?

В чём C# превосходит F#?
1. Асинхронность
Асинхронный код в C# выполняется быстрее, чем в F#. Это в первую очередь потому, что асинхронные конструкции изначально поддерживаются компилятором и генерируют оптимизированный код. Как только в F# появится аналогичная поддержка, эта разница уменьшится. Кроме того, она не очень важна для типичного бизнес-приложения. Мы можем написать библиотеку C# и вызвать ее из F# для реального кода, чувствительного к производительности.

2. Взаимодействие с библиотеками .NET
Поскольку большинство библиотек .NET написано на C#, разработчику проще работать с ними на C# по сравнению с F#.

3. Ранний возврат
В C# для возвращения из метода используется ключевое слово return. Это невозможно в F#. В глубоко вложенных блоках кода эта функция действительно полезна.

4. Ко-/контравариантность
Вариантность поддерживается в C#, но на данный момент не поддерживается в F#. Это особенно полезно для библиотечного кода, который имеет дело с обобщениями.

5. ООП в общем
В C# работать с защищёнными классами проще, так как в нём есть ключевое слово protected, которого нет в F#. Кроме того, реализация типов внутренних классов, неявных интерфейсов и частичных классов возможна в C#, но не в F#.

6. Неявное приведение
Неявное приведение типов поддерживается в C#, но не поддерживается в F#. В F# не поддерживается ни неявный апкаст, ни неявный даункаст. Следовательно, в C# проще использовать библиотеки, полагающиеся на неявное приведение типов.

7. Генераторы кода
Генераторы исходного кода недоступны для F#. Хотя есть Myriad.

8. Упорядочивание файлов
В C# упорядочивание файлов и пространств имён возможно любым способом. В F# строгий порядок (снизу вверх).

9. Инструменты и поддержка в IDE
Инструменты и поддержка IDE в C# лучше по сравнению с F#.

10. Отладка
Во всех IDE процесс отладки проще в C#, чем в F#. Асинхронные рабочие процессы особенно сложно отлаживать на F#.

11. Низкоуровневый код
Небезопасный код и Invoke/P, span и закреплённые указатели также поддерживаются только в C#.

12. WinForms и WPF
C# начинался с разработки клиентов WPF или Winform. Это не важная область для F#.

13. Entity Framework
В мире .NET Entity Framework - очень популярен. Разработчикам инстинктивно приходит в голову не использовать его в F#, потому что его дизайн противоречит F#.

14. Асинхронный Main
В методе Main async может использоваться в C#, в то время как для F# в нём используется Async.RunSynchronously.

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

Источник:
https://www.partech.nl/nl/publicaties/2021/06/key-differences-between-c-sharp-and-f-sharp

UPD: поскольку я не спец в F#, оказалось, что некоторые пункты из поста не совсем верны. Спасибо @Lanayx за комментарий
День девятьсот шестьдесят седьмой. #TypesAndLanguages
Ключевые различия между C# и F#. Окончание
Начало

В чём F# превосходит C#?
1. Неизменяемость по умолчанию
В F# всё неизменяемо, если вы не используете ключевое слово mutable. Неизменяемость помогает упростить распараллеливание и предотвратить дефекты.

2. Пайплайн
В F# пайплайн упрощает написание кода от верхнего левого угла до нижнего правого без использования каких-либо локальных переменных. Следовательно, облегчается чтение кода.

3. Всё - выражение
Понимание кода становятся проще при использовании выражений. Также упрощается отладка кода.

4. Вывод типа
Вывод типа в F# упрощает создание пользовательских типов. Рефакторинг также проще. Идея заключается в том, что нет необходимости указывать типы F# конструкций, за исключением случаев, когда компилятор не может вывести тип самостоятельно. В C# вывод типа также используется, но не так широко.

5. Разграниченные объединения (discriminated unions)
Разграниченные объединения обеспечивают поддержку значений, которые могут быть одним из нескольких именованных вариантов, возможно, каждое с разными значениями и типами. Они присутствуют в F#, что позволяет лучше моделировать бизнес-домен при использовании классов, записей, перечислений и интерфейсов. Лучшее моделирование приводит к простому коду и меньшему количеству дефектов.

6. Вычислительные выражения (computation expression)
Вычислительные выражения объединяют различные аспекты, такие как async, Option, Result, Validation и т.д. удобным для программиста способом. Это широко распространено в F#. Многие выражения представлены в библиотеке FsToolkit.

7. Сопоставление по шаблону
Активные шаблоны в F# позволяют выделить часть шаблона, задать ей имя и использовать в других шаблонах, что упрощает чтение сложных шаблонов. Хотя C# в последнее время улучшил сопоставление по шаблону, но F# всё ещё впереди в этом.

8. Единицы измерения (Measure)
Использование единиц измерения подключает компилятор к проверке, что значения в арифметическом выражении имеют правильные единицы, что помогает предотвратить ошибки и делает код более выразительным.

9. Частичное применение (partial application) и композиция
Частичное применение широко используется в F#. Идея заключается в том, что, если вы фиксируете первые N параметров функции, вы получаете новую функцию для остальных параметров. Это помогает создавать дизайн на основе композиции функций.

Сравнение кода
C#:
public static class SumOfSquares
{
public static int Square(int i)
=> i * i;

// без LINQ
public static int Sum(int n)
{
int sum = 0;
for (int i=1; i<=n; i++)
sum += Square(i);
return sum;
}

// с LINQ
public static int SumLinq(int n)
=> Enumerable.Range(1, n).Select(Square).Sum();
}

var x = SumOfSquares.Sum(100);

F#:
let square x = x * x;
let sumOfSquares n =
[1..n] |> List.map square |> List.sum

sumOfSquares 100

Итого
У обоих языков есть свои преимущества и недостатки. Следовательно, важно выбрать язык в зависимости от создаваемого приложения, чтобы можно было использовать все преимущества их функций для написания хорошего кода. И что самое приятное, эти два языка прекрасно взаимодействуют друг с другом, что позволяет вам смешивать их и использовать в своём приложении.

Источник: https://www.partech.nl/nl/publicaties/2021/06/key-differences-between-c-sharp-and-f-sharp
День 1260. #TypesAndLanguages
5. Чистые и «Нечистые» Функции
Чистые функции — функции, возвращающие один и тот же результат для одних и тех же входных данных, не полагаясь на внешнее состояние. Некоторые любят их. Некоторые утверждают, что все функции должны быть чистыми. Некоторые языки борются с нечистыми функциями или даже вообще не позволяют вам их создавать. Правильно ли говорить, что чистые функции лучше?

Вся аргументация относительно того, какие функции лучше, сводится к влиянию функций на код в целом. У чистых функций меньше «радиус поражения», что упрощает доказательство гипотез относительно них.

Чистые функции принимают входные параметры I и производят выходные данные O. Нечистые функции принимают входные данные в форме (I, S), где I — входные параметры, а S представляет внешнее состояние, подобное глобальным переменным. Нечистые функции также производят результат O, однако при этом O может влиять на S (и даже на I).

Как S меняет ситуацию? Мы можем утверждать, что S представляет состояние приложения, которое является неявными входными данными для функции. Имея это в виду, как чистые, так и нечистые функции возвращают один и тот же результат O для полученных входных данных. Однако чистые функции используют (I, null) в качестве входных данных, а нечистые используют (I, S). С этой точки зрения оба типа функций на самом деле чистые. В чём же разница?

Разница в том, что отследить I намного проще, чем S. Обычно нужно просто проанализировать место вызова и место, где I было задано значение. Нам не нужно смотреть на остальной код. Аналогично мы можем отследить входные данные вызывающей функции, и т.д. вверх по стеку вызовов.

Однако мы не можем так просто отследить S. Чтобы доказать что-либо об S, нам нужно не только показать, как было задано значение S, но также и доказать, что оно не было изменено ничем за пределами стека вызовов. Другими словами, чтобы что-то доказать гипотезу об I, нам нужно просто показать пример, подтверждающий нашу гипотезу. Например, что Math.Sqrt(4) = 2. Однако, чтобы доказать что-то об S, нам нужно показать, что нет никаких «контраргументов» (кода, изменяющего состояние S неожиданным образом), опровергающих нашу гипотезу. Последнее, как правило, намного сложнее, так как нам нужно проанализировать гораздо больше кода, чтобы доказать это. Кроме того, поиск такого кода может быть нетривиальным, так как это может быть рефлексия, параллельно выполняемый код и т.п. Или код может быть вообще недоступен, как какой-то динамически загружаемый плагин.

Однако до тех пор, пока мы можем поддерживать I и S небольшими по размеру, всё остальное не имеет значения. Дело не в том, чистая функция или нет. Всё дело в способности отследить входные данные (I, S). Это зависит от ваших когнитивных навыков, опыта и навыков программирования в целом. Некоторые функции могут быть ужасны и с тремя входными параметрами и без глобального состояния, тогда как другие функции могут отлично работать без параметров, но с пятью глобальными переменными.

А что насчёт изменчивости? В большинстве случаев это не имеет значения. Давайте рассмотрим пример функции double.TryParse. По определению она нечистая, так как 1) изменяет входной параметр и 2) использует глобальное состояние (символ десятичного разделителя). Однако является ли эта функция «неправильной» или «плохой»? Нет, потому что её «радиус поражения» обычно сильно ограничен.

Примечание: на самом деле все функции принимают ещё один параметр E. Параметр представляет собой среду, такую как память, используемую операционную систему, тип процессора, электрические помехи или даже космические лучи. Разница между S и E заключается в том, что мы контролируем. Мы можем контролировать S, поскольку это состояние выполняемого нами приложения, но мы не можем контролировать E. Вы можете косвенно повлиять на память, используемую операционной системой, но, скорее всего, вы не сможете повлиять на космический луч.

Источник: https://blog.adamfurmanek.pl/2022/07/09/types-and-programming-languages-part-14/
👍10
День 1288. #TypesAndLanguages
6. Запрещать или Разрешать при Разработке ПО
Дискуссии в области разработки ПО довольно часто касаются личных предпочтений. Несмотря на то, что все участники дебатов спорят, основываясь на своих знаниях, навыках или опыте, их дискуссии довольно часто сводятся к вопросу вкуса и цвета.

Одним из примеров является спор «Открытое Наследование или Разрешённое Наследование» Мартина Фаулера. Вопрос в том, должны ли мы по умолчанию разрешать наследование класса или блокировать его. Мы можем привести множество аргументов за и против, но нет единого показателя, показывающего, какой подход лучше. То же самое относится к тому, чтобы сделать методы виртуальными или запечатанными по умолчанию. Или о том, насколько всё делать приватным или открытым. Эти дискуссии, как правило, основаны на личном ощущении участников дебатов, и может быть довольно занимательно узнавать аргументацию, стоящую за каждым из вариантов.

Возьмем отрешённый пример. Представьте, что вы возвращаетесь домой ночью, в темном переулке, и тут появляются какие-то люди, бьют вас, забирают телефон и исчезают. Как правило, после такой ситуации вы можете использовать три разных подхода:
1. Вы решаете избегать таких встреч, насколько это возможно. Вы не возвращаетесь домой ночью, избегаете темных переулков или даже остаётесь дома. По сути, вы заставляете себя избегать проблемы.
2. Вы утверждаете, что такой ситуации быть не должно, что полиция должна быть ночью на улице, чтобы помочь вам в таком случае. Вы заставляете других сделать так, чтобы такая ситуация больше не повторилась.
3. Вы также можете пойти в спортзал, научиться боксу или боевым искусствам или носить оружие, чтобы в следующий раз быть готовым и не дать им забрать ваш телефон. По сути, вы заставляете себя принять ситуацию.

Вам решать, какой вариант вы выберете. Хотя вы можете возразить, что некоторые из них невыполнимы в вашем случае или нежелательны.

То же самое относится и к разработке ПО. Допустим, вы нашли ошибку в длинной функции, меняющей состояние приложения. Что вы сделаете? Варианты:
1. Вы решаете, что вы и ваша команда больше не будете так писать. Вы делаете акцент на обзорах кода, чтобы не пропускать такие функции.
2. Вы решаете, что такие функции вообще нельзя разрешать. Вы меняете свой стек на использование чистых функций (например, на F# или Haskell).
3. Вы просто улучшаете свои навыки, чтобы лучше поддерживать длинные функции, меняющие состояние приложения.

Похоже, что индустрия обычно выбирает первый вариант. В сети можно много прочитать о «чистом коде». Такие принципы, как «метод должен иметь не более двух параметров», подкрепляются такими утверждениями, как «Я работаю в отрасли уже 30 лет, поэтому я знаю лучше». Однако в итоге это просто вопрос личных предпочтений. На предпочтение могут влиять когнитивные навыки, окружающая среда, опыт, мастерство, риск, стоимость и, возможно, многие другие причины. Однако нет метрик, показывающих, что «контейнеры DI — это плохо» (как говорят Java-программисты, в то время как .NET-программисты получают поддержку DI «из коробки» в .NET Core), «функции должны иметь длину не более 10 строк» ​​(а затем вы видите, что пул реквесты в коде ядра Linux делают совершенно по-другому), или «чистые функции лучше, чем нечистые» (а затем вы заворачиваете свой код в морской узел, чтобы создать конечный автомат без состояния в нём).

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

Источник: https://blog.adamfurmanek.pl/2022/07/16/types-and-programming-languages-part-15/
👍10
День 1306. #TypesAndLanguages
7. Инкапсуляция или Публичное Всё
Сейчас немного разожгу:
Всё должно быть публичным. Не используйте модификатор private вообще.

А теперь давайте обсудим это немного подробнее.

Одно из свойств инкапсуляции состоит в сокрытии внутреннего устройства. Это может выражаться в различных формах. Самая простая из них — геттеры и сеттеры. Мы делаем поля приватными, а затем открываем методы доступа для чтения и записи. Однако всё может быть общедоступным, и при этом мы всё равно можем скрывать внутренности. Как это возможно?

Бывают ситуации, когда нам нужно получить доступ к некоторым внутренним элементам или изменить их. Либо напрямую, либо через рефлексию, либо через взлом памяти. Проблема в том, что в большинстве случаев компиляторы нам не помогут в таких ситуациях. Компиляторы не будут проверять вашу рефлексию, верна она у вас или нет. Они не остановят компиляцию вашего небезопасного низкоуровневого кода, если он неверен. Это значительно усложняет работу с внутренними компонентами, поскольку они могут быть изменены практически в любое время, а у нас нет механизма, позволяющего обнаружить, что изменение нарушает наш код. Однако такой проблемы не бывает, когда всё публично. Если вы обращаетесь к внутренним компонентам напрямую, компилятор сообщит вам, когда произойдёт критическое изменение. Это позволяет находить ошибки намного раньше и намного проще.

Но как мы можем скрывать внутренности, когда всё публично? Ответ: представления. С помощью различных представлений, которые в большинстве языков реализованы через интерфейсы, мы можем предоставлять абстракцию, но по-прежнему иметь доступ к внутренним компонентам. Представьте, что у нас есть следующий класс:
class Foo {
public void Bar() { … }
private void BarInternals() { … }
}

Можно сказать, что это хорошо, потому что класс скрывает BarInternals, делая его приватным. Таким образом, вызывающая сторона не может получить доступ к внутренним компонентам и не нарушит их. Однако можно использовать интерфейсы:
interface IFoo {
void Bar();
}
class Foo : IFoo {
public void Bar() { … }
public void BarInternals() { … }
}

Теперь любой «порядочный» клиент, вызывающий объект, должен использовать Foo через интерфейс IFoo, поэтому метод BarInternals останется недоступным. Дело не в том, что он приватный или доступ заблокирован контроллером политик. Метода просто нет! Однако, если кому-то нужно схитрить, он может привести IFoo к Foo и получить поддержку компилятора, чтобы убедиться, что BarInternals существует.

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

Примечание: От себя добавлю, что делать совсем всё публичным, наверное, чересчур радикально. На моей практике доступ к внутренностям нужен не так часто, поэтому такой проблемы обычно не возникает. Но в принципе, подход с сокрытием через интерфейс интересный.

Что скажете?

Источник: https://blog.adamfurmanek.pl/2022/07/23/types-and-programming-languages-part-16/
Автор оригинала: Adam Furmanek
👍12👎3
День 1313. #TypesAndLanguages
Ваш Язык Влияет на то, Как Вы Думаете. Начало
Кажется очевидным, не так ли? Ваш язык диктует правила, и вам нужно играть по правилам. Однако последствия гораздо серьёзнее, чем вы думаете. Дело не только в том, что мы выбираем решения, которые просты в использовании на данном языке. Также мы считаем некоторые решения «плохими в целом», потому что они могут создавать некоторые проблемы на данном языке. Однако мы должны помнить, что концепции никогда не бывают хорошими или плохими. Именно то, как мы их используем, делает их таковыми.

Контейнеры внедрения зависимостей
Java-программисты часто заявляют, что DI-контейнеры — это плохо, и мы не должны их использовать. Вы можете найти сообщения в блогах, доклады на конференциях или дискуссии о том, почему мы не должны использовать DI и почему следует отказаться от Spring. В то же время Microsoft поддерживает DI-контейнер непосредственно в платформе .NET Core. Как так?

Дело не в том, что DI-контейнеры хороши или плохи. Дело в том, как мы их используем. Все методы экземпляра в Java являются виртуальными. Это делает переопределение очень простым и возможным почти всегда. Это привело к массовому использованию cglib для создания динамических прокси во время выполнения. Контейнеры DI в Java позволяют внедрять transient объекты в синглтоны — что является плохой практикой и отслеживается в .NET. То же самое касается серверов приложений — они гораздо популярнее и мощнее в мире Java, чем в .NET.

Поскольку проще использовать генерацию кода, проще делать более мощные и более опасные решения. А это приводит к проблемам: чтобы использовать мощные решения, нужно их понимать и уметь не навредить себе. А поскольку у нас не так много времени на изучение наших инструментов, мы часто вносим неправильные изменения в нашу кодовую базу. Это приводит к тонким проблемам с транзакциями, потерей данных и т.п. Java-программисты заметили это и решили не использовать эти возможности. Однако дело не в том, что «контейнеры DI плохи». Это «то, как мы используем DI-контейнеры в мире Java, слишком опасно и приводит к множеству проблем». Просто изучите свои инструменты, и вы будете в большей безопасности.

Частичные моки и виртуальные методы
То же самое происходит и в тестах. Java позволяет переопределить почти любой метод экземпляра, поэтому создание частичного мока разрешено и поддерживается по умолчанию. С другой стороны, вам нужно явно пометить свой метод виртуальным в C#, поэтому обычно вы этого не делаете. Если вы это сделаете, люди могут начать задавать вопросы в код-ревью вроде «почему он виртуальный? вы переопределяете его?». Таким образом, вы не делаете его виртуальным и не можете использовать частичные моки. Совершенно иначе, чем в Java.

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

Множественное наследование
C++ допускал множественное наследование, и люди испугались проблемы алмаза. Java и C# решили полностью избежать этой проблемы, поэтому они множественное наследование запретили. Однако в то же время они заблокировали множественное наследование состояний и множественное наследование реализации. Единственной разрешённой формой было множественное наследование интерфейсов. К сожалению, проблема алмаза реально существует в языках C# и Java с самого начала.

Поэтому люди испугались наследования. В то же время такие языки, как Scala, допускают почти полное множественное наследование, и это не проблема. Даже Java и C# представили реализацию интерфейса по умолчанию, которую можно использовать для реализации миксинов и множественного наследования состояний. А что, теперь стало можно? Опять же, нельзя сказать, что множественное наследование хорошо или плохо само по себе. Проблема в том, как мы его используем.

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

Источник:
https://blog.adamfurmanek.pl/2022/07/23/types-and-programming-languages-part-16/
Автор оригинала: Adam Furmanek
👍7
День 1314. #TypesAndLanguages
Ваш Язык Влияет на то, Как Вы Думаете. Окончание
Начало

Наследование против композиции
Что из этого мы должны использовать? «Очевидно — композицию. Наследование — это плохо». Опять же, легко обобщать, но это не обязательно правильно.

Возможности ООП активно использовались на заре Java. Наследование было прорывом, глубокая классовая иерархия не удивляла. Как ни странно, божественные объекты также были популярны. Прошло время, и люди поняли, что помещать всё в класс длиной 8000 строк — не лучшая идея. Так что нам делать? «Запретить наследование, использовать композицию».

На самом деле дискуссия уходит гораздо глубже. Должны ли мы использовать сценарий транзакции и анемичную модель предметной области? Это хорошо? Или нам следует использовать классы с сохранением состояния и предметно-ориентированное проектирование?

Наследование не является ошибкой. Вам нужно понять концепцию и подумать, где она работает хорошо. Не запрещайте его только потому, что вы боитесь божественных классов.

Goto
Конечно, вы не можете использовать goto. Его надо запретить и убрать из языка. Теперь зайдите на GitHub и посмотрите варианты использования. Вы удивитесь.

Преждевременная оптимизация
Этим сильно злоупотребляют. Полная цитата гласит: «Мы должны забыть о неэффективности примерно в 97% случаев: преждевременная оптимизация — корень всех зол». Однако это относится к микрооптимизациям, таким как i++ против ++i. И да, такая оптимизация, вероятно, не нужна, так как наш код работает медленно из-за других вещей.

В то же время не следует пренебрегать фактором оптимизации. Думайте об алгоритме, а не о циклах под капотом. Удалите ненужные коллекции и структуры данных, но не забивайте голову работой по выравниванию битов.

Однако очень легко снизить производительность, используя возможности языка. Отличным примером является LINQ в .NET — первое, что вы делаете для ускорения работы, — это избавляетесь от LINQ. Хотя по умолчанию он доступен.

Зелёные потоки
Зелёные потоки (если коротко, это потоки, управляемые средой выполнения, а не операционной системой) были популярны, но в какой-то момент подверглись жёсткой критике. В Microsoft написали об этом хорошую статью. C# представил асинхронность, которая позже была скопирована в другие языки (Python, JS, C++, Kotlin).

Однако Java реализовала асинхронность по-другому в своем проекте Loom. Они использовали виртуальные потоки под капотом. И тут в Microsoft внезапно решили поэкспериментировать с зелёными потоками в .NET.

Забавно, правда? Опять же, дело не в том, что концепция плоха. Дело в том, как мы это используем. Зелёные потоки также создают проблемы, особенно в отношении блокирующих операций, примитивов блокировки или interop-вызовов. Но это опять же о реализации, а не об идее.

Источник: https://blog.adamfurmanek.pl/2022/08/13/types-and-programming-languages-part-18/
👍6