.NET Разработчик
6.51K subscribers
427 photos
2 videos
14 files
2.04K links
Дневник сертифицированного .NET разработчика.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День сто восемьдесят пятый. #ЗаметкиНаПолях
Многопоточность.
5. Задания. Продолжение
Автоматический запуск задания по завершении предыдущего
Вызов метода Wait или свойства Result при незавершённом задании скорее всего приведёт к блокировке текущего потока и появлению в пуле нового потока. Чтобы этого не происходило, можно переписать код из предыдущих примеров так, чтобы результат выводился в новом задании, которое стартует после завершения предыдущего:
var t = new Task<int>((n) => Calc((int)n), 10000);
t.Start();
Task cwt = t.ContinueWith(task => Console.WriteLine($"Результат: {task.Result}"));
При этом поток, исполняющий этот код и ожидающий завершения каждого из этих заданий, не блокируется, а выполняет другую работу или возвращается в пул. Поскольку метод ContinueWith также возвращает Task, то и к нему, в свою очередь, можно применять Wait, Result или ContinueWith.
Кроме того, вызвать ContinueWith можно с флагами TaskContinuationOptions. Наиболее интересные из них:
- AttachedToParent – присоединяет задание к родительскому (родительское задание не закончится, пока не исполнятся все дочерние)
- ExecuteSynchronously – новое задание будет выполнено тем же потоком, что и предыдущее
- OnlyOnCanceled – выполнять, если предыдущее задание отменено
- OnlyOnFaulted – выполнять, если предыдущее задание привело к ошибке
- OnlyOnRanToCompletion – выполнять, если предыдущее задание завершилось успешно
- NotOnCanceled, NotOnFaulted, NotOnRanToCompletion – соответствующие отрицательные флаги.
Вывод результата в предыдущем примере можно переписать с этими флагами:
t.ContinueWith(task => 
Console.WriteLine($"Результат: {task.Result}"),
TaskContinuationOptions.OnlyOnRanToCompletion);
t.ContinueWith(task =>
Console.WriteLine($"Ошибка: {task.Exception}"),
TaskContinuationOptions.OnlyOnFaulted);
t.ContinueWith(task =>
Console.WriteLine("Операция отменена"),
TaskContinuationOptions.OnlyOnCanceled);

Фабрики заданий
Если необходим набор заданий в одном состоянии и с одинаковыми параметрами, можно создать фабрику заданий TaskFactory:
string[] files = null
string[] dirs = null;
string docsDir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);

var tf = new TaskFactory();
var tasks = new[]
{
tf.StartNew(() => files = Directory.GetFiles(docsDir)),
tf.StartNew(() => dirs = Directory.GetDirectories(docsDir))
};
tf.ContinueWhenAll(tasks, completed => {
Console.WriteLine("{0} содержит: ", docsDir);
Console.WriteLine(" {0} папок", dirs.Length);
Console.WriteLine(" {0} файлов", files.Length);
});
В коде выше создаётся новая фабрика заданий. Ей можно передать общий токен отмены, параметры создания и продолжения заданий и т.п. Далее задания создаются вызовом метода StartNew. Впоследствии можно добавить задание-продолжение по окончании всех или любого из заданий, вызвав соответственно ContinueWhenAll или ContinueWhenAny. В примере выше в фабрике заданий два параллельных задания подсчитывают количество папок и файлов в папке «Мои документы» текущего пользователя, а после завершения всех заданий результаты выводятся в консоль.

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

Источники:
- Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 26.
-
https://docs.microsoft.com/dotnet/api/system.threading.tasks.task.factory
День сто восемьдесят шестой. #юмор
День сто восемьдесят седьмой. #ЗаметкиНаПолях
Многопоточность.
6. Класс Parallel
Для реализации наиболее популярных сценариев посредством заданий создан класс System.Threading.Tasks.Parallel. Например, следующий код вызывает метод DoWork последовательно:
for(int i = 0; i < 1000; i++) DoWork(i);
Вместо этого можно распределить обработку между несколькими потоками пула:
Parallel.For(0, 1000, i => DoWork(i));

Аналогично метод foreach
foreach(var item in collection) DoWork(item);
можно заменить
Parallel.ForEach(collection, item => DoWork(item));

Параллельно запустить несколько методов можно с помощью метода Invoke:
Parallel.Invoke(
() => Method1(),
() => Method2(),
() => Method3());

Особенности:
1. Вызывающий поток также принимает участие в обработке и не блокируется, за исключением случаев, когда он заканчивает выполнение работы, а остальные потоки ещё нет.
2. Если какая-либо операция выбрасывает необработанное исключение, то вызванный вами метод класса Parallel выбросит исключение AggregateException (см. https://t.iss.one/NetDeveloperDiary/221)
3. Использовать методы класса Parallel выгодно, лишь когда задания действительно могут выполняться параллельно, а также не обращаются к общим ресурсам.

Опции
Методам класса Parallel можно передать объект ParallelOptions со следующими свойствами:
- CancellationToken (токен отмены операции, по умолчанию CancellationToken.None)
- MaxDegreeOfParallelizm (максимальное количество рабочих элементов, по умолчанию -1 равно числу доступных процессоров)
- TaskScheduler – планировщик заданий.

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

Источник: Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 26.
День сто восемьдесят восьмой.
C# In Depth
Еще полгода назад писал про эту книгу Джона Скита (Jon Skeet) и только недавно до неё «дошли руки». Ну, что сказать. Если Рихтер – мастрид для C# мидл-сениор разработчика, то Скит – это абсолютный мастрид после Рихтера. Признаюсь честно, я Рихтера читал дважды. После первого прочтения не осталось в голове почти ничего (практики не было тогда). А после второго в принципе бОльшая часть контента более-менее запомнилась, но осталась парочка серьёзных проблем. Во-первых, всё равно в голове получилась некоторая каша, и многие понятия остались существовать в каком-то вакууме. Не знаю, проблема ли в организации книги, либо сугубо моего восприятия, но факт остаётся фактом. Во-вторых, и это, наверное, главная проблема. Моё издание книги вышло в 2012 году. И даже последнее на данный момент издание 2013 года и охватывает C# 5. А сейчас уже выходит 8я(!!!) версия языка. Таким образом, несмотря на то, что книга невероятно полезна с теоретической точки зрения, она «страшно далека от реальности». Поэтому получается, что в книге есть главы на десятки страниц, например, про делегаты или асинхронные операции, но очень коротко описано то, как они действительно используются сейчас (лямбда-выражения, LINQ или async/await). То есть в знаниях получается «дыра» между хорошей теорией 6-летней давности и современными тенденциями разработки. Это не значит, что описанная теория совсем бесполезна. Но получается что-то вроде того, как один из наших преподавателей в вузе считал, что изучать программирование надо с ассемблера (Ну... как бы, да... но можно и нет :) )
И в этом смысле прочитать Скита именно ПОСЛЕ Рихтера, на мой взгляд, крайне полезно. В моём случае книга Скита – это 4е издание 2019 года, которое описывает самые последние изменения в языке (вплоть до краткого обзора 8й версии). Но самое главное, что все инструменты языка рассмотрены в хронологическом порядке: от первой версии до седьмой. Про каждый инструмент подробно описано: что это, когда возникло, почему возникло, что изменилось от версии к версии и где это применяется. Это позволяет расставить все ваши знания по полочкам и связать различные знания между собой. К примеру, большинство изменений в C#3 (неявное объявление переменных через var, инициализаторы объектов и коллекций, анонимные типы, методы расширения и т.п.) введены для того, чтобы использовать лямбда-выражения и LINQ. И вся книга построена по этому принципу.
Кроме того, в отличие от Рихтера, Скит пишет в очень простой, понятной и дружелюбной манере, поэтому книгу, даже по-английски, читаешь не через силу (потому, что надо), а с удовольствием, как набор постов в блоге.
Далее на канале периодически буду приводить выдержки из книги, которые показались наиболее интересными.

Источники:
- Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012.
- Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019.
День сто восемьдесят девятый. #ЗаметкиНаПолях
Многопоточность
7. Шаблоны Асинхронного Программирования
.NET предоставляет три шаблона для выполнения асинхронных операций:
1. Модель асинхронного программирования (APM), также называемый шаблоном IAsyncResult, является устаревшей моделью, использующей интерфейс IAsyncResult для обеспечения асинхронного поведения. В этом шаблоне для синхронных операций требуются методы Begin и End (например, BeginWrite и EndWrite для реализации асинхронной операции записи). Этот шаблон больше не рекомендуется для новых разработок. Например, рассмотрим метод Read, который считывает указанное количество данных в буфер, начиная с указанного смещения:
public class MyClass
{
public int Read(byte [] buffer, int offset, int count);
}
В APM нужно реализовать методы BeginRead и EndRead:
public class MyClass
{
public IAsyncResult BeginRead(
byte[] buffer, int offset, int count,
AsyncCallback callback, object state);
public int EndRead(IAsyncResult asyncResult);
}

2. Асинхронный шаблон на основе событий (EAP) является устаревшим шаблоном на основе событий для обеспечения асинхронного поведения. Для этого требуется метод с суффиксом Async и одно или несколько событий, делегатов обработчиков событий и типов, производных от EventArg. EAP был представлен в .NET Framework 2.0. Больше не рекомендуется для новых разработок. Для примера выше в EAP потребуется метод и событие:
public class MyClass
{
public void ReadAsync(byte[] buffer, int offset, int count);
public event ReadCompletedEventHandler ReadCompleted;
}

3. Асинхронный шаблон на основе задач (TAP) использует общий метод для инициирования и завершения асинхронной операции. TAP был представлен в .NET Framework 4. В настоящее время это рекомендуемый подход к асинхронному программированию в .NET. Языковую поддержку для TAP обеспечивают ключевые слова async и await в C#. Для примера выше в TAP потребуется реализация одного метода:
public class MyClass
{
public Task<int> ReadAsync(byte[] buffer, int offset, int count);
}

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

Источники:
- Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 27.
-
https://docs.microsoft.com/ru-ru/dotnet/standard/asynchronous-programming-patterns/
День сто девяностый. #ЧтоНовенького
Предпросмотр Инструментов в Visual Studio
Обновлено окно Предпросмотр Инструментов (Preview Features) в разделе Инструменты > Параметры > Среда (Tools > Options > Environment). Новый вид предоставляет больше информации и возможность оставить отзыв о новых функциях. Пока они находятся в разработке, вы можете отключить любую из них, если столкнетесь с проблемами. Microsoft также рекомендует оставлять отзывы о любых проблемах, с которыми вы столкнётесь. Список на этой странице включают функции на ранней стадии разработки, которые влияют на существующую функциональность, те, которые еще дорабатываются, а также экспериментальные инструменты.
Поскольку разработка нового функционала идёт постоянно, список в окне Предпросмотр Инструментов будет изменяться в каждом выпуске Visual Studio, так как некоторые функции добавляются в продукт, а другие удаляются.

Источник: https://devblogs.microsoft.com/visualstudio/preview-features-in-visual-studio/
День сто девяносто первый. #ЗаметкиНаПолях
Многопоточность
8. В чем разница между асинхронным программированием и многопоточностью?
Эти понятия часто путают. Многие думают, что многопоточность и асинхронность - это одно и то же, но это не так. Обычно помогает аналогия. Вы готовите в ресторане. Приходит заказ на яйца и тосты.
- Синхронно: вы готовите яйца, затем вы готовите тост.
- Асинхронно, однопоточно: вы начинаете готовить яйца и устанавливаете таймер. Вы начинаете готовить тосты и устанавливаете таймер. Пока они готовятся, вы убираетесь на кухне. Когда таймеры выключаются, вы снимаете яйца с огня и тосты из тостера и подаете их.
- Асинхронно, многопоточно: вы нанимаете еще двух поваров, один для приготовления яиц и один для приготовления тостов. Теперь у вас есть проблема координации поваров, чтобы они не конфликтовали друг с другом на кухне при совместном использовании ресурсов. И вы должны заплатить им.

Итак, значит ли это, что многопоточность - это только один вид асинхронности? Потоки - это рабочие; асинхронность - это задачи. В многопоточных рабочих процессах вы назначаете задачи работникам. В асинхронных однопоточных рабочих процессах у вас есть график задач, где некоторые задачи зависят от результатов других; По завершении каждой задачи вызывается код, который планирует следующую задачу, которая может быть запущена, с учетом результатов только что выполненной задачи. Но вам (надеюсь) нужен только один работник для выполнения всех задач, а не один работник на задачу.
Это помогает понять, что многие задачи не связаны с процессором. Для задач, связанных с процессором, имеет смысл нанять столько рабочих (потоков), сколько имеется процессоров, назначить по одной задаче каждому рабочему, назначить по одному процессору каждому рабочему, и каждый процессор будет выполняет только работу по вычислению результата как можно быстрее. Но для задач, которые не ждут обработки процессором, вам вообще не нужно назначать работника. Вы просто ждёте сообщения о том, что результат доступен, и делаете что-то еще, пока ждёте. Когда это сообщение прибудет, вы можете запланировать продолжение выполненной задачи в качестве следующего пункта в вашем списке дел.

Источник: https://stackoverflow.com/questions/34680985/what-is-the-difference-between-asynchronous-programming-and-multithreading
День сто девяносто второй. #ЗаметкиНаПолях
Заметки по обобщённым типам
1. Инициализация и состояния обобщённых типов
Если выполнить следующий код:
Console.WriteLine(typeof(List<int>));
Console.WriteLine(typeof(List<string>));
На экран выведется следующее:
System.Collections.Generic.List`1[System.String]
System.Collections.Generic.List`1[System.Int32]
List`1 означает обобщённый тип List арности 1 (один параметр типа), а аргументы параметра типа указаны в квадратных скобках. Таким образом, List<int> и List<string> - это фактически разные типы, которые созданы из одного и того же определения обобщённого типа List<T>. Они называются закрытыми составными типами (closed, constructed type). Это относится не только к тому, как типы используются, но и к тому, как они инициализируются и как обрабатываются статические поля. Каждый закрытый составной тип инициализируется отдельно и имеет собственный независимый набор статических полей. Следующий листинг демонстрирует это с помощью простого обобщённого счетчика.
class GenericCounter<T>
{
private static int value;
static GenericCounter()
{
Console.WriteLine("Инициализация счётчика для {0}", typeof(T));
}
public static void Increment()
{
value++;
}
public static void Display()
{
Console.WriteLine("Счётчик для {0}: {1}", typeof(T), value);
}
}

GenericCounter<string>.Increment();
GenericCounter<string>.Increment();
GenericCounter<string>.Display();
GenericCounter<int>.Display();
GenericCounter<int>.Increment();
GenericCounter<int>.Display();

Вывод будет следующим:
Инициализация счётчика для System.String
Счётчик для System.String: 2
Инициализация счётчика для System.Int32
Счётчик для System.Int32: 0
Счётчик для System.Int32: 1
Из этого можно сделать два вывода:
1. Значение GenericCounter<string> не зависит от GenericCounter <int>.
2. Статический конструктор запускается дважды: один раз для каждого закрытого составного типа. Если бы статического конструктора не было, было бы меньше гарантий того, когда точно происходит инициализация каждого типа. Но можно однозначно сказать, что вы можете рассматривать GenericCounter<string> и GenericCounter<int> как независимые типы.

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
👍1
День сто девяносто третий. #ЗаметкиНаПолях
Заметки по обобщённым типам
2. Вывод типа из аргументов методов
Рассмотрим следующий код:
public static List<T> CopyAtMost<T>(List<T> input, int elements)
{
return input.Take(elements).ToList();
}

var numbers = new List<int>() { 1, 2, 3, 4 };
var firstTwo = CopyAtMost<int>(numbers, 2);

Вам нужен аргумент типа для вызова CopyAtMost, потому что метод имеет параметр типа. Но вам не нужно указывать этот тип аргумента явно. Можно переписать этот код следующим образом:
var firstTwo = CopyAtMost(numbers, 2);
Это точно такой же вызов метода с точки зрения IL, который сгенерирует компилятор. Но аргумент типа int указывать не нужно, компилятор сделал это для вас на основе аргумента для первого параметра метода. Вы используете аргумент List<int> в качестве значения параметра типа List<T>, поэтому T должно быть int. Вывод типа может использовать только аргументы, передаваемые методу, но не тип результата.
Хотя вывод типов применяется только к методам, его можно использовать для более простого создания экземпляров обобщённых типов. Например, рассмотрим семейство типов Tuple, состоящее из необобщённого статического класса Tuple и нескольких обобщённых классов: Tuple<T1>, Tuple<T1, T2>, Tuple<T1, T2, T3> и так далее (до 8). Статический класс имеет набор перегруженных фабричных методов Create:
public static Tuple<T1> Create<T1>(T1 item1)
{
return new Tuple<T1>(item1);
}
public static Tuple<T1, T2> Create<T1, T2>(T1 item1, T2 item2)
{
return new Tuple<T1, T2>(item1, item2);
}
и так далее…
Они выглядят бессмысленно тривиальными, но позволяют использовать вывод типа. То есть, вместо этого:
new Tuple<int, string, int>(10, "x", 20)
Можно написать:
Tuple.Create(10, "x", 20)
Это мощная техника, о которой полезно знать; как правило, её просто реализовать и она может сделать работу с обобщённым кодом намного приятнее.
Разработчики языка постоянно работают над усовершенствованием процесса вывода типа, и порой логика вывода становится достаточно сложной из-за наследований, перегрузок и необязательных параметров. Поэтому надо понимать, что иногда на практике выведение типа может приводить к неожиданным результатам. Например, если вам нужен Tuple<int, object, int>, то из предыдущего вызова Tuple.Create(10, "x", 20) вы его не получите. В этом случае можно использовать либо new Tuple<int, object, int>(10, "x", 20), либо приведение типа Tuple.Create(10, (object)"x", 20). Аналогично с null: Tuple.Create(null, 50) завершится неудачей, но Tuple.Create((string) null, 50) сработает.

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
День сто девяносто четвёртый. #юмор
Отладка.
Да, бывало и так)))
День сто девяносто пятый. #ИнтересныйКод
Ленивый итератор с помощью yield return
Следующий код показывает метод Fibonacci(), возвращающий бесконечную последовательность чисел Фибоначчи, и использование этого метода для вывода чисел до нужного предела:
static IEnumerable<int> Fibonacci()
{
int current = 0;
int next = 1;
// бесконечный цикл? Только если продолжать запрашивать значения
while (true)
{
yield return current;
int oldCurrent = current;
current = next;
next += oldCurrent;
}
}
static void Main()
{
foreach (var value in Fibonacci())
{
Console.WriteLine(value);
// условие остановки цикла
if (value > 1000) { break; }
}
}
Как реализовать что-то подобное без итераторов? Можно изменить метод для создания List<int> и заполнять его, пока не достигнут предел. Но этот список может быть большим, если предел велик, и почему метод, который знает детали последовательности Фибоначчи, также должен знать, когда остановиться? Предположим, иногда нужно остановиться по значению текущего числа, а иногда по количеству выведенных чисел или через некоторое время.
Можно избежать создания списка, печатая значение в цикле, но это делает метод Fibonacci() еще более тесно связанным с тем, что вы хотите сделать со значениями прямо сейчас. Что если вы хотите складывать значения, а не печатать их? Писать другой метод? Это ужасное нарушение разделения ответственности.
Решение через итератор - это именно то, что вам нужно: представление бесконечной последовательности и всё. Вызывающий код может выполнять итерацию по своему усмотрению (по крайней мере, до переполнения int) и использовать значения, как хочет.
Реализация последовательности Фибоначчи вручную проста. Не нужно сохранять много данных состояния (только два предыдущих числа), а управлять логикой выполнения довольно просто (здесь только один оператор yield return). Но если логика усложняется, реализация её в коде становится крайне непростой. Таким образом, использование итераторов с yield return – это мощный инструмент, сильно упрощающий разработку.
Кроме того, компилятор также достаточно умён, чтобы правильно обрабатывать блоки finally, что не так очевидно, как кажется (об этом далее).

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
День сто девяносто шестой. #ЗаметкиНаПолях
Обработка блоков finally в итераторах
Рассмотрим следующий код:
static IEnumerable<string> Iterator()
{
try
{
Console.WriteLine("Перед первым yield");
yield return "первый";
Console.WriteLine("После первого yield");
yield return "второй";
Console.WriteLine("После второго yield");
}
finally
{
Console.WriteLine("Внутри finally");
}
}
Когда будет выполнен блок finally:
- Если считать выполнение прерываемым на каждом вызове yield return, тогда логически они внутри блока try, и нет надобности выполнять блок finally каждый раз.
- Если считать, что на самом деле на каждом yield return вызывается метод MoveNext() итератора, то можно решить, что происходит выход из try, и тогда finally должен выполняться.
Выполним этот код:
foreach (string value in Iterator())
{
Console.WriteLine("Значение: {0}", value);
}
Вывод:
Перед первым yield
Значение: первый
После первого yield
Значение: второй
После второго yield
Внутри finally

Таким образом, блок finally выполнится только после окончания итерации. Это подходит под концепцию ленивого выполнения. Пока ничего сложного. Но что если мы прервём итерацию после первого значения? Выполнится ли блок finally?
На самом деле, и да, и нет. Если реализовать итератор вручную и вызвать MoveNext() один раз, то блок finally действительно никогда не будет выполнен. Однако, если использовать итератор внутри цикла foreach, как это чаще всего происходит, то компилятор использует скрытый блок using вокруг цикла. При выходе из блока using происходит вызов метода Dispose() итератора, и, соответственно, вызов всех блоков finally. Таким образом, результатом выполнения следующего кода:
foreach (string value in Iterator())
{
Console.WriteLine("Значение: {0}", value);
break;
}
будет:
Перед первым yield
Значение: первый
Внутри finally

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

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
День сто девяносто седьмой. #оффтоп
Иногда мне кажется, что самых рукожопых разработчиков мелкомягкие не увольняют, а отправляют в ссылку на проект скайпа.
День сто девяносто восьмой. #ЗаметкиНаПолях
Частичные методы
В C# 3 добавлена дополнительный функционал для частичных классов - частичные методы. Это методы, объявленные без тела в одной части, а затем опционально реализованные в другой части. Частичные методы неявно являются private, должны возвращать void и не иметь out параметров (можно использовать параметры ref). Во время компиляции сохраняются только частичные методы, имеющие реализации; если частичный метод не был реализован, он и все его вызовы удаляются. Это звучит странно, но позволяет автоматически сгенерированному коду предоставлять точки перехвата (hooks) для добавления дополнительной логики в коде, написанном вручную. Это может быть реально полезно. В примере ниже объявлены два частичных метода:
partial class PartialMethodsDemo
{
public PartialMethodsDemo()
{
OnConstruction();
}
public override string ToString()
{
string ret = "Original return value";
CustomizeToString(ref ret);
return ret;
}
partial void OnConstruction();
partial void CustomizeToString(ref string text);
}
Во втором файле частичного класса один из методов реализован, а другой – нет:
partial class PartialMethodsDemo
{
partial void CustomizeToString(ref string text)
{
text += " - customized!";
}
}
В листинге первая часть кода, скорее всего, будет сгенерирована автоматически, объявляет два метода, позволяющие обеспечить дополнительное поведение в конструкторе и при получении строкового представления объекта. Вторая часть соответствует написанному вручную коду, который не требует дополнительной логики в конструкторе, но хочет изменить строковое представление, возвращаемое ToString(). Несмотря на то, что метод CustomizeToString не может возвращать значение напрямую, он может передавать информацию вызывающему его методу через ref параметр. Поскольку OnConstruction не реализован, он и вызовы его удаляются компилятором.

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
This media is not supported in your browser
VIEW IN TELEGRAM
День сто девяносто девятый. #ЧтоНовенького
Поиск по коду в панели быстрого поиска
В Visual Studio 2019 версии 16.3, которая доступна для предварительного скачивания и должна выйти в конце сентября, добавлена возможность поиска по коду через панель быстрого поиска (Ctrl + Q). Теперь можно искать классы и члены классов в коде C# и VB. Результаты будут отображаться при вводе запроса, а также на отдельной вкладке Code.
Также возможен camel-case поиск. Это позволяет вводить только заглавные буквы имени класса или члена вместо полного имени.
Источник: https://devblogs.microsoft.com/visualstudio/code-recent-items-and-template-search-in-visual-studio/
День двухсотый. #ЗаметкиНаПолях
Псевдонимы пространств имён
Псевдонимы пространств имён используются, когда вам нужно обратиться к типам с одинаковым именем из разных пространств имён. Следующий пример показывает, как один метод может обратиться к классам Button из Windows Forms и ASP.NET Web Forms.
using System;
using WinForms = System.Windows.Forms;
using WebForms = System.Web.UI.WebControls;
class Test
{
static void Main()
{
Console.WriteLine(typeof(WinForms.Button));
Console.WriteLine(typeof(WebForms.Button));
}
}
Это хорошо работает до тех пор, пока не появляется пространство имён с именем WinForms. Чтобы различать названия пространств имён и их псевдонимы в C#2 введены квалификаторы псевдонимов пространств имён, представляющие из себя просто пару двоеточий. Таким образом, код выше можно переписать так:
static void Main()
{
Console.WriteLine(typeof(WinForms::Button));
Console.WriteLine(typeof(WebForms::Button));
}

Псевдоним глобального пространства имён
Несмотря на то, что типы крайне редко объявляются в глобальном пространстве имён, его можно использовать как «корневое» пространство имён. В C#2 введён псевдоним global, обозначающий глобальное пространство имён. Например, при использовании типа DateTime, если он объявлен во нескольких пространствах имён, то обратиться к системному типу можно, использовав следующую конструкцию global::System.DateTime. Использовать псевдоним глобального пространства имён полезно, например, при написании автоматических генераторов кода, когда есть большая вероятность коллизий сгенерированных названий пространств имён и типов.

Внешние псевдонимы
В крайнем случае, когда два типа с одинаковыми именами находятся в пространствах имен с одинаковыми именами, но предоставлены в разных сборках, можно воспользоваться внешними псевдонимами, используя ключевые слова extern alias:
extern alias GridV1;
extern alias GridV2;

using GridV1::Grid;
using GridV2::Grid;
Связать файл сборки с именем внешнего псевдонима можно либо в файле конфигурации, либо указав параметр /r компилятору в командной строке:
/r:GridV1=grid.dll
/r:GridV2=grid20.dll

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
День двести первый. #BestPractices
Советы по разработке типов
1. Выбор между классом и структурой
Хорошее понимание различий в поведении ссылочных и значимых типов имеет решающее значение при принятии решения в этом вопросе:
- Инициализация и уничтожение значимых типов обычно дешевле.
- Инициализация и уничтожение массивов значимых типов намного дешевле.
- При приведении значимого типа к ссылочному или к интерфейсному типу и обратно выполняется упаковка и распаковка. Это отрицательно влияет на кучу, скорость сборки мусора и общую производительность.
- Ссылочные типы копируются по ссылке, а значимые по значению, т.е. присвоение большого объекта ссылочного типа дешевле, чем значимого.
- Ссылочные типы передаются по ссылке, то есть изменение экземпляра ссылочного типа изменяет все переменные, которые ссылаются на него. Когда же изменяется переменная значимого типа, это не влияет на копии элемента. Поэтому во избежание путаницы структуры делают неизменяемыми.

Большинство типов должны быть классами (ссылочными типами). Однако есть ситуации, когда характеристики значимого типа подойдут для использования структуры (struct).

⚠️ РАССМОТРИТЕ определение объекта как структуры, а не класса, если экземпляры типа небольшие и живут недолго, либо включены в другие объекты.

ИСПОЛЬЗУЙТЕ структуры, когда тип обладает всеми следующими характеристиками:
- Логически представляет из себя единое значение.
- Размер экземпляра типа меньше 16 байт.
- Он неизменяем.
- Его не придётся часто упаковывать.

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

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести второй. #BestPractices
Советы по разработке типов
2. Разработка абстрактных классов
ИЗБЕГАЙТЕ определения открытых (public) или защищённых внутренних (protected internal) конструкторов в абстрактных типах. Конструкторы должны быть открытыми, только если пользователям нужно будет создавать экземпляры типа. Поскольку вы не можете создавать экземпляры абстрактного типа, абстрактный тип с открытым конструктором неправильно спроектирован и вводит пользователей класса в заблуждение.

ИСПОЛЬЗУЙТЕ защищённые (protected) или внутренние (internal) конструкторы в абстрактных классах. Защищенный конструктор является более распространенным и позволяет базовому классу выполнять собственную инициализацию при создании подтипов. Внутренний конструктор может использоваться для ограничения конкретных реализаций абстрактного класса только сборкой, определяющей класс.

ИСПОЛЬЗУЙТЕ хотя бы один конкретный тип, наследующий от каждого абстрактного класса. Это помогает проверить дизайн абстрактного класса. Например, System.IO.FileStream является реализацией абстрактного класса System.IO.Stream.

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

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести третий. #ЗаметкиНаПолях
Захват переменных лямбда-выражениями
Внутри лямбда-выражения вы можете использовать любую переменную, которую вы могли бы использовать в обычном коде в данном месте. Это может быть статическое поле, поле экземпляра (если вы пишете лямбда-выражение внутри экземплярного метода), переменная this, параметры метода или локальные переменные. Все они называются захваченными переменными, потому что они объявленны вне непосредственного контекста лямбда-выражения. Для сравнения параметры лямбда-выражения или локальные переменные, объявленные внутри лямбда-выражения не являются захваченными переменными. В следующем примере показан захват различных переменных лямбда-выражением:

class CapturedVariablesDemo
{
private string instanceField = "instance field";
public Action<string> CreateAction(string methodParameter)
{
string methodLocal = "method local";
string uncaptured = "uncaptured local";
Action<string> action = lambdaParameter =>
{
string lambdaLocal = "lambda local";
Console.WriteLine("Instance field: {0}", instanceField);
Console.WriteLine("Method parameter: {0}", methodParameter);
Console.WriteLine("Method local: {0}", methodLocal);
Console.WriteLine("Lambda parameter: {0}", lambdaParameter);
Console.WriteLine("Lambda local: {0}", lambdaLocal);
};
methodLocal = "modified method local";
return action;
}
}

var demo = new CapturedVariablesDemo();
Action<string> action = demo.CreateAction("method argument");
action("lambda argument");

Вывод:
Instance field: instance field
Method parameter: method argument
Method local: modified method local
Lambda parameter: lambda argument
Lambda local: lambda local

В этом примере задействованы следующие переменные:
- instanceField - поле экземпляра класса CapturedVariablesDemo,
захватывается лямбда-выражением.
- methodParameter - параметр метода CreateAction, захватывается лямбда-выражением.
- methodLocal - локальная переменная метода CreateAction, захватывается и изменяется лямбда-выражением.
- uncaptured - локальная переменная метода CreateAction, не используется лямбда-выражением, поэтому не захватывается им.
- lambdaParameter - параметр лямбда-выражения, не является захваченной переменной.
- lambdaLocal - локальная переменная в лямбда-выражении, не является захваченной переменной.

Важно понимать, что лямбда-выражение захватывает сами переменные, а не значения переменных в момент создания делегата. Если вы изменили какую-либо из захваченных переменных между временем создания делегата и его вызовом, лямбда-выражение «увидит» эти изменения. Аналогично, лямбда-выражение может изменить значение захваченных переменных. Более того, в этом случае каждый последующий вызов лямбда-выражения будет «видеть» изменения захваченных переменных, сделанные предыдущими вызовами.

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 3.
День двести четвёртый. #BestPractices
Советы по разработке типов
3. Разработка Статических Классов
Статический класс содержит только статические члены (кроме экземплярных членов, унаследованных от System.Object и, возможно, приватного конструктора). Некоторые языки предоставляют встроенную поддержку статических классов. В C# статический класс неявно закрытый (sealed), абстрактный (abstract), и никакие экземплярные члены не могут быть переопределены или объявлены.

Статические классы - это компромисс между чисто объектно-ориентированным дизайном и простотой. Они обычно используются для предоставления ярлыков для других операций (например, System.IO.File), хранителем методов расширения или функциональных возможностей, для которых реализация полностью объектно-ориентированной оболочки невозможно (например, System.Environment).

ИСПОЛЬЗУЙТЕ статические классы в исключительных случаях. Статические классы должны использоваться только как вспомогательные классы для объектно-ориентированного ядра платформы.

ИЗБЕГАЙТЕ использования статических классов для хранения разнородного функционала.

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

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
This media is not supported in your browser
VIEW IN TELEGRAM
День двести пятый. #юмор
Модульное (блочное или юнит-) тестирование — процесс в программировании, позволяющий проверить на корректность отдельные модули программы. При интеграционном тестировании отдельные программные модули объединяются и тестируются в группе. В примере выше модульное тестирование завершилось успешно, а при интеграционном возникли проблемы.