C#Hive: Projects & Progress | Программирование
2K subscribers
153 photos
17 videos
1 file
143 links
Сообщество единомышленников C#: решаем задачи, учимся, развиваемся и общаемся вместе. Советы по работе на фрилансе, готовые проекты, код ревью, рекомендации и исследования.

Вопросы/сотрудничество: @tel_phil9
Download Telegram
🖥 Массовое преобразование типов массива

Вот несколько примеров, демонстрирующих различные способы массового преобразования типов массива:

1. Использование Array.ConvertAll:
int[] numbers = { 1, 2, 3 };
string[] strings = Array.ConvertAll(numbers, x => x.ToString());

2. Использование LINQ:
int[] numbers = { 1, 2, 3 };
string[] strings = numbers.Select(x => x.ToString()).ToArray();

3. Использование цикла:
int[] numbers = { 1, 2, 3 };
string[] strings = new string[numbers.Length];
for (int i = 0; i < numbers.Length; i++)
{
strings[i] = numbers[i].ToString();
}

При массовом преобразовании типов массива возможна потеря данных, если целевой тип не может точно представить исходные значения. Например:
double[] sourceArray = { 1.23, 4.56, 7.89 };
int[] targetArray = Array.ConvertAll(sourceArray, x => (int)x);

В этом случае, десятичные части исходных значений (*.23, *.56, *.89) будут потеряны при преобразовании в целые числа, результат будет записан только в виде целых чисел (1, 4, 7).

#Полезно #LINQ #Array
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 Сравнение строк из коллекции без учёта регистра

Когда-нибудь приходилось делать поиск по строковому ключу в Dictionary? Проблема со строковыми ключами — это непредсказуемость регистра (vip вместо VIP и т.п.).

Да, решить вопрос можно по-разному, например через string.ToLower. Однако это можно считать моветоном. Хорошим решением данного вопроса является передача параметра StringComparer.CurrentCultureIgnoreCase в конструктор Dictionary.

По аналогии со словарём, использовать данное решение можно и в случае с HashSet. Пример реализации:
HashSet<string> statuses = new HashSet<string>(StringComparer.CurrentCultureIgnoreCase)
{
"VIP",
"Premium"
};

Console.WriteLine(statuses.Contains("VIP")); // true
Console.WriteLine(statuses.Contains("vip")); // true


#Полезно #StringComparer #Dictionary #HashSet #Array
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 LINQ: метод Zip для объединения двух коллекций с применяемой функцией к их элементам

Представим, что у нас есть два списка:
List<int> list1 = new List<int> { 1, 2, 3 };
List<int> list2 = new List<int> { 4, 5, 6 };


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

Пример примитивного решения поставленной задачи "в лоб":
var sum = new List<int>();
for (int i = 0; i < list1.Count; i++) sum.Add(list1[i] + list2[i]);


Теперь задача решена и всё хорошо. Однако LINQ обладает замечательным методом Zip, который упакует две наши коллекции в единую, и применит указанную функцию к перечисляемым элементам.

Следующим образом выглядело бы красивое решение:
List<int> list1 = new List<int> { 1, 2, 3 };
List<int> list2 = new List<int> { 4, 5, 6 };

var sum = list1.Zip(list2, (x, y) => x + y).ToList();
// Результат списка sum:
// sum { 5, 7, 9 }


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

#Полезно #LINQ #Array
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 LINQ: метод GroupBy и оператор group by для группировки данных

Имея коллекцию определённых типов нередко возникает необходимость группировать данные. Будь это для удобной выборки и будущих манипуляций или просто для читабельности — это неважно.

Представим, что у нас есть следующий массив:
Person[] people =
{
new Person("Tom", "Microsoft"), new Person("Sam", "Google"),
new Person("Bob", "JetBrains"), new Person("Mike", "Microsoft"),
new Person("Kate", "JetBrains"), new Person("Alice", "Microsoft"),
};

class Person
{
public string Name { get; set; }
public string Company { get; set; }

public Person(string name, string company)
{
Name = name;
Company = company;
}
}


Как мы могли бы сгруппировать людей по компаниям:
// Юзабилити оператора
var companies = from person in people
group person by person.Company;

// Юзабилити метода
var companies = people.GroupBy(x => x.Company);

foreach (var company in companies)
{
Console.WriteLine("\t" + company.Key);
foreach (var person in company) Console.WriteLine(person.Name);
Console.WriteLine(); // для разделения
}


В примере выше используется оператор group by и метод GroupBy, их функциональность идентична. Сама группировка идёт по свойству Company. Каждая группа имеет ключ, который можно получить через свойство Key. Здесь это будет название компании.

Результат вывода будет следующим:
        Microsoft
Tom
Mike
Alice

Google
Sam

JetBrains
Bob
Kate


#Полезно #LINQ #Array
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 LINQ: метод Union

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

Пример использования:
string[] teachers = { "Анастасия", "Артур", "Лев" };
string[] students = { "Илья", "Лев", "Анастасия" };

var names = students.Union(teachers);
foreach (var name in names) Console.WriteLine(name);

// Содержание коллекции names:
// Илья
// Лев
// Анастасия
// Артур


Последовательное применение методов Concat() и Distinct() будет подобно действию рассматриваемого метода.

➡️ Работа со сложными объектами
Для сравнения объектов в последовательностях применяются реализации методов GetHashCode() и Equals(). Поэтому, если мы хотим работать с последовательностями, которые содержат объекты своих классов/структур, то нам необходимо переопределить для них данные методы.

Пример реализации:
Person[] teachers = { new Person("Анастасия"), new Person("Артур"), new Person("Лев") };
Person[] students = { new Person("Илья"), new Person("Лев"), new Person("Анастасия") };

var persones = students.Union(teachers);
foreach (var p in persones) Console.WriteLine(p.Name);

class Person
{
public string Name { get; }
public Person(string name) => Name = name;

public override int GetHashCode() => Name.GetHashCode();

public override bool Equals(object obj)
{
if (obj is Person person) return person.Name == Name;
return false;
}
}


Здесь класс Person при сравнении акцентирует внимание с ключевым свойством, поэтому результаты выборки будут идентичны предыдущему примеру.

#Полезно #LINQ #Array
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 LINQ: методы Except и Intersect

LINQ предоставляет несколько методов для работы с коллекциями как с множествами. Рассмотрим возможность находить их разность и пересечение. В примерах будут использоваться два массива:
string[] conglomerates = { "Amazon", "Яндекс", "Google" };
string[] companies = { "Мираторг", "Яндекс", "2GIS" };


➡️ Разность последовательностей
Метод Except() возвращает разность двух коллекций, то есть те элементы, которые находятся только в первой коллекции.

Пример использования:
var res = conglomerates.Except(companies);
foreach (var d in res) Console.WriteLine(d);

// Результат выборки:
// Amazon
// Google


В данном случае из массива conglomerates убираются все элементы, которые есть в массиве companies. Результатом операции будут два элемента.

➡️ Пересечение последовательностей
Метод Intersect() возвращает пересечение двух коллекций, то есть те элементы, которые встречаются в обоих коллекциях.

Пример использования:
var res = conglomerates.Intersect(companies);
foreach (var d in res) Console.WriteLine(d);

// Результат выборки:
// Яндекс


Так как оба набора имеют только один общий элемент, то, соответственно, только он и попадёт в результирующую выборку.

#Полезно #LINQ #Array
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 Потокобезопасная коллекция

Общие коллекции (и ресурсы в целом) для нескольких потоков, например List<T>, должны быть потокобезопасными. Самый простой способ сделать его таким — это блокировать доступ с помощью lock.

Ниже предоставляю примитивный пример потокобезопасной коллекции ConcurrentList на базе ReaderWriterLockSlim, которая отсутствует в .NET. Сама коллекция является обёрткой над List<T>, которую можно будет использовать в своих проектах как общий ресурс для нескольких потоков.

public class ConcurrentList<T> : IEnumerable<T>, ICollection<T>
{
public bool IsReadOnly => false;
public int Count
{
get
{
try
{
locker.EnterReadLock();
return list.Count;
}
finally { locker.ExitReadLock(); }
}
}

public ConcurrentList() : this(null) { }
public ConcurrentList(IEnumerable<T> items)
{
list = items is null ? new List<T>() : new List<T>(items);
locker = new ReaderWriterLockSlim();
}

private readonly List<T> list;
private readonly ReaderWriterLockSlim locker;

public void Add(T item)
{
try
{
locker.EnterWriteLock();
list.Add(item);
}
finally { locker.ExitWriteLock(); }
}

public void Clear()
{
try
{
locker.EnterWriteLock();
list.Clear();
}
finally { locker.ExitWriteLock(); }
}

public bool Contains(T item)
{
try
{
locker.EnterReadLock();
return list.Contains(item);
}
finally { locker.ExitReadLock(); }
}

public void CopyTo(T[] array, int arrayIndex)
{
try
{
locker.EnterReadLock();
list.CopyTo(array, arrayIndex);
}
finally { locker.ExitReadLock(); }
}

public bool Remove(T item)
{
try
{
locker.EnterWriteLock();
return list.Remove(item);
}
finally { locker.ExitWriteLock(); }
}

private IEnumerable<T> Enumerate()
{
try
{
locker.EnterReadLock();
foreach (T item in list) yield return item;
}
finally { locker.ExitReadLock(); }
}

public IEnumerator<T> GetEnumerator() => Enumerate().GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => Enumerate().GetEnumerator();
}


➡️ А как же убедиться?
Проверим корректность на следующем примере ниже и по результату убедимся, что коллекция заполнена ровно на 100к строк, как мы и ожидаем: 200 потоков, каждый заполняет коллекцию на 500 строк = 200 * 500 = 100к.

ConcurrentList<string> list = new ConcurrentList<string>();
List<Thread> threads = new List<Thread>();

for (int i = 0; i < 200; i++)
{
Thread thread = new Thread(Proc);
thread.Start();
threads.Add(thread);
}

foreach (var t in threads) t.Join();
Console.WriteLine(list.Count);

void Proc()
{
for (int i = 0; i < 500; i++) list.Add(Guid.NewGuid().ToString());
}


#Полезно #Array #Многопоточность
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 NaturalSort.Extension: естественная сортировка строк

Данная библиотека является расширением метода для StringComparison и IComparer<string>. Имеет поддержку естественной сортировки, когда мы, например, ожидаем порядок "abc1, abc2, abc10" вместо "abc1, abc10, abc2".

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

Пример использования:
using NaturalSort.Extension;

var arr = new string[] { "a1.txt", "a2.txt", "a10.txt", "a7.txt", "b.txt", "b55.txt", "a13.txt" };
Array.Sort(arr, StringComparer.OrdinalIgnoreCase.WithNaturalSort());

// arr { "a1.txt", "a2.txt", "a7.txt", "a10.txt", "a13.txt", "b.txt", "b55.txt" }


Полная документация библиотеки.

#Полезно #NaturalSort_Extension #StringComparer #Array
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 ReadOnlyCollection не является неизменяемой

Коллекция «только для чтения» не означает, что она неизменяема. Сегодня посмотрим, как можно её изменить.

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

Приведу пример, результат которого будет довольно очевиден:
var numbers = new List<int> { 1, 2 };
var asReadOnly = numbers.AsReadOnly();
var readOnly = new ReadOnlyCollection<int>(numbers);

Console.WriteLine($"List: {numbers.Count}");
Console.WriteLine($"AsReadOnly: {asReadOnly.Count}");
Console.WriteLine($"ReadOnly: {readOnly.Count}");

// Очевидный вывод:
// List: 2
// AsReadOnly: 2
// ReadOnly: 2


Но что, если мы добавим элемент в исходный список?
numbers.Add(3);


Теперь вывод в консоль будет следующим:
List: 3
AsReadOnly: 3
ReadOnly: 3


Как и представление в базе данных, наше представление списка «только для чтения» обновляется. Это плохо? Нет, потому что имеет некоторые преимущества. Самое главное — базовая операция по созданию ReadOnlyCollection очень бюджетная. Она занимает O(1) времени, поскольку аллокация памяти не требуется.

➡️ Как достичь реальной неизменяемости?
Чтобы иметь реальную неизменяемость (immutable), нужно использовать другие типы, например, ImmutableList.

Если мы проведём тот же тест, что и выше, мы получим то, что ожидаем от неизменяемого типа:
var numbers = new List<int> { 1, 2 };
var immutable = numbers.ToImmutableList();

Console.WriteLine($"List: {numbers.Count}");
Console.WriteLine($"Immutable: {immutable.Count}");

numbers.Add(3);

Console.WriteLine($"List: {numbers.Count}");
Console.WriteLine($"Immutable: {immutable.Count}");

// Вывод:
// List: 2
// Immutable: 2
// List: 3
// Immutable: 2


Мы видим, что длина остаётся прежней. Недостаток в том, что нам нужно скопировать все элементы из исходного списка в неизменяемый список за O(n).

➡️ Итог
ReadOnlyCollection не является неизменяемым. Это просто представление коллекции, доступное только для чтения. Вы, как потребитель, не можете их изменить, но это не значит, что изначальный создатель/владелец списка не может этого сделать. Если вам действительно нужно текущее состояние, которое не может измениться, используйте ImmutableArray или ImmutableList.

#Полезно #Immutable #ReadOnlyCollection #Array
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 Span<T>: что это такое?

Тип Span представляет непрерывную область памяти. Цель данного типа — повысить производительность и эффективность использования памяти. Span позволяет избежать дополнительных выделений памяти при операции с наборами данных. Поскольку Span является структурой, то объект этого типа располагается в стеке, а не в куче.

➡️ Инициализация Span
Для создания объекта можно использовать конструктор:
string[] person = { "Roman", "Alex", "Элла" };
Span<string> personSpan = new(person);


Также можно непосредственно присвоить массив, и он будет неявно преобразован в Span:
string[] person = { "Roman", "Alex", "Элла" };
Span<string> personSpan = person;


В обоих случаях, Span будет хранить ссылки на три строки. Далее мы можем получать, устанавливать или перебирать данные также, как в массиве:
string[] person = { "Roman", "Alex", "Элла" };
Span<string> personSpan = person;

personSpan[1] = "Drake";
Console.WriteLine(person[1]); // Drake
Console.WriteLine(personSpan[1]); // Drake

foreach (var p in personSpan)
{
Console.WriteLine(p);
}


➡️ Ограничения и преимущество
Чтобы понять «где» и «когда» нам может пригодиться данный тип, нужно рассмотреть его на примере. Допустим, есть массив, хранящий количество сделок за три недели и нам нужно получить из него два набора — количество сделок за первую и последнюю неделю. Используя массивы, мы бы могли сделать так:
int[] ordersAmount =
{
3, 1, 5, 7, 4, 4, 0,
1, 0, 4, 11, 8, 9, 5,
9, 14, 7, 4, 8, 22, 3
};

int[] firstWeek = new int[7]; // выделяем память для первой недели
int[] lastWeek = new int[7]; // выделяем память для третьей недели

Array.Copy(ordersAmount, 0, firstWeek, 0, 7); // копируем данные в первый массив
Array.Copy(ordersAmount, 14, lastWeek, 0, 7); // копируем данные во второй массив


Для обоих массивов мы вынуждены выделить память, хотя оба массива, по сути, содержат те же данные, что и ordersAmount, но в отдельных частях памяти. Span позволяет работать с памятью более эффективно и избежать ненужных выделений памяти. Так, используем его вместо массивов:
int[] ordersAmount =
{
3, 1, 5, 7, 4, 4, 0,
1, 0, 4, 11, 8, 9, 5,
9, 14, 7, 4, 8, 22, 3
};

Span<int> spanOrdersAmount = ordersAmount;
Span<int> firstWeek = spanOrdersAmount.Slice(0, 7); // нет выделения памяти под данные
Span<int> lastWeek = spanOrdersAmount.Slice(14, 7); // нет выделения памяти под данные


Span имеет ряд ограничений:
Не может быть присвоена переменной типа Object, dynamic или переменной типа интерфейса;
Не может быть полем в объекте ссылочного типа (а только внутри ref-структур);
Не может использоваться в пределах операций await или yield.

➡️ ReadOnlySpan
Структура ReadOnlySpan аналогична Span, только предназначена для неизменяемых данных. Например:
string text = "hello, world";

string worldString = text.Substring(7, 5); // есть выделение памяти под символы
ReadOnlySpan<char> worldSpan = text.AsSpan().Slice(7, 5); // нет выделения памяти под символы

//worldSpan[0] = 'a'; // Нельзя изменить
Console.WriteLine(worldSpan[0]); // выводим первый символ


С помощью метода AsSpan() преобразуем строку в объект ReadOnlySpan<char> и затем выделяем из него диапазон символов. Поскольку ReadOnlySpan предназначен только для чтения, то, соответственно, мы не можем изменить через него данные, но можем получить. В остальном работа с ReadOnlySpan аналогична Span`у.

#Полезно #Span #ReadOnlySpan #Array
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 Parallel LINQ: метод AsParallel

Мы знаем, что по умолчанию все элементы коллекции в LINQ обрабатываются последовательно. Можем убедиться в этом на примере вычисления количества чётных чисел:
var values = Enumerable.Range(1, 10);

var countOfEven = values.Count(x =>
{
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Проверка {x}");
return (x & 1) == 0;
});


Повторяя данную процедуру сколь угодно раз, вывод в консоль всегда будет статичным:
[1] Проверка 1
[1] Проверка 2
[1] Проверка 3
[1] Проверка 4
[1] Проверка 5
[1] Проверка 6
[1] Проверка 7
[1] Проверка 8
[1] Проверка 9
[1] Проверка 10


Взаимодействие с элементами происходит последовательно, как изначально и была заполнена коллекция. А идентификатор потока во всех случаях одинаков, что означает однопоточную работу с запросом.

Однако существует класс ParallelEnumerable, который инкапсулирует функциональность Parallel LINQ (часто его ещё называют PLINQ) и позволяет выполнять обращения к коллекции в параллельном режиме.

При обработке коллекции PLINQ использует возможности всех процессоров в системе. Источник данных разделяется на сегменты и каждый обрабатывается в отдельном потоке. Это позволяет произвести запрос на многоядерных машинах намного быстрее.

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

➡️ Метод AsParallel
Метод позволяет распараллелить запрос к источнику данных. Он реализован как метод расширения LINQ у массивов и коллекций. При вызове данного метода источник данных разделяется на части (если это возможно) и над каждой частью отдельно производятся операции.

Вернёмся к примеру выше и преобразуем запрос соответственно:
var countOfEven = values.AsParallel().Count(x =>
{
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Проверка {x}");
return (x & 1) == 0;
});


Теперь же вывод будет непредсказуем с каждым разом, хаотичным и с разными потоками:
[9] Проверка 2
[1] Проверка 3
[1] Проверка 6
[6] Проверка 1
[9] Проверка 4
[10] Проверка 5
[1] Проверка 7
[6] Проверка 8
[9] Проверка 9
[10] Проверка 10


Смысл применения PLINQ имеется преимущественно на больших коллекциях или при сложных операциях, где действительно выгода от распараллеливания запросов может перекрыть возникающие при этом издержки. Поэтому было бы более уместно, если бы наша коллекция была big data:
int countOfEven =
Enumerable.Range(1, 1_000_000)
.AsParallel()
.Count(x => (x & 1) == 0);


#Полезно #LINQ #Array #Многопоточность
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 Span и многомерные массивы

Мы уже знаем, что из себя представляет структура Span, позволяющая эффективно работать с памятью. Однако явным образом структура принимает только одномерные массивы.

Если мы попробуем написать следующее для двухмерного массива, то результат, который мы ожидаем, получен не будет:
int[,] array = { };
Span<int> span1 = array; // Ошибка компиляции
Span<int> span2 = array.AsSpan(); // Ошибка компиляции


Однако один из перегруженных конструкторов структуры Span имеет возможность указания по указателю, благодаря чему мы можем написать следующий метод:
unsafe Span<int> AsSpan(int[,] matrix)
{
fixed (int* p = matrix) return new Span<int>(p, matrix.Length);
}


Таким подходом двухмерный массив, как и любой другой многомерный массив, сможет теперь управляться Span`ом. Используем его:
int[,] arr = { { 1, 2 }, { 3, 4 } };
Console.WriteLine($"{arr[0, 0]} {arr[0, 1]} {arr[1, 0]} {arr[1, 1]}");

Span<int> span = AsSpan(arr);
span[0] = 0;
span[3] = 0;
Console.WriteLine($"{arr[0, 0]} {arr[0, 1]} {arr[1, 0]} {arr[1, 1]}");


Вывод:
1 2 3 4
0 2 3 0


Минус такого подхода в том, что Span работает с непрерывной областью памяти, а это может быть не очень удобно при попытке выделить определённые элементы, ведь они в Span будут предоставлены последовательно:
int[,] arr = { { 55, 12 }, { 97, 0 } };
Span<int> span = AsSpan(arr); // [55, 12, 97, 0]


Мы можем выделить промежуток элементов с 1 по 2 или с 2 по 4, но выделить только 1 и 4 элемент, без выделения дополнительной памяти, у нас не получится. Тем не менее, Span этим и прекрасен.

➡️ Выделение строки из матрицы
А чтобы из матрицы (двухмерного массива) выделить все элементы N-ой строки, мы можем написать нечто следующее:
unsafe Span<int> AsRowSpan(int[,] matrix, int indexRow)
{
fixed (int* p = matrix)
{
var span = new Span<int>(p, matrix.Length);
int columns = matrix.GetLength(1);
int start = indexRow * columns;

return span.Slice(start, columns);
}
}


Применим:
int[,] array2D = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
Span<int> span = AsRowSpan(array2D, 1); // [4, 5, 6]

span.Fill(-1);
// Теперь array2D = [1, 2, 3, -1, -1, -1, 7, 8, 9]


Либо же использовать существующее решение, приводящее к аналогичному результату:
int[,] array2D = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
Span<int> span = MemoryMarshal.CreateSpan(ref array2D[1, 0], array2D.GetLength(1));


#Полезно #Span #Array #Unsafe #Pointers
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 Отложенное и немедленное выполнение LINQ

Есть два способа выполнения запроса LINQ: отложенное (deferred) и немедленное (immediate) выполнение. При отложенном выполнении выражение LINQ не выполняется, пока не будет произведена итерация или перебор по выборке, например, в цикле foreach. Обычно подобные операции возвращают объект IEnumerable<T> или IOrderedEnumerable<T>.

Полный список отложенных операций LINQ:
AsEnumerable;
Cast;
Concat;
DefaultIfEmpty;
Distinct;
Except;
GroupBy;
GroupJoin;
Intersect;
Join;
OfType;
OrderBy;
OrderByDescending;
Range;
Repeat;
Reverse;
Select;
SelectMany;
Skip;
SkipWhile;
Take;
TakeWhile;
ThenBy;
ThenByDescending;
Union;
Where.

Рассмотрим следующий код:
var numbers = new List<int> { 1, 2, 3, 4, 5 };

var evenNumbers = numbers
.Where(x =>
{
Console.WriteLine($"Проверка {x}");
return x % 2 == 0;
});


То есть фактическое выполнение запроса здесь даже не произойдёт, коллекция evenNumbers не будет вычислена, а консоль будет чистым. Но если же мы добавим следующую строку:
foreach (int num in evenNumbers) Console.WriteLine(num);


То получим следующий вывод:
Проверка 1
Проверка 2
2
Проверка 3
Проверка 4
4
Проверка 5


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

После определения запроса он может выполняться множество раз. И до выполнения запроса источник данных (массив numbers) может изменяться. Наглядно увидеть и убедиться в этом можно по старому опросу.

➡️ Немедленное выполнение запроса
С помощью ряда методов мы можем применить немедленное выполнение запроса. Это методы, которые возвращают либо одно атомарное значение, либо один элемент, либо данные типов Array, List и Dictionary.

Полный список подобных операций LINQ:
Aggregate;
All;
Any;
Average;
Contains;
Count;
ElementAt;
ElementAtOrDefault;
Empty;
First;
FirstOrDefault;
Last;
LastOrDefault;
LongCount;
Max;
Min;
SequenceEqual;
Single;
SingleOrDefault;
Sum;
ToArray;
ToDictionary;
ToList;
ToLookup.

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

Так, например, если мы изменим ранний пример, добавив метод ToArray к запросу:
var numbers = new List<int> { 1, 2, 3, 4, 5 };

var evenNumbers = numbers
.Where(x =>
{
Console.WriteLine($"Проверка {x}");
return x % 2 == 0;
})
.ToArray();


То получим коллекцию evenNumbers уже с результатом вычисления, т.к. сработает немедленное выполнение. А вывод будет следующим:
Проверка 1
Проверка 2
Проверка 3
Проверка 4
Проверка 5


#Полезно #LINQ #Array
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 Фильтр Блума

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

Вопреки такому решению, хранение всех этих данных (IP`шки) для нас может быть лишним. Мы не будем их использовать в дальнейшем, но каждый хранимый объект в памяти будет непременно "раздувать" его общее заполнение. В оптимизации такой задачи нам и поможет фильтр Блума, который мы сегодня рассмотрим на примере поставленной задачи.

➡️ Что это и как он работает
Фильтр Блума проверяет принадлежность конкретного элемента к множеству данных без необходимости его хранения. Он использует массив битов (0 — false, 1 — true), а также несколько хеш-функций. Когда новый объект попадает в базу, его хеш-коды вычисляются, заполняя соответствующие биты массива в значении 1. Например, если хеш-функция возвращает число 50, то 50 бит (он же индекс) массива устанавливается в 1. Так происходит добавление IP-адреса в фильтр.

Если при получении нового IP-адреса все нужные биты уже установлены в значение 1, то это значит, что он уже встречался ранее. Но важно помнить, что фильтр Блума не даёт 100% гарантии, что IP-адрес был уже замечен. Всегда есть небольшой риск ложноположительного результата. Эта вероятность зависит от размера массива битов, количества хеш-функций и количества уникальных IP-адресов.

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

➡️ Снижение вероятности коллизий
Коллизии возникают, когда несколько различных элементов имеют одинаковые хеш-коды, что может привести к ложноположительным результатам при проверке.

Стратегии снижения вероятности коллизий:
Увеличение размера массива битов;
Увеличение количества хеш-функций;
Выбор качественных хеш-функций.

➡️ Простая реализация
public class BloomFilter<T>
{
public BloomFilter(int capacity, int hashFunctionCount)
{
if (capacity < 1) throw new ArgumentOutOfRangeException(nameof(capacity), "Размер должен быть положительным");
if (hashFunctionCount < 1) throw new ArgumentOutOfRangeException(nameof(hashFunctionCount), "Количество хеш-функций должно быть положительным");

bitArray = new(capacity);
hashCount = hashFunctionCount;
}

private readonly BitArray bitArray;
private readonly int hashCount;

public void Add(T item)
{
foreach (int hash in GetHashValues(item))
{
bitArray[hash] = true;
}
}

public bool Contains(T item)
{
foreach (int hash in GetHashValues(item))
{
if (!bitArray[hash]) return false;
}

return true;
}

public void Clear() => bitArray.SetAll(false);

private IEnumerable<int> GetHashValues(T item)
{
for (int i = 0; i < hashCount; i++)
{
int seed = i * 31;
int hash = (item?.GetHashCode() ?? 0) ^ seed;
yield return Math.Abs(hash) % bitArray.Length;
}
}
}


Проверим:
var filter = new BloomFilter<string>(1000, 5);
var apple = "apple";
var banana = "banana";

filter.Add(apple);
filter.Add(banana);
Check(apple);
Check(banana);
Check("date");
Check("grape");

filter.Clear();
Check(apple);
Check(banana);

void Check(string item) => Console.WriteLine(filter.Contains(item));


Вывод:
True
True
False
False
False
False


Отмечу, что реализация сильно простая. Рекомендую заменять GetHashCode на хеш-функцию MurmurHash2, MD5 или др. Ставьте реакции на пост, если хотите посмотреть на лучшую реализацию — напишу в комментариях.

#Полезно #Хеширование #GetHashCode #Array
Please open Telegram to view this post
VIEW IN TELEGRAM
🖥 Выражение 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
Please open Telegram to view this post
VIEW IN TELEGRAM