.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
День одиннадцатый. #ЗаметкиНаПолях
События.
С самого начала работы с формами ещё на Delphi, мне всегда было интересно, что это за магические параметры у вызова события нажатия на кнопку:
OnClick(Object sender, EventArgs e);
На самом деле это реализация паттерна проектирования «Наблюдатель». Если коротко, то у нас есть объект наблюдения (допустим система сообщений), и есть «наблюдатели», например, различные сервисы отправки сообщений: факс, смс, e-mail и т.п.
Паттерн состоит в следующем:
- наблюдатели подписываются на рассылку, передавая объекту делегат метода обратного вызова
- объект при возникновении нового события вызывает этот метод у всех подписанных наблюдателей, передавая в него «сообщение» - класс, наследуемый от EventArgs (e) и ссылку на себя (sender).

Рассмотрим, реализацию этого паттерна на С#:
1. Определяем тип сообщения, например, письмо:
class NewMailEventArgs : EventArgs {
public string From {get; set;}
public string To {get; set;}
public string Message {get; set;}
}
2. В объекте наблюдения определяем член-событие:
class MailManager {
public event EventHandler<NewMailEventArgs> NewMail;

}
Здесь обобщённый делегат System.EventHandler принимает тип нашего сообщения. Значит наблюдатели должны предоставить метод обратного вызова, прототип которого совпадает с делегатом. Например:
void Method(Object sender, NewMailEventArgs e);
Объект наблюдения ответственен за создание объекта-сообщения и вызов метода обратного вызова с передачей ему ссылки на себя (Object sender) и сообщения (NewMailEventArgs e).
3. В объекте наблюдения определяем метод, уведомляющий о событии:
class MailManager {

//уведомляем о событии
private void OnNewMail(NewMailEventArgs e) {
EventHandler<NewMailEventArgs> temp = NewMail;
if(temp != null) temp(this, e);
}

}
4. В объекте наблюдения определяем метод, вызывающий событие:
class MailManager {

public void SimulateNewMail(…) {

//создаём сообщение
var msg = new NewMailEventArgs{From=“…”, To=“…”, Message=“…”};
//вызываем метод, уведомляющий о событии
OnNewMail(msg);
}
}
5. Определяем наблюдателя:
class Fax {
//храним ссылку на объект наблюдения
private MailManager mailMgr;

//передаём в конструктор ссылку на объект наблюдения
public Fax(MailManager mm) {
mailMgr = mm;
}

// метод обратного вызова
private void FaxMsg(Object sender, NewMailEventArgs e) {

// обрабатываем сообщение e (отправляем факс)

}

// подписываемся на уведомления (можно сразу в конструкторе)
public void Register() {
mailMgr.NewMail += FaxMsg;
}

// отписываемся от уведомлений
public void Unregister() {
mailMgr.NewMail -= FaxMsg;
}
}

C# допускает упрощённый синтаксис подписки и отписки
mailMgr.NewMail += FaxMsg;
mailMgr.NewMail -= FaxMsg;
На самом деле в объекте наблюдения хранится список наблюдателей. Оператор += вызывает метод, добавляющий наблюдателя в список, а -=, соответственно, метод, удаляющий из списка.

6. В основной программе:
//создаём объект наблюдения
MailManager mailMgr = new MailManager();

//создаём наблюдателя и подписываем его
Fax fax = new Fax(mailMgr);
fax.Register();

//возникновение события
mailMgr.SimulateNewMail(…);

Источник: Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 11.
👍4
День двенадцатый. #CodeComplete
1. Управление сложностью. Окончание
5. Инкапсуляция
Инкапсуляция – это не то же самое, что абстракция.
- Инкапсуляция не позволяет вам смотреть на упрощённую информационную структуру в усложнённом виде.
- Скрывайте детали за хорошо спроектированным интерфейсом, чтобы избежать программирования «сквозь» интерфейс (использовать класс, зная о деталях его реализации).
- Сосредоточьте своё внимание на интерфейсах. Реализация интерфейса воздействует только на качество кода. Качество интерфейса воздействует на срок жизни, расширяемость, масштабируемость и т.п. программного обеспечения в целом.
6. Связность
- Каждый метод/класс должен решать РОВНО ОДНУ задачу!
- Сложность создания ясного имени для метода или класса часто тревожный знак плохой его связности. Это не проблема именования, это проблема дизайна.
7. Разделение ответственности
- Основная идея – «Разделяй и властвуй»
- Пытайтесь разделять программу на сферы ответственности
- Разделение ответственности применяется к более высоким уровням проектирования, например, разделению на подсистемы (см. картинку ниже).
- Часто разделение невыполнимо из-за синтаксиса языка.
День тринадцатый. #Оффтоп
Научиться чему-либо невозможно без практики. Наткнулся вот на интересный сервис https://exercism.io (только на английском языке). Там представлены упражнения для закрепления материала на множестве языков, в том числе C#. Начиная с «Hello, world!» и до достаточно сложных. В каждом языке можно выбрать курс с «ментором», который будет просматривать и оценивать (одобрять или не одобрять) ваши решения и давать комментарии (кстати, удивительно, что менторы дают действительно нужные комментарии, всегда корректно и по делу). Пока ментор не одобрит ваше решение, переходить к следующему упражнению основного курса нельзя. Но в то же время можно порешать множество сторонних задач, которые не будут оцениваться ментором, достаточно решить задачу и выложить код на всеобщее обозрение.
Ну а если уж дожидаться оценок не охота, можно проходить курс самостоятельно, хотя это не рекомендуется.
Интересный момент, что все упражнения построены по принципу разработки через тестирование (TDD). Вы скачиваете проект с юнит-тестами. Все они, естественно не проходят. Ваша задача решить проблему так, чтобы все тесты проходили. То есть правильность решения можно проверять и самостоятельно.
Интересно также и смотреть и решения других людей. Их можно оценивать и комментировать. Даже для вполне тривиальных задач решения попадаются просто гениальные. Лично я для себя отмечаю абсолютно взрывающие мозг решения через LINQ. Народ ужимает 30+ строк обычного кода до одной строки даже там, где это, казалось бы, невозможно. Понимаю для себя, что этот момент надо подтягивать.
День четырнадцатый. #ЗаметкиНаПолях
Конструкторы типов
CLR помимо конструкторов экземпляров поддерживает конструкторы типов (также известные как статические конструкторы, конструкторы классов или инициализаторы типов). Они служат для установки первоначального состояния типа. У типа может быть только один конструктор без параметров. Формат конструктора типа:
class SomeClass {
static SomeClass() {
//исполняется при первом обращении к типу
}
}
Поток, начавший выполнение конструктора типа, получает исключающую блокировку, что гарантирует, что конструктор типа вызовется только один раз.
Для инициализации статических полей лучше использовать встроенный синтаксис инициализации, потому что CLR может оптимизировать типы, не имеющие явно определённых статических конструкторов. То есть лучше использовать:
class SomeClass {
private static int x = 5;
}
вместо
class SomeClass {
private static int x;
static SomeClass() {
x = 5;
}
}
Конструкторы типов применяются:
- для инициализации объектов-одиночек (паттерн Singleton)
- когда класс использует запись в лог
- при создании оболочек для неуправляемого кода, когда конструктор типа используется для вызова метода LoadLibrary.

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

Поэтому не рекомендуется использовать следующий пример из книги Рихтера:
Иногда конструктор типа используется для обобщённого типа, чтобы аргументы типа соответствовали определённым критериям. Например, обобщённый тип, используемый только с перечислимыми типами:
public class GenericTypeRequiresEnum<T> {
static GenericTypeRequiresEnum() {
if (!typeof(T).IsEnum)
throw new ArgumentException("T должно быть перечислимым типом");
}
}
Вместо этого лучше использовать конструкцию where:
public class GenericTypeRequiresEnum<T> where T : Enum
{

}
Она позволяет проверять принадлежность T к определённому типу на этапе компиляции (см. скриншот ниже). Однако, поддержка перечислимых типов (Enum) в конструкции where появилась только в C# версии 7.3.

Источники:
- Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Главы 8, 12.
-
https://docs.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/static-constructors
.NET Разработчик pinned «Hello, world! В этих ваших интернетах всякие тренеры личностного роста и прочие спецы по мотивации говорят, что бороться с прокрастинацией и не бросать начатое проще, если хотя бы записываешь прогресс в дневник. А ещё лучше, если делишься им с друзьями. Я…»
День пятнадцатый. #CodeComplete
2. Именование

1. Имена переменных
Обычно имена переменных, которые недостаточно конкретны, чтобы быть использованы только для одной цели в процедуре, это плохие имена.
- Имя переменной должно быть существительным, описывающим реальную сущность
- Не используйте префиксы, обозначающие тип переменной или отделяющие поля класса от локальных переменных (“__...”, “m_...”, “s_...”, “i_...” и т.п.)
- Делайте имена переменным настолько конкретными, насколько это возможно
- Чем очевиднее, тем лучше
- Чем ближе имя к сущности реального мира, тем обычно оно более полезно
2. Именование методов
- Описывайте в имени всё, что делает метод (либо разбейте его на несколько)
- Делайте имена настолько длинными, насколько можно
- Бессмысленные или неопределённые глаголы (Calc, HandleClass, ProcessInput и т.п.) – тревожный знак
- Приближайте имена к необходимому уровню абстракции, чтобы скрыть детали реализации метода
- Делайте имена методов настолько высокоуровневыми (близкими бизнес-логике или к реальному миру), насколько это возможно
3. Соглашения об именовании
- утвердите соглашение о согласованном наименовании стандартных операций, например, user.GetID() или user.ID.Get()
- то же касается стандартных аббревиатур и языковых вариаций (color/colour)
- избегайте ненужных вариаций: сокращения, непонятные аббревиатуры, смесь языков и т.п. (totalCount/ttlCnt/itog, firstName/fName/firstNm/FN/imya).
День шестнадцатый. #ЗаметкиНаПолях
Использование using
Все мы привыкли к использованию ключевого слова using в начале наших файлов для упрощения обращения к типам в определённом пространстве имён. Но у него есть и другие интересные варианты использования.

Директива
1. Наиболее распространённый вариант использования уже упоминался выше. Нет необходимости каждый раз писать полное имя типа с пространством имён, например:
using System.Text;

var sb = new StringBuilder();

2. Для упрощённого обращения к статическим методам класса можно использовать using static, например:
using static System.Console;
using static System.Math;

WriteLine(Sqrt(3*3 + 4*4));

3. Использование псевдонимов (алиасов) для пространств имён или типов. Например, для упрощения использования обобщённых типов (в самой директиве, правда, все имена придётся написать полностью):
using System;
using DateTimeList = System.Collections.Generic.List<System.DateTime>;

var dtl = new DateTimeList {DateTime.MinValue, DateTime.Now, DateTime.MaxValue};

Оператор
Оператор using упрощает работу с объектами которые реализуют интерфейс IDisposable. Интерфейс имеет один метод Dispose(), который используется для освобождения ресурсов, использованных объектом. При использовании using не обязательно явно вызывать Dispose() для объекта.
using (SqlConnection conn = new SqlConnection()) { 

}
При этом компилятор сгенерирует приблизительно следующий код:
SqlConnection conn = new SqlConnection();
try {

}
finally {
if (conn != null)
((IDisposable)conn).Dispose();
}
👍2
День семнадцатый. #Оффтоп
Перевёл статью для канала CodeBlog “Топ-7 вещей необходимых разработчику ПО”. Немного добавлю от себя. Мне изначально понравился подход автора, который не стал углубляться в банальности, типа, «вам надо изучить алгоритмы» или «вы обязательно должны знать, как работать с указателями», или, что особенно доставляло в вузе, «изучение программирования хорошо бы начинать с ассемблера, а потом переходить к высокоуровневым языкам» (WTF???)
В статье же упор делается на условия работы. Как наиболее эффективно использовать доступные вам инструменты, чтобы не только быстро и качественно делать свою работу, но и получать от неё удовольствие. Про себя могу отметить, что попадаю почти под все пункты :) Хотя, пришёл к этому далеко не сразу. Удобство хорошего кресла и просторного рабочего стола я оценил всего лишь года три назад, когда захотели дома сделать спальню из моего кабинета, а меня выселили в другую комнату. Я заодно прикупил себе и кресло с большим компьютерным уголком. А начинал я работу, сидя буквально на обычном деревянном стуле за старым советским столом-книжкой (олды должны помнить такие), который к тому же ещё и ходил ходуном, если на него облокотиться. Идею двух мониторов тоже подсмотрел у коллег, которые ими активно пользовались. Хотя поначалу у меня одним монитором был экран ноута, а вторым старый 15-дюймовый монитор 4:3 с максимальным разрешением 1024х768.
Должен отметить, что мне откровенно повезло с работодателем. И железо, и софт постоянно обновляются по инициативе работодателя. На данный момент мой рабочий ноутбук – это 4х-ядерный Intel Core i7, 16Gb оперативной памяти и 2 SSD диска на 1Tb и 2Tb и 2 монитора по 24”. Вы не представляете, как быстро привыкаешь к хорошему! Когда я прихожу к знакомым помочь с компьютером, я часто ловлю себя на том, что запускаю 3 или 4 окна браузера сразу. Просто потому, что тыкаю на ярлык, и ничего не происходит. На моём ноуте это значит, что я просто промахнулся или не нажал кнопку мыши, поэтому я на автомате тыкаю ещё раз, и ещё раз. А оказывается, просто компу нужно время, чтобы запустить чёртов браузер!
Почитал комментарии в контакте под статьёй. Конечно, там попеняли на то, что хрен ты чего от наших работодателей добьёшься. Да, согласен. Но здесь остаётся только посочувствовать и пожелать налегать на последний пункт статьи - книги и сертификаты - и доказывать, что вы этого достойны. Хороших всем работодателей, интересных задач, а главное, получайте удовольствие от работы, ведь для этого вы и выбрали карьеру программиста, не так ли?
https://shwanoff.ru/top-7-things-a-software-developer-needs/
👍5
День восемнадцатый. #CodeComplete
3. Процесс Программирования с Псевдокодом
При проектировании сложных или объёмных методов сложно написать метод от начала до конца из головы. В этих случаях помогает процесс программирования с псевдокодом. Он состоит из нескольких этапов:
1. Напишите, что должен делать каждый блок кода на естественном языке, используя комментарии (это будет псевдокод).
Комментарии должны быть на достаточно высоком уровне, чтобы не дублировать то, что будет написано в коде, а объяснять, что делает блок кода. Например, вместо:
// для каждого i меньше длины массива objectArr

используйте
// перебираем все объекты массива в цикле
2. Проверьте весь псевдокод метода. Добавьте более детальные комментарии там, где это необходимо.
3. Напишите код каждого блока под каждым блоком комментариев.
Применяйте ППП рекурсивно, если нужно. Например, если блок написан слишком общими словами, которые сложно сразу перевести в код, примените ППП к этому блоку (рассмотрите возможность вынести этот блок в отдельный метод).
4. Оставьте псевдокод в виде комментариев.

Преимущества ППП:
- Упрощает изначальное написание кода
- Упрощает обзор кода
- Приводит к лучше организованному коду
- Автоматизирует процесс добавления комментариев
- Упрощает возвращение к работе, если вас прервали на середине
- Сокращает количество ошибок, поскольку процесс позволяет (вынуждает) вас больше думать о правильном проектировании метода
👍2
День девятнадцатый. #ЗаметкиНаПолях
Обобщения
Обобщения – это механизм многократного использования алгоритма. Разработчик описывает алгоритм, но не указывает типы данных, с которыми тот работает. Применяя алгоритм, другой разработчик должен указать конкретные типы данных. Например, обобщённый список определяется как List<T>. Здесь переменная T, указывающая на тип называется параметром типа. В объявлении обобщённого типа её можно использовать в любом месте, где указывается, тип данных. Например:
- в параметре метода
public void Add(T item);
- в возвращаемом значении
public T[] ToArray();
public T this[int index] { get; set; }
- при определении локальных переменных и полей внутри класса
Microsoft рекомендует называть параметры типа T или словом, начинающимся с T (TKey, TResult). При этом рекомендуется использовать значимые имена, где это возможно или если параметр типа ограничен конкретным классом или интерфейсом (например, параметр типа, ограниченный интерфейсом ISession, может быть назван TSession).

Преимущества обобщений:
- защита исходного кода: для использования обобщённого алгоритма не нужен доступ к исходному тексту алгоритма;
- безопасность типов: компилятор и CLR обеспечивают, чтобы в алгоритме использовались лишь объекты, совместимые с указанным типом данных;
- более простой и понятный код: из-за того, что компилятор обеспечивает безопасность типов уже на этапе компиляции, требуется меньше операций приведения типов, а такой код проще писать и поддерживать;
- повышение производительности: не требуется лишних операций приведения типов, для структур не требуется упаковки/разупаковки, что ускоряет работу кода.

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

При создании ваших собственных обобщённых типов, важно учитывать следующие моменты:
1. Какие типы обобщать в параметры типа. Обычно, чем больше типов вы параметризуете, тем более гибким становится ваш код. Но слишком обобщённый тип может привести к коду, который будет трудно читать и понять другим разработчикам.
2. Какие ограничения, если они нужны, применить к параметрам типа (об ограничениях в следующих постах). Хорошим тоном будет применить максимально возможные ограничения, которые всё равно позволят вам использовать требуемые типы. Например, если обобщённый тип подразумевает использование только ссылочных типов, добавьте ограничение class. Это не допустит использования вашего типа со структурами.
3. Обдумайте обобщённое поведение в базовом классе и подклассах.
4. Нужно ли реализовать один или больше обобщённых интерфейсов.
Например, если вы разрабатываете класс, который будет использоваться в качестве элемента обобщённой коллекции, вам может потребоваться реализовать интерфейс вроде IComparable<T>, где T – это тип вашего класса.

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

Источники:
- Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 12.
-
https://docs.microsoft.com/ru-ru/dotnet/csharp/programming-guide/generics/index
👍3
День двадцатый. #Оффтоп
Stackoverflow
Наверное, самый известный из используемых мной ресурсов. Захожу туда почти ежедневно и не по разу, чуть реже, чем в гугл. То есть, строго говоря, 9 из 10 вопросов к гуглу по программированию ведут на stackoverflow.com (английский естественно). Уже лет 15 как решил для себя, что поиск ответа с гораздо большей вероятностью приведёт к желаемому результату, если вопрос задан пусть даже на ломаном английском, нежели на русском. И тут нет ничего странного. Англоговорящее (и даже ломано-англоговорящее) сообщество гораздо обширнее русскоговорящего (хотя русскоязычная версия сайта тоже есть - ru.stackoverflow.com). Но все попытки поискать информацию по-русски меня почему-то зачастую вели на малознакомые и странноватые форумы, да и там часто только задавался мой вопрос, а ответа на него не было.
Стараюсь вспомнить случаи, когда решения какого-нибудь вопроса или разновидности решения не было на stackoverflow, и вспоминаю за все эти годы от силы случаев 10. При этом в основном это были узкоспециализированные вопросы по нюансам работы какой-нибудь специфической библиотеки или плагина, вроде jQuery Datatables.
Одно время я даже пытался заработать там немного рейтинга, отвечая на вопросы, но это невероятно сложно, потому что там настоящая гонка. Вопрос не успевает появиться, и уже через минуту-другую под ним 2-3 ответа или комментария. Так что в принципе, даже задав там вопрос, ответа долго ждать не пришлось бы. Но я так и не добрался пока до того, чтобы что-то спросить. Причина – см. предыдущий абзац :)
День двадцать первый. #ЗаметкиНаПолях
Ограничения обобщений
Ограничения сообщают компилятору, какими характеристиками должен обладать параметр типа. Без ограничений параметр типа может быть любым. Если клиентский код попытается использовать ваш класс с типом, который не совместим с ограничением, возникнет ошибка компиляции. Синтаксис:
public class SomeList<T> where T : BaseClass
{

}

Типы ограничений:
T : struct – значимый тип (структура);
T : class – ссылочный тип: класс, интерфейс, делегат (C#7.3+) или массив;
T : new() – аргумент типа должен иметь конструктор без параметров;
T : <имя класса> - аргумент типа должен быть этого класса или наследником класса;
T : <имя интерфейса> - аргумент типа должен быть этим интерфейсом или классом, реализующим этот интерфейс (можно добавлять несколько ограничений интерфейса, тогда аргумент типа должен реализовывать все интерфейсы);
T : TBase – параметры типа T и TBase такие, что T должен быть типом TBase или наследовать от него;
T : unmanaged (C#7.3+) – не ссылочный тип и не имеет членов ссылочного типа на всех уровнях наследования (значимые примитивные типы, указатели, перечисления или структуры, определённые пользователем).

Неограниченные параметры типа
Параметры типа, не имеющие ограничений, называются неограниченными. К ним применяются следующие правила:
- операторы != и == не могут быть использованы, поскольку нет гарантии, что конкретные аргументы типа их поддерживают;
- они могут быть приведены к типу System.Object или явно к любому интерфейсному типу;
- вы можете сравнивать их с null (значимые типы при этом всегда возвратят false).

Зачем использовать ограничения
Ограничивая параметр типа, вы увеличиваете количество доступных операций и вызовов методов теми, которые поддерживаются типом-ограничением и всеми типами в его иерархии наследования. Неограниченные параметры типа поддерживают только простейшие операции и методы, определённые в System.Object.

При применении ограничения where T : class избегайте использования операторов == и != к параметру типа, поскольку эти операторы проверяют только идентичность ссылки, а не значения. Для проверки по значению используйте ограничения where T : IEquatable<T> или where T : IComparable<T> и реализуйте эти интерфейсы в классах, которые будут использоваться в обобщённом классе.

Ограничения нескольких параметров
Вы можете применить ограничения к нескольким параметрам, и несколько ограничений к одному параметру. Cами ограничения могут быть обобщёнными типами:
class Base { }
class Test<T, U>
where U : struct
where T : Base, System.IComparable<T>, new()
{ }

Параметры типа как ограничения
Позволяет указать, что между указанными аргументами типа должны быть определённые отношения:
private static List<TBase> ConvertList<T, TBase>(IList<T> list) where T : TBase
{
// преобразование списка T в список TBase

}
Здесь параметр T ограничен параметром TBase. То есть аргумент, заданный для T, должен быть совместим с аргументом, заданным для TBase.

Источник: https://docs.microsoft.com/ru-ru/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters
👍1
День двадцать второй. #CodeComplete
4. Утверждения
Утверждение – условие, которое всегда верно; предположение о дизайне, на котором основана процедура.
- документируйте предположения о дизайне через утверждения вместо комментариев;
- документируйте условия, которые всегда верны – в 100% случаев;
- если проверка утверждения терпит неудачу, правильное действие – изменить и перекомпилировать код;
- концептуально утверждение — это помощь при разработке, оно не используется в продакшн-коде (удаляется при компиляции).

Роль утверждений:
- полезны при использовании метода проектирования по контракту (с предусловиями и постусловиями);
- полезны в больших и сложных программах;
- используются как защитная техника, чтобы люди правильно использовали ваш код.

Важно:
Не путайте утверждения с обработкой ошибок!
День двадцать третий. #ЗаметкиНаПолях
Ковариантность и Контравариантность в Обобщениях (начало)

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

1. Общие понятия
Ковариантность и контравариантность относятся к возможности использования более конкретного (производного) типа или более общего (базового) типа, чем изначально обозначенный. Обобщённые параметры типа поддерживают ковариантность и конртавариантность, чтобы предоставлять большую гибкость в присвоении и использовании обобщённых типов. В следующих примерах допустим, что Animal – базовый класс, а Cat – производный от него.

Инвариантность означает, что вы можете использовать только изначально определённый тип.

Ковариантность позволяет вам использовать производный (более конкретный) тип, чем изначально обозначенный. Вы можете присвоить экземпляр IEnumerable<Cat> переменной типа IEnumerable<Animal>.
Ковариантные параметры типа позволяют вам делать присваивания, похожие на обычный полиморфизм:
IEnumerable<Cat> cats = new List<Cat>();
IEnumerable<Animal> animals = cats;
Класс List<T> реализует интерфейс IEnumerable<T>, поэтому List<Cat> реализует IEnumerable<Cat>. А ковариантный параметр типа делает всё остальное.

Котравариантность позволяет вам использовать более общий (базовый) тип, чем изначально обозначенный. Вы можете присвоить экземпляр делегата Action<Animal> переменной типа Action<Cat>.
Контравариантность выглядит контринтуитивной. В следующем примере создаётся делегат типа Action<Animal>, а затем он присваивается переменной типа Action<Cat>.
Action<Animal> actAnimal = (target)=>{Console.WriteLine(target.GetType().Name);};
Action<Cat> actCat = actAnimal;
actCat(new Cat()); // выводит “Cat”
Лямбда-выражение создаёт метод-делегат, который принимает один параметр типа Animal, и у которого нет возвращаемого значения. Этот делегат может быть присвоен переменной типа Action<Cat>, поскольку параметр T делегата Action<T> контравариантен. Когда делегат типа Action<Animal> вызывается так, как если бы он был типа Action<Cat>, его аргумент должен быть типа Cat.

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

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

Источник: https://docs.microsoft.com/ru-ru/dotnet/standard/generics/covariance-and-contravariance
👍1
День двадцать четвёртый. #ЗаметкиНаПолях
Ковариантность и Контравариантность в Обобщениях (продолжение)

2. Вариантность в обобщённых интерфейсах

Обобщённые интерфейсы с ковариантными параметрами типа
В .NET Framework 4+ несколько обобщённых интерфейсов имеют ковариантные параметры типа, например: IEnumerable<T>, IEnumerator<T>, IQueryable<T>, или IGrouping<TKey,TElement>. Все параметры типа этих интерфейсов ковариантны, поэтому они используются только для типов возвращаемых значений методов. Например, для вышеупомянутых классов Animal и производного от него Cat:
public void Feed(IEnumerable<Animal> animals) {…}

List<Cat> catList = new List<Cat>();
Feed(catList);
IEnumerable<Animal> animalIEnum = catList;
Поскольку параметр типа интерфейса IEnumerable ковариантен, а List<T> реализует IEnumerable<T>, то можно вызывать метод, принимающий IEnumerable<Animal> с параметром List<Cat>, а также присваивать переменной типа IEnumerable<Animal> экземпляр типа List<Cat> без приведения типов.

Обобщённые интерфейсы с контравариантными обобщёнными параметрами типа
В .NET Framework 4+ несколько обобщённых интерфейсов имеют контравариантные параметры типа, например: IComparer<T>, IComparable<T>, или IEqualityComparer<T>. Эти интерфейсы имеют только контравариантные параметры типа, поэтому параметры типа используются только в качестве параметров методов интерфейсов.
Следующий пример иллюстрирует контравариантные параметры типа. В примере объявляется абстрактный класс Shape со свойством Area и класс
ShapeAreaComparer, реализующий IComparer<Shape>. Реализация метода Compare этого интерфейса основана на сравнении площадей фигур по свойству Area, поэтому ShapeAreaComparer может быть использован для сортировки объектов Shape по площади.
Класс Circle наследует от Shape и переопределяет Area. Пример создаёт сортированную коллекцию SortedSet<T> из объектов Circle, используя конструктор, принимающий параметр IComparer<Circle>. Но вместо передачи параметра типа IComparer<Circle>, пример передаёт объект ShapeAreaComparer, реализующий IComparer<Shape>. Пример может передавать параметр базового (более общего) типа, поскольку параметр типа обобщённого интерфейса IComparer<T> контравариантен. Контравариантность позволяет ShapeAreaComparer сортировать коллекцию любого типа или коллекцию объектов разных типов, которые наследуют от Shape.
using System;
using System.Collections.Generic;

abstract class Shape
{
public virtual double Area { get { return 0; }}
}

class Circle : Shape
{
private double r;
public Circle(double radius) { r = radius; }
public double Radius { get { return r; }}
public override double Area
{
get { return Math.PI * r * r; }
}
}

class ShapeAreaComparer : IComparer<Shape>
{
int IComparer<Shape>.Compare(Shape a, Shape b)
{
if (a == null) return b == null ? 0 : -1;
return b == null ? 1 : a.Area.CompareTo(b.Area);
}
}

class Program
{
static void Main()
{
SortedSet<Circle> circlesByArea =
new SortedSet<Circle>(new ShapeAreaComparer())
{
new Circle(7.2),
new Circle(100),
null,
new Circle(.01)
};

foreach (Circle c in circlesByArea)
{
Console.WriteLine(c == null ?
"null" :
"Круг площадью " + c.Area);
}
}
}

Этот код производит следующий вывод:
null
Круг площадью 0.000314159265358979
Круг площадью 162.860163162095
Круг площадью 31415.9265358979
Аналогично можно сделать, например, объект Square, производный от Shape, и создать коллекцию объектов Square, используя тот же класс ShapeAreaComparer:
SortedSet<Square> squaresByArea = new SortedSet<Square>(new ShapeAreaComparer());

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

Источник: https://docs.microsoft.com/ru-ru/dotnet/standard/generics/covariance-and-contravariance
👍2
День двадцать пятый. #ЗаметкиНаПолях
Ковариантность и Контравариантность в Обобщениях (продолжение)

3. Вариантность в обобщённых делегатах
В .NET Framework 4+ обобщённые делегаты Func, такие как Func<T,TResult>, имеют ковариантные возвращаемые типы и контравариантные типы параметров. Обобщённые делегаты Action, такие как Action<T1,T2>, имеют контравариантные типы параметров. Это означает, что делегаты можно присваивать переменным, имеющим производные (более конкретные) типы параметров и (в случае обобщённых делегатов Func) базовые (более общие) возвращаемые типы.

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

Пусть объявлена следующая переменная:
Func<object, ArgumentException> fn1 = null;
Тогда можно привести её к типу Func с отличающимися параметрами обобщённого типа:
Func<string, Exception> fn2 = fn1; //явного приведения типа не требуется
Ecxeption e = fn2(“ ”);
Такое присвоение можно сделать без приведения типа, поскольку:
- тип параметра ковариантен, а string наследует от object,
- тип возвращаемого значения контравариантен, а Exception – базовый класс для ArgumentException.

Пример показывает, что этот обобщённый делегат может храниться в переменных или параметрах метода, имеющих более конкретные типы параметров и более общие возвращаемые типы, при условии, что все типы делегата сконструированы из обобщённого типа делегата Func<T,TResult>.

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

Источник: https://docs.microsoft.com/ru-ru/dotnet/standard/generics/covariance-and-contravariance
День двадцать шестой. #ЗаметкиНаПолях
Ковариантность и Контравариантность в Обобщениях (окончание)

4. Определение вариантных интерфейсов и делегатов
В .NET Framework 4+ в C# появились ключевые слова, позволяющие помечать параметры обобщённого типа интерфейсов и делегатов как ковариантные или контравариантные.

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

Примечание. Если метод интерфейса имеет параметр типа обобщённого делегата, параметр ковариантного типа этого интерфейса может использоваться для указания параметра контравариантного типа этого типа делегата.

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

Параметры вариантного типа могут иметь только интерфейсы и делегаты. Тип интерфейса или тип делегата может иметь как ковариантные, так и контравариантные параметры типа.

Следующий пример показывает определение ковариантного и контравариантного интерфейсов. Пусть есть абстрактные классы животное (Animal) и млекопитающее (Mammal). И производные от класса млекопитающего классы лошадь (Horse) и кот (Cat). В ковариантном интерфейсе фермы (IFarm), определён метод GetNextAnimal, возвращающий следующее животное. А в контравариантном интерфейсе кормилки для животных (IFeeder), определён обобщённый метод Feed имеющий тип параметра U, ограниченный типом параметра интерфейса T:

abstract class Animal {}
abstract class Mammal : Animal {}
class Horse : Mammal {}
class Cat : Mammal {}

interface IFarm<out T> where T : Animal
{
T GetNextAnimal();
}
interface IFeeder<in T> where T : Animal
{
void Feed<U>(U animal) where U : T;
}

class Program
{
static void Main(string[] args)
{
IFarm<Horse> horsefarm = null;
IFarm<Animal> animalfarm = horsefarm;
Animal animal = horsefarm.GetNextAnimal();

IFeeder<Animal> feeder = null;
feeder.Feed<Mammal>(new Horse());
feeder.Feed<Mammal>(new Cat());
}
}

Источник: https://docs.microsoft.com/ru-ru/dotnet/standard/generics/covariance-and-contravariance
День двадцать седьмой. #CodeComplete
5. Отладка

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

Этапы отладки:
1) Стабилизация ошибки. Найдите сценарий, при котором ошибка всегда проявляется.
2) Локализация источника ошибки. Точно найдите место в коде, где возникает ошибка.
3) Исправление ошибки.
4) Проверка и тестирование исправления.
5) Поиск похожих ошибок. Определите тип ошибки и проверьте, нет ли в остальном коде ошибок этого типа.

Советы по обнаружению ошибок
Следующие советы не обязательно применимы ко всем случаям, но могут помочь в некоторых сложных ситуациях:
- используйте все доступные данные для формулирования гипотезы о сути ошибки;
- создайте точные тесты, которые приводят к ошибке;
- воссоздайте ошибку несколькими разными способами;
- по ситуации: сузьте/расширьте сектор кода для поиска ошибки;
- проверьте методы, которые имели ошибки ранее (часто исправление одной ошибки приводит к возникновению другой);
- проверьте недавно изменённый код;
- проверьте код на типичные ошибки (широко распространённые вообще, вроде деления на ноль, нулевых указателей, выхода за границы массива и т.п., либо типичные для вашей области/организации/вас лично);
- поговорите с другим человеком (обзор кода): часто просто объяснение проблемы кому-то другому помогает понять, что не так;
- сделайте паузу: займитесь чем-то отвлечённым на некоторое время;
- прочитайте все предупреждения компилятора: предупреждения компилятора не сообщают об ошибках напрямую, но могут свидетельствовать о недостатках в коде, приводящих к ошибкам в связанных с этим блоком местах;
- проверьте, не является ли это ошибкой проектирования: плохо спроектированный класс или метод иногда проще переписать заново, чем постоянно исправлять;
- проверьте, нет ли ошибки в тестовых данных: написанию тестов обычно уделяют меньше внимания, чем основному коду, поэтому ошибки в тестах встречаются гораздо чаще;
- изначально избегайте ошибок: правильное проектирование и написание кода приводит к значительному сокращению количества ошибок.

Инструкция по исправлению ошибки:
1) убедитесь, что вы понимаете проблему, прежде чем исправлять её;
2) убедитесь, что вы понимаете всю программу, а не только проблемную часть;
3) подтвердите диагноз: убедитесь с помощью различных тестов, что ошибка именно здесь и именно в этом;
4) расслабьтесь :)
5) сохраните исходный вариант кода (системы управления версиями очень помогают);
6) делайте ОДНО исправление за раз: не пытайтесь исправить сразу несколько ошибок одновременно, это приводит к возникновению новых ошибок;
7) проверьте ваше исправление;
8) выполните юнит-тесты (не только относящиеся к исправлению, чтобы проверить, не привело ли исправление к ошибкам в других местах);
9) выполните интеграционные тесты с изменённым кодом;
10) поищите похожие ошибки.

Отладка с применением грубой силы
1) Установите максимальное время на отладку обычным способом, а потом рассмотрите вариант перехода на отладку грубой силой.
2) Составьте список вещей, которые стоит проверить.

Примеры отладки грубой силой:
- переписывание кода с нуля;
- компиляция кода с полной отладочной информацией;
- установка прерывания на каждом исключении;
- создание набора автоматизированных тестов и запуск их на всю ночь;
- выполнение полного обзора проекта/кода;
- точное воссоздание конфигурации оборудования конечного пользователя.
👍1
День двадцать восьмой. #ЗаметкиНаПолях
Явная реализация интерфейсного метода (EIMI)
class SomeType : IDisposable
{
public void Dispose()
{
Console.WriteLine(“public Dispose”);
}
void IDisposable.Dispose()
{
Console.WriteLine(“IDisposable.Dispose”);
}
}
Если в C# перед именем метода вы ставите имя интерфейса, в котором определён этот метод, то вы создаёте явную реализацию интерфейсного метода. Этому методу нельзя указывать область доступа (private или public), однако компилятор, создавая метаданные для метода, делает его private. Поэтому единственный способ вызвать интерфейсный метод – обратиться через переменную этого интерфейсного типа.

Явная реализация методов интерфейса служит двум главным целям:
1) Поскольку она недоступна через экземпляры класса, это позволяет исключить эти методы из общедоступного интерфейса класса. Это, в частности, полезно, когда класс реализует внутренний интерфейс, который не представляет интереса для пользователей класса.
Например, List<T> реализует IList<T> неявно, а IList (необобщённый интерфейс) явно. Это значит, что, когда вы используете класс напрямую, вы увидите только обобщённые методы и не увидите методы, принимающие Object.
Допустим, вы создали экземпляр List<Person>. Если бы IList был реализован неявно, тогда у вас было бы два метода Add, напрямую доступных из класса: Add(Person item) и Add(Object item), что разрушило бы безопасность типов, предлагаемую обобщениями. Вызов list.Add("Foo") успешно бы скомпилировался, поскольку была бы автоматически выбрана перегруженная версия Add(Object).

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

В следующем примере класс ресторана реализует два интерфейса для выдачи вариантов меню: на вынос IWindow и «в ресторане» IRestaurant. Эти интерфейсы имеют одинаковые методы GetMenu, но с разными типами возвращаемого значения. Также ресторан может иметь свою собственную реализацию метода GetMenu, не имеющую отношения к интерфейсным методам. Основная программа запрашивает все три метода, приводя экземпляр класса к интерфейсному типу для вызова явных реализаций интерфейсных методов.
using System;

public abstract class Menu { }
public class WindowMenu : Menu { }
public class RestaurantMenu : Menu { }

public interface IWindow { WindowMenu GetMenu(); }
public interface IRestaurant { RestaurantMenu GetMenu(); }

public sealed class Pizzeria : IWindow, IRestaurant
{
//Явная реализация интерфейсного метода для IWindow
WindowMenu IWindow.GetMenu() => new WindowMenu();

//Явная реализация интерфейсного метода для IRestaurant
RestaurantMenu IRestaurant.GetMenu() => new RestaurantMenu();

//Дополнительный открытый метод, не имеющий отношения к интерфейсам
public string GetMenu() => "Public method";
}

class Program
{
static void Main(string[] args)
{
Pizzeria mp = new Pizzeria();
//открытый метод
Console.WriteLine(mp.GetMenu());

//Метод IWindow.GetMenu
var windowMenu = ((IWindow) mp).GetMenu();
Console.WriteLine(windowMenu.ToString());

//Метод IRestaurant.GetMenu
var restaurantMenu = ((IRestaurant) mp).GetMenu();
Console.WriteLine(restaurantMenu.ToString());

Console.ReadLine();
}
}

Источник: Джеффри Рихтер “CLR via C#”. 3-е изд. – СПб.: Питер, 2012. Глава 13.
👍2