.NET Разработчик
6.6K subscribers
445 photos
4 videos
14 files
2.15K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День пятьсот девяносто седьмой. #MoreEffectiveCSharp
17. Избегайте Возврата Ссылок на Внутренний Класс
Вы можете думать, что свойство только для чтения доступно только для чтения и вызывающие объекты не могут его изменять. К сожалению, так получается не всегда. Если свойство возвращает ссылочный тип, вызывающий код может получить доступ к любому общедоступному члену этого объекта, включая те, которые изменяют его состояние. Например:
public class MyObject {
public MyObject() {
Data = new List<ImportantData>();
}
public List<ImportantData> Data { get; }

}
Получаем доступ к списку и удаляем его элементы. Это поведение не предусмотрено, но и не запрещено:
var stuff = obj.Data;
stuff.Clear();
Таким образом свойство только для чтения - дыра в продуманной инкапсуляции вашего класса. Не говоря уже о том, что создатель класса рассматривает это свойство как неизменяемое и не ожидает подвоха.

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

Очевидно, что вы хотите предотвратить подобное поведение. Вы создали интерфейс для своего класса и хотите, чтобы пользователи использовали его, а не изменяли внутреннее состояние ваших объектов без вашего ведома. Для защиты внутренних данных от непреднамеренных изменений есть 4 стратегии: типы значений, неизменяемые типы, интерфейсы и оболочки.

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

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

3. Интерфейсы позволяют клиентам получать доступ к подмножеству функционала вашего внутреннего члена. Предоставляя функциональность через интерфейс, вы сводите к минимуму вероятность того, что ваши внутренние данные изменятся не так, как вы предполагали. Клиенты могут получить доступ к внутреннему объекту через предоставленный вами интерфейс, который не будет включать в себя все функции класса. В нашем случае публичное свойство можно представлять клиентам в виде IEnumerable<T> вместо List<T>.

4. Последний вариант – предоставить объект-оболочку, минимизирующую варианты доступа к содержащемуся объекту. В .NET представлены различные типы неизменяемых коллекций, которые это поддерживают. Тип System.Collections.ObjectModel.ReadOnlyCollection<T> - это стандартный способ обернуть коллекцию для предоставления во вне данных только для чтения:
public class MyObject {
private List<ImportantData> listOfData =
new List<ImportantData>();

public ReadOnlyCollection<ImportantData> Data =>
new ReadOnlyCollection<ImportantData>(listOfData);

}

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

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 17.
День шестьсот девяносто восьмой. #MoreEffectiveCSharp
18. Предпочитайте Переопределения Обработчикам Событий
Многие классы .NET предоставляют два разных способа обработки событий. Вы можете подписать обработчик событий или переопределить виртуальную функцию базового класса. Внутри производных классов вы всегда должны переопределять виртуальную функцию. Ограничьте использование обработчиков событий реагированием на события в несвязанных объектах.

Рассмотрим код приложения WPF, где нужно реагировать на события нажатия мыши. Вы можете переопределить метод OnMouseDown():
public partial class MainWindow : Window
{
//…
protected override void OnMouseDown(MouseButtonEventArgs e)
{
DoMouseThings(e);
base.OnMouseDown(e);
}
}

Либо вы можете подписать обработчик события, что требует изменений как в файле C#, так и в файле XAML:
<Window x:Class="WpfApp1.MainWindow"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
MouseDown="OnMouseDownHandler">
<Grid></Grid>
</Window>

public partial class MainWindow : Window
{
//…
private void OnMouseDownHandler(
object sender, MouseButtonEventArgs e)
{
DoMouseThings(e);
}
}

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

Хорошо, но подписка на события зачем-то добавлена. Зачем?
1. Переопределения предназначены для производных классов. Но сторонние классы должны использовать механизм событий.
2. XAML поддерживает декларативную подписку на события. То есть дизайнер приложения в визуальном редакторе может выбрать действие при нажатии мыши, если оно доступно, и не добавлять работы программисту.
3. Это позволяет подписываться на события во время выполнения. Вы можете подключить разные обработчики событий, в зависимости от обстоятельств выполнения программы.
4. Возможно подключить несколько обработчиков к одному событию.

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

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 18.
День семьсот одиннадцатый. #MoreEffectiveCSharp
19. Избегайте Перегрузки Методов, Определённых в Базовых Классах
Когда базовый класс выбирает имя члена, он назначает определённый семантический смысл этому имени. Ни при каких обстоятельствах производный класс не должен использовать то же самое имя для других целей. И всё же есть много причин, по которым производный класс может захотеть использовать то же имя. Например, реализовать ту же семантику другим способом или с другими параметрами. Но вы не должны перегружать методы, объявленные в базовом классе.

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

Добавление перегрузки для метода базового класса увеличивает вероятность того, что ваша интерпретация спецификации не совпадёт с интерпретацией компилятора, и, безусловно, запутает ваших пользователей. Решение простое: выберите другое имя метода.

Рассмотрим пару примеров:
public class Fruit { }
public class Apple : Fruit { }
Вот класс с методом, использующим производный параметр (Apple):
public class Animal {
public void Eat(Apple food) =>
WriteLine("Animal.Eat");
}
var obj1 = new Animal();
obj1.Eat(new Apple());
Понятно, что это выведет "Animal.Eat". Добавим производный класс с перегруженным методом с параметром базового типа:
public class Monkey : Animal {
public void Eat(Fruit food) =>
WriteLine("Monkey.Eat");
}
Итак, что выведет следующий код?
var obj2 = new Monkey();
obj2.Eat(new Apple());
obj2.Eat(new Fruit());

Оба вызова выведут "Monkey.Eat". Всегда в первую очередь вызывается метод производного класса, даже если в базовом классе есть более подходящий кандидат. Смысл в том, что автор производного класса лучше знает сценарий использования, поэтому производному методу отдаётся предпочтение. А если вот так:
Animal obj3 = new Monkey();
obj3.Eat(new Apple());
Смотрим внимательно: тип времени компиляции obj3 - Animal (базовый), хотя, во время выполнения тип будет Monkey (производный). Метод Eat не виртуальный, поэтому obj3.Eat() должен использовать Animal.Eat.

Больше ада! Добавим дженериков:
public class Animal {

public void Consume(IEnumerable<Apple> food) =>
WriteLine("Animal.Consume");
}
И перегрузку с коллекцией базового типа в производном классе:
public class Monkey : Animal {

public void Consume(IEnumerable<Fruit> food) =>
WriteLine("Monkey.Consume");
}
var food = new List<Apple> { new Apple(), new Apple() };
var obj2 = new Monkey();
obj2.Consume(food);

Что будет выведено на этот раз? Начиная с C#4.0 обобщённые интерфейсы поддерживают ковариантность и контравариантность. Это означает, что Monkey.Consume является кандидатом для IEnumerable<Apple>, хотя формально тип его параметра IEnumerable<Fruit>. Однако более ранние версии C# не поддерживают вариантности, и в них обобщённые параметры инвариантны. В этом случае единственным кандидатом будет Animal.Consume.

Да, вы можете удивить друзей на вечеринке программистов глубокими познаниями логики разрешения перегрузок в C#. Но не ожидайте, что пользователи вашего API будут иметь такие подробные знания, чтобы правильно использовать ваш API. Просто не перегружайте методы, объявленные в базовом классе. Это не представляет никакой ценности и только приведёт ваших пользователей в замешательство.

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 19.
День семьсот двадцать четвёртый. #MoreEffectiveCSharp
20. Учитывайте, Что События Усиливают Связанность Объектов во Время Выполнения
Кажется, что события предоставляют способ полностью отделить ваш класс от классов, которые он должен уведомлять. Вы определяете тип события и позволяете подписываться на него. Внутри вашего класса вы вызываете событие. Ваш класс ничего не знает о подписчиках и не налагает ограничений на них. Код можно расширить, создавая любое необходимое поведение при возникновении этих событий. Однако не всё так просто.

Начнём с того, что некоторые типы аргументов событий содержат флаги состояния, которые предписывают вашему классу выполнять определённые операции.
public class WorkerEngine {
public event
EventHandler<WorkerEventArgs> OnProgress;
public void DoLotsOfStuff() {
for (int i = 0; i < 100; i++) {
SomeWork();
var args = new WorkerEventArgs();
args.Percent = i;
OnProgress?.Invoke(this, args);
if (args.Cancel) return;
}
}
private void SomeWork(){…}
}
Теперь все подписчики на это событие связаны. Если один подписчик запросит отмену операции, установив Cancel в true, другой может отменить это. Таким образом, последний подписчик в цепочке может переопределить действие любого предыдущего. Невозможно заставить иметь только одного подписчика, и нет способа гарантировать, что делегат какого-то из подписчиков будет выполнен последним. Вы можете изменить аргументы события, чтобы гарантировать, что после установки флага отмены ни один подписчик не сможет его выключить:
public class WorkerEventArgs : EventArgs {
public int Percent { get; set; }
public bool Cancel { get; private set; }
public void RequestCancel() {
Cancel = true;
}
}
Это изменение сработает в этом случае, но так можно сделать не всегда. Если вам нужно убедиться, что есть ровно один подписчик, придётся выбрать другой способ связи классов. Например, определить интерфейс и вызывать метод интерфейса вместо события. Или запрашивать делегат подписчика в качестве параметра метода. Затем этот единственный подписчик может решить, хочет ли он поддерживать несколько подписчиков и как организовать семантику запросов на отмену.

Во время выполнения возникает ещё одна форма связи между источником события и подписчиками. Источник содержит ссылку на делегат, который предоставляет подписчик. Время жизни объекта подписчика теперь будет соответствовать времени жизни объекта источника. Источник будет вызывать обработчик подписчика всякий раз, когда происходит событие. Но это не должно продолжаться после удаления подписчика. То есть подписчикам на события необходимо реализовать паттерн Disposable и отписываться от события в методе Dispose(). В противном случае подписчики продолжат существовать, поскольку в источнике будут ссылки на их делегаты.

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

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

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 20.
День 1191.
Подборка тегов, используемых в постах на канале, чтобы облегчить поиск. Не могу гарантировать, что все 1190 постов идеально и корректно помечены тегами, но всё-таки, эта подборка должна помочь.

Общие
Эти посты на совершенно разные темы, помечены этими тегами только с целью различать общую направленность поста.

#ЗаметкиНаПолях – технические посты. Краткие описания теории, особенности языка C# и платформы .NET, примеры кода, и т.п.

#Шпаргалка - примеры кода, команды для утилит и т.п.

#Юмор – шутки, комиксы и просто весёлые тексты или ссылки на видео.

#Оффтоп – всё прочее.


Специализированные
Эти теги более тематические, выделяют основную тему поста.

#Карьера – советы по повышению продуктивности, карьерному росту, прохождению собеседований и т.п.

#Книги – обзоры книг, которые (чаще всего) я лично прочитал, либо ещё нет, но советую прочитать.

#Курсы – обзоры и ссылки на онлайн курсы.

#МоиИнструменты – различные программы, утилиты и расширения IDE, которые я использую в работе.

#ЧтоНовенького – новости из мира .NET.


Узкоспециализированные
Эти теги относятся к определённой узкой теме.

#AsyncTips – серия постов из книги Стивена Клири “Конкурентность в C#”
#AsyncAwaitFAQ – серия постов “Самые Частые Ошибки при Работе с async/await.”

#BestPractices – советы по лучшим практикам, паттернам разработки.

#DesignPatterns – всё о паттернах проектирования, SOLID, IDEALS и т.п.

#DotNetAZ – серия постов с описанием терминов из мира .NET.

#GC – серия постов “Топ Вопросов о Памяти в .NET.” от Конрада Кокосы.

#MoreEffectiveCSharp – серия постов из книги Билла Вагнера “More Effective C#”.

#Testing – всё о тестировании кода.

#TipsAndTricks – советы и трюки, в основном по функционалу Visual Studio.

#Quiz - опросы в виде викторины.

#97Вещей – серия постов из книги “97 Вещей, Которые Должен Знать Каждый Программист”.

#ВопросыНаСобеседовании – тег говорит сам за себя, самые часто задаваемые вопросы на собеседовании по C#, ASP.NET и .NET.
#ЗадачиНаСобеседовании – похоже на вопросы, но здесь больше приводятся практические задачи. Чаще всего это 2 поста: собственно задача и ответ с разбором.

#КакСтатьСеньором – серия постов «Как Стать Сеньором» с советами о продвижении по карьерной лестнице.

Помимо этого, можно просто воспользоваться поиском по постам и попробовать найти то, что вам нужно.
1👍60👎1