C#Hive: Projects & Progress | Программирование
前往频道在 Telegram
Сообщество единомышленников C#: решаем задачи, учимся, развиваемся и общаемся вместе. Советы по работе на фрилансе, готовые проекты, код ревью, рекомендации и исследования. Вопросы/сотрудничество: @tel_phil9
显示更多1 780
订阅者
无数据24 小时
-17 天
-1030 天
帖子存档
🖥 ValueTask<T> для максимальной производительности
Разница между Task<T> и ValueTask<T> заключается в том, что Task<T> — ссылочный тип, а ValueTask<T> — значимый тип. Эти типы представляют асинхронные операции, но имеют разные особенности и используются в разных случаях.
Использование типа ValueTask<T> во многом аналогично применению Task<T>, за исключением некоторых различий в работе с памятью. ValueTask — структура, которая содержит большее количество полей. Поэтому применение ValueTask вместо Task приводит к копированию большего количества данных и соответственно создаёт некоторые дополнительные издержки.
Преимуществом ValueTask перед Task является то, что данный тип позволяет избежать дополнительных выделений памяти в куче. Например, иногда требуется синхронно возвратить некоторое значение. Если асинхронная операция часто завершается синхронно (например, данные уже находятся в кэше), использование ValueTask<T> может избежать выделения памяти для Task, что положительно скажется на производительности, как в примере ниже:
public async ValueTask<string> GetDataAsync(string key)
{
if (cache.TryGetValue(key, out string value))
{
// Завершается синхронно
return value;
}
// Асинхронный запрос
value = await dataSource.GetDataAsync(key);
cache[key] = value;
return value;
}
#Полезно #Async #Await📈 Обогащаемся знаниями!
#Дайджест полезного материала за последнее время.
Задачи:
⏺Наибольший общий делитель
⏺Решето Эратосфена
⏺Преобразование числа в пропись
⏺Быстрое возведение в степень
⏺Гномья сортировка
⏺Постоянная Капрекара
⏺Ключевые слова
Опросы — что будет выведено на экран:
⏺№1
⏺№2
⏺№3
⏺№4
⏺№5
⏺№6
Полезное:
⏺Имена объектов, если бы они были русскими
⏺Отправка решённой Google reCAPTCHA v2 через Selenium
⏺Асинхронные стримы и интерфейс IAsyncEnumerable
⏺Фильтр Блума
⏺Измерение производительности с BenchmarkDotNet
⏺Выражение stackalloc
⏺Валидация объектов
⏺Читаемый ввод/вывод в консоли
Самые важные хэштеги:
#Фриланс #Полезно #Задача #Опрос #LINQ #Проект #НеочевидныеБаги
🖥 Из примера выше, что будет выведено на экран?
🖥 Консоль: читаемый ввод/вывод
Консоль крайне удобная и полезная вещь в программировании, а значит нам нужно хорошо понимать её нюансы, как и нюансы любых других инструментов, работать с которыми нам приходится.
Напишем следующий код:
string outRu, outEn;
outRu = "Русский вывод (RU). Ввод: ";
outEn = "English output (EN). Input: ";
Console.Write(outRu);
Console.ReadLine();
Console.Write(outEn);
Console.ReadLine();
Ожидаемый вывод из кода выше нам вполне очевиден, но может возникнуть ситуация, при которой можно увидеть нечто следующее:
??????? ????? (RU). ????: 10 десять English output (EN). Input: 10 tenОсобая паника может накрыть в случаях, когда на нашем ПК всё работало красиво и без косяков, а после передачи решения заказчику он кидает нам скриншоты таких выводов и недоумевает — ничего же непонятно. Проблемы в этом даже нет, ибо всё решается настройкой кодировки. Ниже разберу два варианта решения. ➡️ Первый вариант В первом случае нам нужно изменить язык программ во всей системе. Порядок действий: 1. Через "Пуск" заходим в "Панель управления"; 2. Выбираем "Региональные стандарты"; 3. В открывшемся окне жмём на вкладку "Дополнительно"; 4. Кликаем кнопку "Изменить язык системы"; 5. Далее уже дело логики. Из списка выбираем "Русский (Россия)" и сохраняем результат. Однако, вероятнее всего, на нашем личном ПК всё это уже настроено, а поэтому такой подход будет более уместен в случаях, когда мы льём софт на чистый сервер. Возможно и такое, что эти настройки уже установлены, а вывод в консоль всё равно остаётся нечитаемым, либо же вообще с пустыми строками. Тогда нам нужно перейти ко второму варианту. ➡️ Второй вариант Здесь же решение заключается в настройке ввода/вывода внутри приложения. Для этого нам нужно поиграться со свойствами класса Console. В большинстве случаев, проблему решит следующий код:
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
Либо, если не помогло, то задать свойствам Encoding.Unicode. Всё методом проб и ошибок.
Главное помнить, что задать настройки консоли нужно в самом начале, а значит полный код будет выглядеть так:
Console.InputEncoding = Encoding.Unicode;
Console.OutputEncoding = Encoding.Unicode;
string outRu, outEn;
outRu = "Русский вывод (RU). Ввод: ";
outEn = "English output (EN). Input: ";
Console.Write(outRu);
Console.ReadLine();
Console.Write(outEn);
Console.ReadLine();
#Полезно #КонсольРешение задачи к посту.
void KeywordExtractor(string text)
{
if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException();
Dictionary<string, int> dict = new();
var words = ToWords(text);
foreach (string word in words)
{
if (string.IsNullOrWhiteSpace(word)) continue;
int quantity = dict.ContainsKey(word) ? dict[word] + 1 : 1;
dict[word] = quantity;
}
OutputTop10(dict);
}
string[] ToWords(string text)
{
string words = text.ToLower();
foreach (char sym in text) if (!char.IsLetter(sym)) words = words.Replace(sym, ' ');
return words.Split(' ');
}
void OutputTop10(Dictionary<string, int> words)
{
var top10 = words
.OrderByDescending(x => x.Value)
.Take(10)
.ToArray();
for (int i = 0; i < top10.Length; i++) Console.WriteLine($"{i + 1}. \"{top10[i].Key}\" — {top10[i].Value}");
}
#Задача #Решение #Полезно🖥 Задача: ключевые слова (⭐️)
Написать метод, который принимает текст, анализирует его, выделяя все слова, подсчитывая частоту их встречаемости и выводит список наиболее часто встречающихся слов (топ 10). Ниже приведены примеры.
string text = File.ReadAllText("Text.txt");
KeywordExtractor(text);
// Начинка Text.txt:
/*
У лукоморья дуб зелёный;
Златая цепь на дубе том:
И днём и ночью кот учёный
Всё ходит по цепи кругом;
Идёт направо — песнь заводит,
Налево — сказку говорит.
Там чудеса: там леший бродит,
Русалка на ветвях сидит;
Там на неведомых дорожках
Следы невиданных зверей;
Избушка там на курьих ножках
Стоит без окон, без дверей;
Там лес и дол видений полны;
Там о заре прихлынут волны
На брег песчаный и пустой,
И тридцать витязей прекрасных
Чредой из вод выходят ясных,
И с ними дядька их морской;
Там королевич мимоходом
Пленяет грозного царя;
Там в облаках перед народом
Через леса, через моря
Колдун несёт богатыря;
В темнице там царевна тужит,
А бурый волк ей верно служит;
Там ступа с Бабою Ягой
Идёт, бредёт сама собой,
Там царь Кащей над златом чахнет;
Там русский дух… там Русью пахнет!
И там я был, и мёд я пил;
У моря видел дуб зелёный;
Под ним сидел, и кот учёный
Свои мне сказки говорил.
*/
👉 1. "там" - 14
👉 2. "и" - 9
👉 3. "на" - 5
👉 4. "у" - 2
👉 5. "дуб" - 2
👉 6. "зелёный" - 2
👉 7. "кот" - 2
👉 8. "учёный" - 2
👉 9. "идёт" - 2
👉 10. "без" - 2
string text = File.ReadAllText("Text.txt");
KeywordExtractor(text);
// Начинка Text.txt:
/*
По мере расследования Тедди обнаруживает, что больница на острове скрывает большое количество тайн и ужасов. Маршал находит сбежавшую Рейчел Соландо, нашедшую пристанище в пещере. Подозрительным кажется и главный врач больницы — Джон Коули, с которого и начинается кульминация действия. Именно с подачи доктора Тедди осознаёт, что всё происходящее на острове — продукт воображения, а сам он является заключённым психиатрической больницы Эшклиффа в качестве пациента.
*/
👉 1. "и" - 3
👉 2. "тедди" - 2
👉 3. "что" - 2
👉 4. "на" - 2
👉 5. "острове" - 2
👉 6. "в" - 2
👉 7. "больницы" - 2
👉 8. "с" - 2
👉 9. "по" - 1
👉 10. "мере" - 1
string text = File.ReadAllText("Text.txt");
KeywordExtractor(text);
// Начинка Text.txt:
/*
Теперь нам нужен класс, который и реализует валидацию нашего пароля. Я предпочитаю извлекать методы валидации в отдельные объекты, что позволяет более гибко тестировать фактические типы данных. Дополнительно класс-валидации мы можем использовать как временный объект с некоторой доп. инфой — оценка сложности пароля (слабый/средний/хороший) и т. п. —, без нужды вечного хранения такой информации.
*/
👉 1. "класс" - 2
👉 2. "и" - 2
👉 3. "пароля" - 2
👉 4. "валидации" - 2
👉 5. "теперь" - 1
👉 6. "нам" - 1
👉 7. "нужен" - 1
👉 8. "который" - 1
👉 9. "реализует" - 1
👉 10. "валидацию" - 1
Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.
#Задача #Lvl1Новичок: боится спугнуть клиента.
Тем временем я:
#Юмор
🖥 Из примера выше, что будет выведено на экран?
Решение задачи к посту.
void KaprekarConstant(int number)
{
if (number < 100 || number > 9999) throw new ArgumentOutOfRangeException(nameof(number));
int count = number < 1000 ? 3 : 4;
int constant = count == 3 ? 495 : 6174;
int[] digits = new int[count];
while (number != constant)
{
ExtractDigits(number, digits);
int ascending = AscendingValue(digits);
int descending = DescendingValue(digits);
number = descending - ascending;
Console.WriteLine($"{descending} - {ascending} = {number}");
}
}
void ExtractDigits(int from, int[] to)
{
for (int i = 0; i < to.Length; i++)
{
from = Math.DivRem(from, 10, out int rem);
to[i] = rem;
}
for (int i = 1; i < to.Length; i++) if (to[i - 1] != to[i]) return;
throw new ArgumentException();
}
int AscendingValue(int[] from) => from.OrderBy(x => x).Aggregate((x, y) => x * 10 + y);
int DescendingValue(int[] from) => from.OrderByDescending(x => x).Aggregate((x, y) => x * 10 + y);
#Задача #Решение #Полезно🖥 Задача: постоянная Капрекара (⭐️⭐️)
Написать метод, который принимает число, реализует алгоритм нахождения постоянной Капрекара и выводит итерации на экран. Метод должен уметь работать с трёхзначными и четырёхзначными числами. Ниже приведены примеры.
KaprekarConstant(5621);
👉 6521 - 1256 = 5265
👉 6552 - 2556 = 3996
👉 9963 - 3699 = 6264
👉 6642 - 2466 = 4176
👉 7641 - 1467 = 6174
KaprekarConstant(796);
👉 976 - 679 = 297
👉 972 - 279 = 693
👉 963 - 369 = 594
👉 954 - 459 = 495
KaprekarConstant(3412);
👉 4321 - 1234 = 3087
👉 8730 - 378 = 8352
👉 8532 - 2358 = 6174
KaprekarConstant(6389);
👉 9863 - 3689 = 6174
KaprekarConstant(123);
👉 321 - 123 = 198
👉 981 - 189 = 792
👉 972 - 279 = 693
👉 963 - 369 = 594
👉 954 - 459 = 495
Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.
#Задача #Lvl2🖥 Валидация объектов
Валидация — это процесс проверки данных различных типов по критериям корректности и полезности для конкретного применения.
Смысл валидации в том, что мы совершаем проверку типа до его использования в дальнейшем. Она позволяет исключить поступление на вход заведомо ошибочных, неполных или неточных данных, которые могут привести к ошибочным результатам работы, утрате данных и сбоям в работе систем.
Фреймворк .NET предлагает удобный функционал в виде атрибутов из пространства имён System.ComponentModel.DataAnnotations. Я же люблю ручную самовалидацию, что мы и разберём ниже.
➡️ Разбор на примере
Рассмотрим крайне популярный процесс валидации на сегодняшний день — это корректность создаваемого пароля.
Сперва напишем интерфейс для валидации объектов. Я предпочитаю использовать сразу две версии, классическую и с обобщением, что расширит его гибкость:
public interface IValidatable
{
bool IsValid(object context);
}
public interface IValidatable<Tin, TOut> : IValidatable
{
TOut Validate(Tin context);
}
Теперь вспомним о современных требованиях к паролю. По классике — это 8+ символов, минимум 1 цифра, минимум 1 заглавная буква и минимум 1 специальный символ. В связи с этим мы можем реализовать структуру с обязательными требованиями к паролю:
public struct PasswordRequirements
{
public int MinLength { get; }
public int MinUppercase { get; }
public int MinDigits { get; }
public int MinSpecialChars { get; }
public PasswordRequirements() : this(8, 1, 1, 1) { }
public PasswordRequirements(int minLength, int minUppercase, int minDigits, int minSpecialChars)
{
MinLength = minLength;
MinUppercase = minUppercase;
MinDigits = minDigits;
MinSpecialChars = minSpecialChars;
}
}
Теперь нам нужен класс, который и реализует валидацию нашего пароля. Я предпочитаю извлекать методы валидации в отдельные объекты, что позволяет более гибко тестировать фактические типы данных. Дополнительно класс-валидации мы можем использовать как временный объект с некоторой доп. инфой — оценка сложности пароля (слабый/средний/хороший) и т. п. —, без нужды вечного хранения такой информации. Напишем же такой класс:
public class PasswordValidation : IValidatable<string, bool>
{
public static PasswordRequirements Requirements { get; }
// Здесь можно добавить свойство
// оценки сложности пароля
static PasswordValidation() => Requirements = new();
public PasswordValidation() { }
public bool IsValid(object password) => Validate(password?.ToString());
public bool Validate(string password)
{
if (string.IsNullOrWhiteSpace(password) || password.Length < Requirements.MinLength) return false;
int uppercases = 0, digits = 0, specialChars = 0;
foreach (char c in password)
{
if (char.IsUpper(c)) uppercases++;
else if (char.IsDigit(c)) digits++;
else if (!char.IsLetterOrDigit(c)) specialChars++;
}
return uppercases >= Requirements.MinUppercase && digits >= Requirements.MinDigits && specialChars >= Requirements.MinSpecialChars;
}
}
Поскольку требования к паролю у нас для всех юзеров одинаковы свойство Requirements у меня статическое.
Финалим наш пример фактическим типом пароля:
public class Password
{
public string Hash { get; }
static Password() => validator = new();
public Password(string value)
{
if (!validator.Validate(value)) throw new FormatException();
// Hash = ComputeMd5Hash(value);
// ...
}
private static readonly PasswordValidation validator;
}
Таким образом пароль будет проверяться сразу при инициализации и выбрасывать исключение при несоответствии требований:
Password p1 = new("qwerty"); // Ошибка
Password p2 = new("123qwerty"); // Ошибка
Password p3 = new("Qwerty123@"); // Успех
Валидацию пароля можно проверять и до инициализации фактического класса, ибо классы независимы друг от друга — это одна из гибкостей, о которой я писал выше.
#Полезно #Пароль #Generics🖥 Из примера выше, что более вероятно будет выведено на экран?
🖥 Выражение stackalloc
Ранее я уже немного писал о stackalloc, однако сейчас хочу разобрать более детально этот вопрос.
Ключевое слово позволяет выделить память под массив в стеке. Смысл выделения памяти в стеке — это повышение быстродействия кода. Выделенный стеком блок памяти автоматически удаляется при выходе из этого метода, поэтому он не подлежит сборке мусора.
➡️ Убедимся в этом
Конструкторы ссылочных типов, используя оператор new, выделяют память в куче, а значит мы можем убедиться в том, что stackalloc не занимает память в куче.
Напишем класс:
[MemoryDiagnoser]
public class MemoryBenchmark
{
public MemoryBenchmark() { }
[Benchmark]
public void HeapArray()
{
decimal[] arr = new decimal[100];
for (int i = 0; i < 100; i++) arr[i] = 123;
}
[Benchmark]
public unsafe void StackArray()
{
decimal* arr = stackalloc decimal[100];
for (int i = 0; i < 100; i++) arr[i] = 123;
}
}
Метод HeapArray создаёт массив в привычном нам виде, а метод StackArray создаёт массив через аллокацию памяти в стеке. Для бенчмарка мы помечаем класс и методы соответствующими атрибутами.
Специально для примера берём самый объёмный тип decimal, который занимает 16 байт и 100 его элементов. Из этого следует, что мы ожидаем ~1600 занятых байтов в памяти (16 * 100). Запустим тест:
BenchmarkRunner.Run<MemoryBenchmark>();
Дождавшись результатов, я получил следующий отчёт:
| Method | Mean | Error | StdDev | Gen0 | Allocated | |----------- |---------:|--------:|---------:|-------:|----------:| | HeapArray | 322.1 ns | 6.39 ns | 12.00 ns | 0.7763 | 1624 B | | StackArray | 228.7 ns | 3.15 ns | 2.95 ns | - | - |Столбец "Allocated" сообщает, что метод HeapArray занял 1624 байта, что мы и ожидали. Метод же StackArray ни аллоцировал и байта, что свидетельствует поставленной задаче. А теперь посмотрим на столбец "Mean": 322.1 наносекунд против 228.7 — разница в скорости заметная. ➡️ Некоторая особенность При выделении слишком большого объёма памяти в стеке возникает исключение StackOverflowException. Рекомендуется ограничить объём памяти, выделяемый под стек, например, как в следующем коде:
int length = 1000;
Span<byte> buffer = length <= 1024 ? stackalloc byte[length] : new byte[length];
Поскольку объём доступной памяти на стеке зависит от среды, в которой выполняется код, при определении фактического предельного значения следует использовать консервативное значение.
➡️ Проверим очистку стека
Хоть это и ни к чему, но давайте убедимся в очистке стека после выхода из метода. Напишем и запустим следующий код:
var t = new Test();
t.Print();
public unsafe class Test
{
public Test()
{
int* ptr = stackalloc int[length];
pointer = ptr;
for (int i = 0; i < length; i++) ptr[i] = i + 1;
Print();
}
private const int length = 5;
private int* pointer;
public void Print()
{
for (int i = 0; i < length; i++) Console.WriteLine(pointer[i]);
Console.WriteLine("------------");
}
}
Вывод:
1 2 3 4 5 ------------ 1 0 2100389455 2046 5 ------------При первом выводе массива на экран всё работает как и ожидается, ибо вызов метода Print происходит внутри конструктора, где была выделена память. После выхода из конструктора и вызова метода Print во второй раз блок памяти уже заполнен иначе, а значит стек уже не хранит наш массив и дальнейшая попытка работы с ним небезопасна. #Полезно #Array #Unsafe #Pointers #Benchmark
