Тонкости C# : то что вы всегда хотели знать, но боялись спросить

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Всем привет. Меня зовут Владислав Фурдак, я .NET-техлид и работаю c платформой .NET около 10 лет. Ранее писал на DOU на темы карьеры, а также технические статьи: «Асинхронность в C#», «Эволюция .NET-стека», «Как стать Full Stack разработчиком», а также недавно «Какие навыки необходимы хорошему разработчику» Эта статья покрывает небольшое количество вопросов, однако проводя последнее время большое количество технических интервью, сталкивался с тем, что даже люди с большим опытом могут путаться в них либо не знать каких-то нюансов самого языка C#. Думаю эта статья может быть полезна как новичкам, так и опытным разработчикам.

Базовый C#

Expression Tree и его отличие от делегата

Что такое делегат ? Экземпляр делегата — это инстанс класса, инкапсулирующий ссылку на метод. Делегаты — это безопасный способ производить вызов методов, цепочек методов, передавать ссылки на методы конкретных инстансов либо же на статические методы. В C# есть три основных типа делегатов : Func, Action и более устаревший Predicate. С появлением Action & Func уже отпала необходимость в своих типах делегатов. Но довольно часто можно увидеть подобные конструкции, особенно при работе с O/RM над разными источниками данных:

Expression<Func<int, bool>> exprTree = num => num < 5;

Мы видим, что это все тот же анонимный делегат, созданный с помощью лямбда синтаксиса, принимающий int параметр и возвращающий bool, но сам делегат является generic-параметром класса Expression Tree. Expression Trees / Expression / Дерево выражений — это механизм, который предоставляет API для работы с кодом как с метаданными. Вообще в C# существует множество механизмов работы с кодом из кода, кроме деревьев выражений, например:

  • Reflection — позволяет смотреть на код как на метаданные, производить динамические операции вызова, искать нужные методы и классы.
  • Reflection Emits — позволяет генерировать код на лету, напрямую используя промежуточный язык CLR — MSIL (IL)
  • CodeDOM — предшественник Roslyn, способ скомпилировать куски кода на ходу.
  • Roslyn — API для кодогенерации и динамической компиляции кода.

Разницу между CodeDOM & Roslyn можно почитать тут. Возвращаясь к деревьям выражений — основная идея в том, что вы можете, как на ходу собрать кусок кода, руководствуясь какой-то логикой, так и проанализировать ранее собранный Expression, преобразовав его, например, в язык запросов к источнику данных (так работает Entity Framework). Также вы можете скомпилировать его и запустить на исполнение. Так вот, запись в форме Expression сразу создает дерево выражений на основе кода, который является анонимным делегатом. Кстати, деревья выражений иммутабельные, следовательно, к примеру для операций объединения предикатов через OR или AND — вам потребуется создать новое дерево, на основе двух старых. Эту проблему решают через ExpressionVisitor. Одна из лучших статей, которую я видел по этой теме.

Разница между Делегатом и событием (event)

Ключевое слово event ни что иное, как синтаксический сахар, позволяющий добавить дополнительное поведение свойству типа делегата. Проблема, которую синтаксически решают события — это сделать так, чтобы мы не могли случайно переопределить всю цепочку вызовов методов, добавленных к переменной типа делегата. Вы можете использовать псевдо методы (почти аналог геттера и сеттера) add & remove, за исключением что event будет оберткой вокруг переменной типа делегата. Также, если нам не нужны псевдо-методы add & remove ключевое слово event не позволяет сделать присвоение делегата извне класса, тем самым переписав всю цепочку вызовов.

Разница между локальными функциями и делегатами

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

Какой LINQ синтаксис лучше использовать ?

В C# есть два возможных синтаксиса использования LINQ:

  • Method chain, т.е. Цепочка методов
  • SQL Like, т.е. Псевдо sql код.

Лично я сторонник использования цепочки методов в большинстве случаев. Но некоторые выражения возможно реализовать только в SQL-like форме, а именно: использование let выражений. Так же можно имплементировать LEFT join & CROSS join в обеих формах, вопрос вкуса и читаемости кода:

Лично мне кажется, что sql-like форма тут более лаконична и понятна. В иных случаях, мне кажется, более целесообразно использовать форму записи цепочки методов.

Зачем нужны Properties & Fields

Property (свойство) — используется как внешний контракт класса Field (поле) — как приватный член класса. Интерфейсы синтаксически позволяют работать только со свойствами, так же объявляя для них псевдо методы get & set. Свойства могут быть virtual & override, но вот управлять приватностью псевдо методов в переопределенном свойстве в наследнике не выйдет.

Разница Readonly и const модификаторов

Const — на этапе компиляции производит inline подстановку значения. Readonly же защищает поле от изменения после выполнения конструктора. Следовательно, для того, чтобы значение из const подставилось в другие сборки — необходима их перекомпиляция.

Чем IQueryable отличается от IEnumerable

Чисто синтаксически — ничем, это одинаковые по сигнатуре интерфейсы. Но наличие интерфейса IQueryable у некоторого объекта подразумевает наличие, например extension методов, реализующих удаленный запрос к данным, используя этот объект. При работе с LINQ в Entity Framework, если вы напишите что-то вроде

//в переменной db лежит объект типа DbContext 
IEnumerable customers = db.Customers;
var findJohn = customers.Where(p => p.Name.Contains(“John”)).ToList();

То метод Where — будет взят из linq to entities, т.е. Произойдет вызов в базу данных, затем фильтрация. Если же вы используете Var customers = db.Customers.AsQueryable();, то на IQueryable будет вызван extension-метод, который может построить дерево выражений и вернуть его для вызова другого extension-метода ToList, где будет запущен код вызова в базу данных

Тонкости работы с null

По-умолчанию в C# < 8 версии либо с выключенным nullable reference type (C# 8) обычному ссылочному типу можно присвоить значение null на этапе компиляции. Далее, мы имеем такие средства работы с null: 1. Оператор условного возврата null. var variable = someObj?.Prop?.Value; В случае если у someObj  Prop = null, мы не получим исключение, а просто вернем null. Эта конструкция аналогична

if(someObj != null && someObj.Prop != null){
   variable = someObj.Prop.Value;
} 
else
{
   variable = null;
}

2. Оператор поглощения null

var variable = someObj.Prop ?? new PropClass();

Тут в случае Prop = null мы поглотим это значение с помощью создания нового инстанса PropClass. 3. Оператор условной инициализации В случае если значение переменной null, мы производим инициализацию, в противном случае — нет. Например someList[0] ??= new PropClass(); выполнит присвоение первому элементу листа значением нового инстанса, если там был null. Довольно удобно пользоваться конструкциями, вроде  a ?? b ?? c, либо d ??= e ??= f Код выше будет исполнен как:

a ?? (b ?? c)
d ??= (e ??= f)

Но все же советую использовать скобки для приоритетов операций, и избежания путаницы. Кроме того, операторы ?? и ??= могут быть перегружены для класса, что я с 99% вероятностью не рекомендую делать просто так :) 4. Ключевое слово default. Для ссылочных типов является синонимом null. Для значимых — пустого значения. Является более универсальным. 5. В случае использования тернарного оператора (condition ? expression1 : expression2) если в одном из экспрешенов возвращается null, то оба блока должны быть nullable. Ранее нам была доступна только структура для Nullable, которая могла сделать из ValueType переменную, которая может хранить еще и Null. Далее был создан синтаксический сахар для структуры Nullable, как оператор «. В C#8 мы получили Nullable reference types. Эта фича может быть включена как для проекта целиком так и для конкретного куска кода. Польза от этой фичи в том, что вам не нужно боятся что где-то будет null, если вы его туда не присваивали. Ранее это решалось библиотеками вроде Optional, это была своеобразная обертка над возможным null в функциональном стиле. Если в моделях вашего контроллера есть Non-nullable значение, то в случае отсутствия его в запросе — будет выброшено исключение валидации модели.

Работа с исключениями

Рассмотрим вопрос — чем отличается проброс исключения из блока catch через Throw и throw e :

catch (Exception ex)
{
throw; // Сохраняет оригинальный стек вызова
throw new MyException("failed", ex); // оборачивает старое исключение в новое
throw new MyException("failed"); // заменяет исключение
throw ex; // Очищает оригинальный стек вызова
}

Также реализовать поведение, схожее с

catch (Exception ex){
throw;
}

сохраняя стек оригинального вызова, можно использовав класс ExceptionDispatchInfo, делая это в любом месте:

ExceptionDispatchInfo exceptionDispatchInfo = null;
 
try
{
    // Код который вызывает Exception тут
}
catch (Exception ex)
{
    exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex);
}
 
// Тут какая-то логика
 
if (exceptionDispatchInfo != null)
{
    exceptionDispatchInfo.Throw();
}

Более подробно тут.

Тонкости работы с датой и временем

Работа с датой и временем всегда боль. Почти на каждом зрелом проекте я встречал баги, связанные с часовыми поясами или неправильной интерпретацией времени. Лично мое мнение (и не только), что хранение времени вместе с тайм зоной в классе DateTime чревато ошибками. Гораздо удобней хранить его либо в DateTimeOffset либо в Long, как Unix time, т.е. хранение даты и времени и работа с временем в целом в приложении должны быть отвязаны от часовых поясов (UTC), и они должны накладываться при необходимости в нашей бизнес-логике.

Ко и Контр — вариантность в C#

Ко и контр вариантность — это возможность производить неявное преобразование обобщенных параметров, в обобщенных делегатах и интерфейсах. Т.к. Ключевое слово out указывает, что на месте параметра T может быть сам класс так и его родительские классы, как в сигнатуре интерфейса IEnumerable:

public interface IEnumerable : IEnumerable
{
 IEnumerator GetEnumerator();
}

Для ключевого слова in — наоборот, сам класс и его производные. Для демонстрации приведу такой кусок кода, где все сразу станет понятно:

interface ISome<out OParam, in IParam>{
}

class InParam
{
}

class OutParamParent
{
}

class InParamDerived : InParam
{
}

class OutParam : OutParamParent
{
}

class Some<OParam, IParam>: ISome<OParam, IParam>
{
}

private void SomeMethod()
{
   ISome<OutParamParent, InParamDerived> some = new Some<OutParam, InParam>();
}

Если бы не ключевые слова in & out в интерфейсе ISome — неявное преобразование типов не работало бы.

Default interface implementation vs Abstract class

В C# 8 реализовали возможность иметь реализацию по-умолчанию в интерфейсах. Сравнивать эту фичу языка с абстрактными классами чисто по возможностям не совсем корректно. Сделано это было для обратной совместимости, например какая-то система реализует механизм плагинов, и чтобы не ломать плагины, разработанные сторонними разработчиками (т. е. Не заставлять их имплементировать новые члены интерфейса), заливать новый апдейт, то была добавлена возможность поставлять имплементацию по-умолчанию уже в интерфейсе. Использовать эту фичу не рекомендуется в иных кейсах.

Асинхронное программирование

Разница между Task и Thread

Класс Task является всего лишь моделью, переносящей состояние выполнения куска кода, переданного ей. За стратегию выполнения (т.е. определения потока выполнения) отвечает TaskScheduler, в случае если вы его не укажете явно — будет взят планировщик по-умолчанию (планировщик пула потоков). Класс Thread является объектной оберткой вокруг реального потока операционной системы, а также методами управления этим потоком. Поток, созданный с помощью инстанцирования класса Thread не будет помещен в пул потоков.

Что отличает вызовы await и .Result

  • Await — неблокирующий вызов, отпускающий вызывающий поток, ожидая результат.
  • .Result — блокирующий вызов, текущий поток будет удержан до завершения операции и возврата результата Таски.

Как работает Aggregate exception

Aggregate Exception — исключение, оборачивающее исключения в классе Task. Зачем оно было придумано ? Во-первых, задачи (Task) могут комбинироваться, где в результате будет получена новая задача, например методом WhenAll. Так вот, исключения всех участвующих задач будут обернуты в одно исключение AggregateException у задачи-результата этого вызова. Кстати, если вы захотите сделать await такой задачи, то будет выброшено только первое исключение.

Разница между async / await и Task API ContinueWith

Механизм async / await делает из метода стейт-машину под капотом, присоединяя продолжение после неблокирующего ожидания используя контекст синхронизации текущего потока как стратегию. ContinueWith никак не связан с async / await, производит продолжение цепочки тасок, когда таска в коллбеке этого метода завершена по какой-то причине. Кстати, по этой же причине если вы разместите внутри ContinueWith await какого-то метода, то после прерывания на неблокирующее ожидание, будет выполнен следующий ContinueWith, а затем только продолжение первого. Более подробно можно почитать в моей статье.

Вызов ConfigureAwait

Метод ConfigureAwait вызывается на классе TaskAwaiter, тем самым конфигурируя выставление контекста синхронизации для кода, который будет вызван после await. При вызове ConfigureAwait(false) — мы устанавливаем контекст по-умолчанию (контекст тред пула), тем самым прерывая наследование контекста, это было актуально при работе с классическим asp.net, а также в коде своих библиотек. В asp.net core это больше не актуально. Однако, если вы разрабоатываете на любом UI-фреймворке (winforms, wpf) хотите выполнить продолжение метода в пуле потоков (вместо UI потока), то можно вызвать await вместе с ConfigureAwait(false), но по-хорошему так лучше не делать.

Использование CancellationTokenSource

CancellationTokenSource (CTS) — фабрика CancellationToken’ов (CT). CT используется для передачи как параметр в Task при ее запуске, чтобы управлять механизмом согласованной отмены задачи. При вызове операции отмены в CTS (Cancel), мы можем использовать инстанс CT внутри кода таски чтобы проверить

  • Была ли отменена операция ? token.IsCancellationRequested, далее реализуя свою логику
  • Либо же вызывать время от времени ThrowIfCancellationRequested , который выбросит исключение, если на CTS произошла отмена. Главный нюанс здесь — если токен не был ассоциирован с Task и мы выбросили исключение — статус задачи будет Failed, в случае же ассоциации — статус будет Cancelled.

При работе с долгими запросами можно произвести инъекцию CTS прямо в контроллер, чтобы механизм asp.net в случае разрыва соеденения сам производил отмену в CTS.

Многопоточность

Чем отличаются Monitor, Semaphore и Mutex

Mutext и Semaphore — являются синхронизирующими конструкциями ядра операционной системы, используя объектные обертки в .NET, мы можем координировать кросс-процессные операции. Например, с помощью Mutex вы можете контролировать количество запущенных инстансов приложения. Mutex пропускает только один поток. Semaphore делает то же самое, но позволяет ограниченному числу потоков иметь доступ к ресурсу. Альтернативой Semaphore в .NET является SemaphoreSlim, не являющийся объектом ядра операционной системы. Monitor — объект .NET платформы, в C# используется внутри оператора lock, для синхронизации доступов к управляемым ресурсам.

Порядок в понятиях : Task, TAP и TPL

Множество людей путают Task, TAP и TPL.

  • Task — это условно механизм вызова кода и отслеживания его статуса, реализованный с выходом .NET 4.0.
  • TAP Task Asynchronous Pattern — асинхронный паттерн программирования, построенный вокруг механизма тасок.
  • TPL — Task Parallel Library — библиотека, использующая таски, но решающая иную задачу : распараллеливание вычислений. Так же мощной составляющей TPL есть DataFlow, реализует модель акторов и дает возможность параллелить вычисления.

Работа CLR

ApplicationDomain в .NET Core

Домены приложения были упразднены с выходом .NET Core, однако, была оставлена некоторая обратная совместимость по обработке ошибок. Сделано это было с целью перекладывания ответственности на изоляцию на более надежные и менее затратные технологии, вроде контейнеризации

.NET Standard и NetCoreApp

С выходом .NET 5 версионирование с помощью .NET Standard было упразднено. Однако, для прошлых версий .NET Core и .NET Framework .NET Standard указывает на совместимости, сделано такое разделение было для того, чтобы разные версии библиотек могли полагаться на различные версии .NET Core & .NET Framework имплементирующие какой-то конкретный .NET Standard (с обратной совместимостью).

Подписанные сборки и строгие имена

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

InternalsVisibleToAttribute

Для того, чтобы иметь возможность преодолеть ограничения модификаторов доступа, например, если нам нужно будет сослаться в тестовой сборке на internal класс — можно использовать атрибут InternalsVisibleToAttribute

.NET 5

В .NET 5 было добавлено довольно много нововведений в работу самой CLR, например:

  • Single file apps — убрана необходимость распаковки во временную директорию.
  • App trimming — Tree Shaking для .NET сборки.
  • Улучшение производительности работы кода
  • Улучшение производительности сборки мусора
  • Изменения в JIT компиляции и новый режим Ready to run
  • Изменения в многопоточной работе

И много чего еще. Вот неплохой доклад на эту тему.

В заключение

Если вы начинающий .NET разработчик, вступайте в мое Telegram комьюнити, если же вы опытный синьор разработчик/техлидер, можете написать мне на фейсбук, я добавлю вас в закрытый Telegram канал, для обмена опытом и обсуждения сложных технических вопросов.

👍НравитсяПонравилось15
В избранноеВ избранном14
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

СОЛИД, СОЛИД забыли, без него этот опросник будет неполным!!!!

Похоже на допросный список на собеседовании :)

ConfigureAwait(false)

Несколько раз видел карго-культ этого долбанного ConfigureAwait-а, когда его, начитавшись блогов, начинали бездумно лепить вообще во все асинхронные вызовы, и это было чуть ли не частью code convention на проекте.

ПС: Статья хоть и поверхностная, но хорошая. Цель статьи — не разжевать упомянутые темы, а обратить на них внимание тех, кто про них не знал и, обязательно с ними когда-то столкнётся.

Почему карго культ? Вы считаете уменьшение накладных расходов на копирование контекста бесполезным или Вы знаете другой способ этого избежать?

в asp.net core уже контекст устанавливается по-дефолту тредпуловский.

Не в .NET Core/5 не в MVC 5 контекст же не работает вроде. В .NET Core/5 он null. В MVC 5 ConfigureAwait(ture) тоже ничего не сделает.

null = default = тредпул контекст

asp.net это конкретное окружение. Тут мы хоть что-то знаем о TaskScheduler, его дефолтном поведении и можем строить предположения. В библиотеках этого знания нет. Соответственно для того, чтобы библиотека имела одно и тоже поведение везде куда будет подключена нужно везде писать .ConfigureAwait(false).

Проблема в том что библиотеке по-дефолту будут наследовать контекст синхронизации вызывающего потока да, писал как-то об этом dou.ua/...​asynchronous-programming

Логика и интуиция подсказывают мне, что в стандартных сценариях всегда лучше полагаться на заложенное дефолтное поведение и внутренние оптимизации самого framework. Поэтому ConfigureAwait(false) я рассматриваю только как инструмент точечного твикинга производительности в некоторых hot paths, а не что-то, что нужно бездумно лепить повсюду. К тому же это может приводить к непредсказуемым проблемам. То есть его нужно использовать только эпизодически, когда ты точно знаешь, что делаешь, и что тебе точно не нужен вызывающий контекст, даже каким-то неочевидным образом.

devblogs.microsoft.com/...​i-use-configureawaitfalse

When should I use ConfigureAwait(false)?
if you’re writing app-level code, do not use ConfigureAwait(false)
if you’re writing general-purpose library code, use ConfigureAwait(false)

Т.е. все таки не карго культ.
Редко какое приложение, особенно в крупной компании имеет много внутренней логики. Обычно все поразнесено по разным слоям и соответственно библиотекам. А в библиотеках нельзя писать без .ConfigureAwait(false).
В случае приложений это по большей части твики, которые еще и правильно применить нужно. Но даже в приложениях не всегда можно написать код без DeadLock не используя .ConfigureAwait(false).
Нужно понимать почему ты пишешь то или иное. В идеале понимать как работает асинхронщина в C#. Проблема в том что это сложно и очень многие не понимают. Вот и пытаются в разных статьях упростить это и дать рекомендации которые будут работать лучше в большинстве случаев.

Редко какое приложение, особенно в крупной компании имеет много внутренней логики. Обычно все поразнесено по разным слоям и соответственно библиотекам. А в библиотеках нельзя писать без .ConfigureAwait(false).

95% библиотек для .NET веб проектов будет написано под ASP.NET Core, и там можно писать без ConfigureAwait(false). Даже если они в 5% случаев будут переиспользоваться в контексто-зависимых приложениях, то вводить это стоит на уровне подобных приложений (консоль, десктоп). Для либ — только если они специфичны для этих окружений. Так как в 95% это будет пустой тратой времени и существенной потерей удобства.

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

Если у вас 95% приложений это Asp.Net это не значит что так у всех. У нас к примеру ~40% библиотек используются в Asp.Net, Xamarin и WPF проектах.
Если Вы понимаете такие тонкости и решили, что в вашем проекте нет надобности это писать, то это выбор вашей команды, вашего архитектора или кто там отвечает за такие решения.
Не нужно писать что это вообще бесполезная штука и карго культ. А то получается бесполезно и карго культ, а потом сотни вопросов на StackOverflow почему у меня дедлок, я всегда так делал.

У нас 100% это ASP.NET)
Я думал WPF еще 5 лет назад сдох, когда я хотел найти с ним работу, и было 2 вакансии на весь Киев.

Хорошо Вам. Можно не думать о таком и писать чуть меьше кода. :)

Та че тут думать, хочешь что бы код после await выполнился в том же потоке, пишешь ture(точнее нечего не пишешь), если все равно, то fasle, это в WPF/UWP или любых либах, которые могут на них использоваться. А в ASP можно ничего не писать.

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

Где тут тонкости? Это база высеченная топором, при чем здесь она представлена в настолько сжатом виде что не несет особой ценности.

Согласен что тонкостей куда больше существует, но то что для вас база многие и не знают.

Single file apps — убрана необходимость распаковки во временную директорию.

www.joelonsoftware.com/...​-sir-may-i-have-a-linker

Жду вместе с Joel уже 17 лет)

App trimming — Tree Shaking

30 mb — это конечно не совсем тот размер, который ожидается.

Но некоторые выражения возможно реализовать только в SQL-like форме, а именно: использование let выражений.

Так ведь в конечном счёте, любой let превратится в цепочку вызовов.
Цепочка является по сути более низкоуровневым вариантом записи.

www.joelonsoftware.com/...​-sir-may-i-have-a-linker

Жду вместе с Joel уже 17 лет)

Instead, .NET has this idea of a „runtime” ... a big 22 MB steaming heap of code

Прямо всплакнул

Да, я помню, в XP не было .NET, этот инсталлер приходилось тащить на CD.
Флешки еще не стартанули.

В нас зараз на Qt AppImage 170 МБ.

Диски дешеві, в чому проблема? Зате зручно.

Спасибо за очередную интересную и полезную статью.

Хотел бы уточнить несколько моментов:

Ко и контр вариантность — это возможность производить неявное преобразование обобщенных параметров, в обобщенных классах, делегатах и интерфейсах.

Ко и контр вариантность все-таки относится к интерфейсам и делегатам. Скорее всего именно это имелось в виду, но это ключевой момент, который должен быть четко обозначен — нельзя добавить ключевые слова in и out к параметрам в обобщенном классе, только в интерфейсе или делегате.

Не хватает параметров в интерфейсе
interface ISome<out OParam, in IParam>

Ковариантность поддерживается массивами, но нужно знать о подводных камнях (пост на эту тему)

К этой теме также относится «covariant returns» в C# 9.

.NET 5
— Single file apps

На самом деле Single file app появился в .NET Core 3.0. По сути это архив, который распаковывается во временную директорию

%USERNAME%\AppData\Local\Temp\.net\{AppName}
из которой запускается приложение (создавая определенные проблемы, например если логирование по умолчанию настроено в директорию с исполняемым файлом). В .NET 5 убрана необходимость распаковки во временную директорию.

Спасибо за комментарий,
1. согласен
2. да, опечатка, исправлю
3. да можно уточнение добавить.

a ?? (b ?? c)
d ??= (e ??= f)

В адекватных проектах отбивают руки за такое и с позором заставляют переписывать ифами.
У нас на одном проекте заставляли даже явно писать if (something == true) {}. И это очень важно (особенно в конце рабочего дня).

И почему эти проекты адекватные? По-моему поглощать проверки на null очень удобно для повышения читаемости.
В тех же исходниках asp.net core подобные штуки на каждом шагу

var context = _context ?? _contextAccessor?.HttpContext;
Logger = logger ?? throw new ArgumentNullException(nameof(logger));

Кстати дополнительная фишка, что в правой части null coalescing operator можно выбрасывать исключение, а не только возвращать значение.

if (something == true) {}.

я считаю перебор, может люди невнимательные там работали.

a ?? (b ?? c)

эта конструкция чисто для ознокомления, писать в продакшене лучше так не стоит, а декомпозировать.

В адекватных проектах настраивают linter и никто даже не заморачивается руки отбивать / прибивать

Линтер не поможет от багов типа var sum = price ?? 0 + tax ?? 0 + surcharge ?? 0.
Угадайте что будет если tax != null?
Правильно — без разницы т.к. при price != null остальная часть выражения значения не имеет.

Кроме линтера хорошо бы уметь пользоваться приоритетами операций и писать тесты.
Тесты сами по себе тоже не спасут т.к. далеко не все понимают, что если запись выглядит проще, ее сложность никуда не делась и количество различных переходов в IL будет практически таким же как если бы мы писали с помощью if.
Т.е. лучше не писать длинные цепочки с null coalesce оператором. Тоже касается null-propagation и тем более в миксе с математикой. Потом это читается легко, а работает не так как читается с первого взгляда. Особенно это не совпадает со взглядом новичков, которые склонны упрощать сложные вещи.

Вот тут хорощо описано про приоритеты и ассоциативность:
docs.microsoft.com/...​guage-reference/operators.

Это вообще к чему написано?
Линтер не относится к проверке ошибок.

К тому, что руки прибивать/отбивать все-таки стоит, если люди пишут слишком сложные выражения. И линтер тут не поможет.

if (something == true) {}. И это очень важно

Зочеееем?

По поводу ??/??= соглашусь от части. Если это один раз в одной строке — хорошее решение, если уже начинаются такие выражение — это довольно сложно для восприятия.

if (something == true) {}.

это явно неадекватно :)
Нужно просто булевскую переменную называть не something а isSomethingChanged, isSomethingExists etc... и тогда все будет понятно :)

LEFT JOIN
CROSS JOIN

Когда вы в Сришетке что-то подобное городите — трижды убедитесь, что в это сходу смогут въехать ваши коллеги прямо сейчас, и вы сами через пару месяцев не будете лупать от удивления шо оно за костыль этот DefaultIfEmpty. Если не уверены — напишите обычными циклами без вые*онов. Оно и попонятнее и работать будет побыстрее.

DefaultIfEmpty

вполне нормальная практика для организации LEFT JOIN средствами EF, например.

Ну, если вы напишите это в форме цепочки методов — ясней код не станет. С linq-like ИМХО просто явнгей выглядит.
Ну и в linq2entities можно как угодно писать, там нет цели трансляции в SQL. Хоть циклами)

Разница между Делегатом и событием (event)
Ключевое слово event ни что иное, как синтаксический сахар, позволяющий добавить дополнительное поведение свойству типа делегата. Проблема, которую синтаксически решают события — это сделать так, чтобы мы не могли случайно переопределить всю цепочку вызовов методов, добавленных к переменной типа делегата.

Ну такое поведение можно реализовать и обычными свойствами.
В C# есть поддержка events на уровне языка, которую от меня хотели услышать когда-то на собеседовании, а именно — дернуть event можно только внутри класса, в котором он объявлен независимо от параметров доступа, его нельзя вызвать из вне класса, вызвать же делегат можно откуда угодно где он виден.

Интересно, а как можно обычными свойствами заблокировать изменение делегата?

Пронаследоваться и переопределить присваивание?

Подписаться на комментарии