Опытный разработчик не так давно зашёл в .Net и поставил цель получить сертификат Microsoft. Свой ежедневный прогресс он описывает на канале .Net Разработчик. Заметки об изученном материале, советы по повышению производительности и поддержке мотивации, ин
static async IAsyncEnumerable<int> GetAsync(int n)
{
for (int i = 0; i < n; i++)
{
await Task.Delay(1000);
yield return i;
}
}
JsonSerializerOptions options = new() {
WriteIndented = true
};
using Stream output =
Console.OpenStandardOutput();
var data = new { Data = GetAsync(5) };
await JsonSerializer.SerializeAsync(
output, data, options);
Для десериализации JSON-массивов корневого уровня добавлен метод DeserializeAsyncEnumerable:
// оборачивает текст в IAsyncEnumerable<T>
using MemoryStream stream =
new(Encoding.UTF8.GetBytes("[0,1,2,3,4]"));
await foreach (int item in
JsonSerializer.DeserializeAsyncEnumerable<int>(stream))
{
Console.WriteLine(item);
}
6. Работа с JSON как с DOM
.NET 6 предоставляет типы для произвольного доступа к элементам JSON в структурированном представлении данных. Новые типы из пространства имён System.Text.Json.Nodes:
- JsonArray
- JsonNode
- JsonObject
- JsonValue
// Парсим объект JSON
var node = JsonNode.Parse(
"{\"Value\":\"Text\",\"Array\":[1,5,13,17,2]}"
);
string value = (string)node["Value"];
// либо
value = node["Value"].GetValue<string>();
Console.WriteLine(value); // Text
int item = node["Array"][1].GetValue<int>();
// либо
item = (int)node["Array"][1];
Console.WriteLine(arrayItem); // 5
// Создаём JsonObject
var obj = new JsonObject
{
["Value"] = "Text",
["Array"] = new JsonArray(1, 5, 13, 17, 2)
};
Console.WriteLine(obj["Value"]); // Text
Console.WriteLine(obj["Array"][1]); // 5
// преобразуем в строку
string json = obj.ToJsonString();
Console.WriteLine(json);
// {"Value":"Text","Array":[1,5,13,17,2]}
Источник: blog.okyrylchuk.dev/system-…dotnet-6JsonSerializerOptions options = new()
{
ReferenceHandler = ReferenceHandler.IgnoreCycles
};
string json = JsonSerializer.Serialize(myObj, options);
2. Уведомления при сериализации/десериализации
Появилось 4 новых интерфейса, которые можно реализовать в соответствии с вашими потребностями:
- IJsonOnDeserialized
- IJsonOnDeserializing
- IJsonOnSerialized
- IJsonOnSerializing
class Product : IJsonOnDeserialized, IJsonOnSerializing
{
public string Name { get; set; }
public string Test { get; set; }
void IJsonOnDeserialized.OnDeserialized()
=> Validate();
void IJsonOnSerializing.OnSerializing()
=> Validate();
private void Validate()
{
if (Name is null)
throw new InvalidOperationException(
"Name can't be null."
);
}
}
3. Порядок сериализации свойств
Атрибут JsonPropertyOrder позволяет контролировать порядок сериализации свойств:
class Product
{
// после Price
[JsonPropertyOrder(2)]
public string Category { get; set; }
// после свойств с порядком по умолчанию
[JsonPropertyOrder(1)]
public decimal Price { get; set; }
// порядок по умолчанию - 0
public string Name { get; set; }
// перед свойствами с порядком по умолчанию
[JsonPropertyOrder(-1)]
public int Id { get; set; }
}
Вывод:
{
"Id": 1,
"Name": "Surface Pro 7",
"Price": 550,
"Category": "Laptops"
}
4. Сериализация из/в поток
В .NET 6 добавлены перегрузки синхронных методов Serialize/Deserialize для потоков:
// десериализация из потока
using MemoryStream ms = new MemoryStream(bytes);
MyClass ex =
JsonSerializer.Deserialize<MyClass>(ms);
// сериализация в поток
JsonSerializerOptions options = new() {
WriteIndented = true
};
using Stream output = Console.OpenStandardOutput();
MyClass ex = new() { Value = "Test" };
JsonSerializer.Serialize<MyClass>(output, ex, options);
Окончание следует…
Источник: blog.okyrylchuk.dev/system-…dotnet-6DateTime в .NET является полезным примером объекта-значения.
Объект-значение — это неизменяемый тип, экземпляры которого можно отличить только по значению их свойств. Любые два объекта-значения с одинаковыми свойствами можно считать равными.
При моделировании системы многие разработчики задаются вопросом, когда использовать объект-значение, когда использовать сущность и как их объединить. Часто задуманный изначально объект-значение далее ведёт себя как сущность.
Разработчики спрашивают что-то вроде: «Могу ли я иметь список объектов-значений в моей модели», - и ответ почти всегда - нет. Опять здесь может помочь DateTime. Имеет ли смысл иметь список объектов DateTime в модели? DateTime сам по себе, без контекста, не имеет значения в модели предметной области. Только когда он используется для описания чего-то (обычно Сущности) он становится полезным. Вот пример:
07.02.2022 08.02.2022 09.02.2022Если вы обнаружите этот список в модели, что он значит? Нет способа это узнать. А вот так:
var article = new Article {
CreationDate = new DateTime(2022,2,7),
PublicationDate = new DateTime(2022,2,8),
LastModifiedDate = new DateTime(2022,2,9)
};
Те же три значения DateTime теперь имеют контекст, так как они используются для описания сущности статьи.
Аналогичный вопрос возникает относительно адреса. Опять же, он сам по себе не имеет смысла. Он имеет значение только тогда, когда вы делаете его адресом клиента или адресом доставки.
Почему важна неизменяемость?
Все свойства объектов-значений должны быть доступны только для чтения. Но какой в этом смысл? Вы наверняка часто использовали тип DateTime в своих программах. А сколько раз вы проверяли день или месяц экземпляра DateTime на допустимое значение? Ни разу. Вам не нужно этого делать, потому что невозможно создать экземпляр DateTime в недопустимом состоянии, и экземпляры неизменяемы.
Если вы попытаетесь создать DateTime с недопустимыми значениями, например, с месяцем 13, вы получите исключение ArgumentOutOfRangeException:
var someDate = new DateTime(2022,13,1);А если вы попытаетесь изменить значение свойства, вы получите ошибку компиляции, т.к. оно доступно только для чтения:
someDate.Month = 13;Есть и другие причины важности неизменяемости, но с точки зрения моделирования предметной области это самая важная. Вы обретаете уверенность в том, что эти значения верны, и устраняете массу повторяющегося кода, который в противном случае был бы необходим для подтверждения достоверности во многих местах. Так как же их изменить? Объекты-значения могут предоставлять методы, которые «кажутся» изменяющими их, но на самом деле создают новые экземпляры. Это относится как к
DateTime, так и к String в .NET. AddDays(), ToLower() и т.п. возвращают новый экземпляр.
Помните об этом при разработке собственных объектов-значений. Любые предоставляемые вами методы, которые будут манипулировать состоянием объекта, должны вместо этого возвращать новый экземпляр с обновлёнными значениями. Одним очевидным преимуществом этого подхода является то, что логика проверки в конструкторе объекта-значения гарантированно будет выполняться для каждого метода модификации без необходимости дублировать её где-либо ещё.
Итого
Хотя объекты-значения чаще всего обсуждаются в контексте DDD, есть примеры их использования в фреймворках, с которыми вы работаете каждый день. В .NET типы DateTime (и string) являются примерами объектов-значений, и знание их особенностей может помочь при проектировании таких типов в ваших приложениях.
Источник: ef='https://ardalis.com/datetime-as-a-value-object/'>https://ardalis.com/datetime-as-a-value-object/Parallel содержит простой метод Invoke, спроектированный для таких сценариев. В следующем примере массив разбивается надвое, и две половины обрабатываются независимо:
void Process(double[] arr)
{
Parallel.Invoke(
() => ProcessPartial(arr, 0, arr.Length/2),
() => ProcessPartial(arr, arr.Length/2, arr.Length)
);
}
void ProcessPartial(double[] array, int begin, int end)
{
// Обработка, интенсивно использующая процессор...
}
Методу Parallel.Invoke также можно передать массив делегатов, если количество вызовов неизвестно до момента выполнения:
void DoActions(Action action)
{
Action[] actions = Enumerable.Repeat(action, 20).ToArray();
Parallel.Invoke(actions);
}
Parallel.Invoke поддерживает отмену, как и другие методы класса Parallel:
void DoActions(Action action, CancellationToken token)
{
Action[] actions = Enumerable.Repeat(action, 20).ToArray();
Parallel.Invoke(
new ParallelOptions { CancellationToken = token },
actions);
}
Метод Parallel.Invoke — отличное решение для простого параллельного вызова. Отмечу, что он уже не так хорошо подходит для ситуаций, в которых требуется активизировать действие для каждого элемента входных данных (для этого лучше использовать Parallel.ForEach), или если каждое действие производит некоторый вывод (вместо этого следует использовать Parallel LINQ).
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 4.RecordSet – и ловлю NullReference. Посреди кода, активно обращающегося к этому RecordSet. Прилично офигеваю и лезу дебажить. Оказывается, результаты скидываются в другой объект, RecordSet уничтожается, а ошибки маскировались вызовом «On Error Resume Next» где-то выше по стеку вызовов – я его закомментировал, пока делал изменения. Этот код только при мне «работал» лет 6, и примерно ещё лет 10 до меня.
Бывали у вас подобные истории в практике?CalculateIncomeTax и CalculateTransportTax, вы не можете предполагать, что пользователя интерфейса будут заботить только входные и выходные данные методов. Он также должен быть уверен, что во всех реализациях этого интерфейса каждый из методов будет содержать логику расчёта соответствующего налога, и она, например, не будет перепутана местами. Этого компилятор не сможет обнаружить. Об этом говорит принцип подстановки Лисков.
Источник: levelup.gitconnected.com/design-…b7c3500aCar и из метода Accelerate хотите записать в лог текущую скоростью автомобиля. Вы бы внедрили ILogger, верно? Да, это сработает, и по сегодняшним стандартам дизайна это идеально. Однако, вот такой вопрос: можем ли мы сказать, что класс Car на самом деле зависит от ILogger до такой степени, что без него он не сможет выполнять свою работу?
Ответ прост: Car не должен зависеть от ILogger. Вы можете возразить, что даже если он не полностью зависит от него, он все равно нуждается в нём. Не совсем так. На самом деле ILogger нужен основному приложению, которое знает и о Car, и о ILogger. Основное приложение должно получить некоторую информацию от класса Car, а затем использовать ILogger для регистрации этой информации. Поэтому правильной реализацией будет удалить эту зависимость и реализовать события. В нашем примере класс Car должен определить событие SpeedChanged, а основное приложение должно подписаться на него:
public delegate void SpeedChangedEventHandler(object sender, double speed);
public class Car
{
public event SpeedChangedEventHandler SpeedChanged;
public void Accelerate()
{
var speed = …;
…
OnSpeedChanged(speed);
}
protected virtual void OnSpeedChanged(double speed)
{
SpeedChanged?.Invoke(this, speed);
}
}
Заметьте, что мы не вызываем событие напрямую из метода Accelerate, а оборачиваем вызов в виртуальный метод, который, во-первых, безопасно вызывает событие, а во-вторых, может быть переопределён (например, если для каких-то типов наследников не нужно вызывать это событие).
5. Использование моментальных снимков (Snapshot)
Продолжая пример выше, допустим, мы захотели отображать значение скорости на экране. Создадим объект трекера, который будет подписан на событие SpeedChanged:
public class Tracker
{
private double currentSpeed = 0;
public Tracker(Car car)
{
car.SpeedChanged += (o, speed) =>
{
currentSpeed = speed;
ShowOnScreen(speed);
};
ShowOnScreen(currentSpeed); // здесь будет 0
}
…
}
Проблема в том, что к моменту создания трекера объект Car уже может быть создан, запущен и иметь некоторую постоянную скорость. Тогда до изменения скорости трекер будет выводить на экран 0. Здесь поможет шаблон «Моментальный снимок» (Snapshot). Это неизменяемый объект-значение, хранящий состояние объекта Car. Для него как нельзя лучше подойдёт тип записи:
public record CarState(double speed, double temperature, …);
public class Car
{
public CarState Snapshot { get; private set; } = new CarState(0,0,…);
public void Accelerate()
{
var speed = …;
…
Snapshot = Snapshot with { Speed = speed };
OnSpeedChanged(Snapshot.Speed);
}
…
}
Тогда при создании трекера его можно инициализировать данными из моментального снимка объекта Car:
public Tracker(Car car)
{
currentSpeed = car.Snapshot.Speed;
car.SpeedChanged += (o, speed) =>
{
…
}
}
Окончание следует…
Источник: levelup.gitconnected.com/design-…b7c3500avoid RenameFile(string newName) принимает новое имя файла и ничего не возвращает. В реальном мире это может быть проблемой, потому что операция может завершиться неудачей, и тогда метод должен что-то вернуть, чтобы сообщить об этом вызывающей стороне.
Я уже слышу ваши варианты: вернуть булевый результат, вернуть новое имя файла в случае успеха или пробросить исключение вызывающему коду. Эти решения могут работать, однако лучше всего здесь вернуть унифицированный объект с некоторыми данными, описывающими, что на самом деле произошло:
public enum FileRenameFailureReason {
None = 0,
ExistingName = 1,
PathTooLong = 2
}
public class FileRenameResult
{
public FileRenameFailureReason FailureReason { get; }
public string FinalName { get; }
public bool Succeeded =>
FailureReason == FileRenameFailureReason.None;
}
Некоторые преимущества возврата унифицированного объекта:
- Перемещение логики туда, где она используется: обработчик хранит логику возврата правильного результата, вызывающий код – логику обработки этого результата (например, выдачи сообщения пользователю).
- Определение более чёткого контракта между вызывающим кодом и обработчиком.
- Теперь вызывающий код может принимать точные решения на основе полной информации, предоставленной обработчиком.
- Работа с унифицированными объектами значительно упрощает разработку общих модулей.
2. Разбиение на страницы
Разбиение на страницы — один из широко используемых паттернов, но почти никто не говорит о нём с точки зрения практики проектирования. Когда вы создаёте API, вы должны контролировать объём данных, проходящих через него. Поэтому при разработке API полезно спроектировать стратегию регулирования объема трафика. Вы можете сказать, что у вас нет лимитов, но скорее всего они просто настолько большие, что вы их не осознаёте. Например, если у вас есть метод GetAllEmployees, вы можете разрешить вызывающей стороне получать данные всех сотрудников. А что, если когда-нибудь в системе будет 10 000 сотрудников. Сейчас самое подходящее время вернуть только первые 1000 или 5000 и предоставить вызывающей стороне объект метаданных, сообщающий, что произошло и как получить следующую порцию.
3. Делегаты вместо Func<>
Когда нужно определить ссылку на метод, иногда полезнее явно определить делегат, а затем его использовать:
public delegate bool MessageHandler(int id, string msg);
public class Module1
{
public MessageHandler Handler { get; set; }
}
public class Module2
{
public Func<int, string, bool> Handler { get; set; }
}
Отличие в том, что вы получите более информативные подсказки в IntelliSense в виде имён параметров вместо arg1, arg2 в случае использования Func<>.
Продолжение следует…
Источник: levelup.gitconnected.com/design-…b7c3500aWHERE перед DELETE.
- Магия вуду существует, и ты можешь в этом убедиться, когда что-то происходит в продакшене, а не на машине разработчиков.
Источник: twitter.com/nickcha…22320131