C#Hive: Projects & Progress | Программирование @csharphive Channel on Telegram

C#Hive: Projects & Progress | Программирование

@csharphive


Сообщество единомышленников C#: решаем задачи, учимся, развиваемся и общаемся вместе. Советы по работе на фрилансе, готовые проекты, код ревью, рекомендации и исследования.

Вопросы/сотрудничество: @tel_phil9

C#Hive: Projects & Progress | Программирование (Russian)

Добро пожаловать в Сообщество C#Hive: Projects & Progress! Наш канал @csharphive создан для всех любителей программирования на языке C#. Здесь вы найдете единомышленников, с которыми можно решать задачи, учиться, развиваться и общаться вместе. Мы делимся советами по работе на фрилансе, предлагаем готовые проекты, проводим код ревью, даем рекомендации и делимся результатами наших исследований. Если у вас есть вопросы или вы хотели бы сотрудничать с нами, не стесняйтесь обратиться к @tel_phil9. Присоединяйтесь к C#Hive: Projects & Progress уже сегодня и станьте частью активного и дружелюбного сообщества разработчиков!

C#Hive: Projects & Progress | Программирование

31 Dec, 04:59


🖥 Атомарные операции, безопасность потоков и состояние гонки

«Атом» происходит от греческого atomos — "неразрезаемый" и использовался в смысле "неделимая наименьшая единица", пока физики не обнаружили, что на самом деле существуют объекты меньших размеров, чем атомы. В параллельном программировании (многопоточная среда) атомарные методы удобны, так как они гарантируют детерминированность, то есть достижение одного и того же результата вне зависимости от того, сколько потоков одновременно пытаются выполнить инструкцию.

Существуют две главные характеристики атомарных методов:
Если один поток исполняется атомарным методом, другой поток не видит промежуточное состояние, когда метод либо не был начат, либо уже был закончен. Тем не менее, не существует промежуточного состояния между началом и концом;
Операция будет успешно завершена или полностью провалена без внесения каких-либо изменений. Это похоже на транзакции баз данных, где все операции успешны или не проведены вовсе при наличии хотя бы одной ошибки.

➡️ Как достичь безопасность потоков
Важной концепцией в среде параллелизма является потоковая безопасность. Метод называется потокобезопасным, если его можно выполнить одновременно в нескольких потоках без возникновения ошибок.

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

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

➡️ Состояния гонки
Состояние гонки возникает, когда несколько потоков используют одну и ту же переменную и пытаются одновременно её изменить. Проблема заключается в том, что в зависимости от порядка проведения потоками операций над переменной её значения будут отличаться. В таком случае даже инкрементация может быть проблематичной, потому что данная операция не атомарна.

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

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

➡️ Подавление проблемы на примере
for (int i = 0; i < 4; i++)
{
int value = 0;
Parallel.For(0, 100_000, _ => value++);
Console.WriteLine($"Actual Result: {value}");
}


В примере выше объявлена переменная, значение которой увеличивается с использованием цикла Parallel.For. Так как этот цикл использует многопоточность, несколько потоков пытаются обновить одну и ту же переменную value. Здесь цикл выполняется 100к раз, а значит значение переменной мы ожидаем в 100000.

Однако по запуску данного кода результат каждый раз (все 4 раза в конструкции for) будет разным:
Actual Result: 100000
Actual Result: 83615
Actual Result: 73592
Actual Result: 89645


Мы можем использовать механизмы синхронизации с заменой исходного примера. Например класс Interlocked, который предоставляет методы Increment, Add, Exchange и т.д:
int value = 0;
Parallel.For(0, 100_000, _ => Interlocked.Increment(ref value));
Console.WriteLine($"Actual Result: {value}");


Либо оператор lock, в блоке которого код будет выполняться только одним потоком за раз:
object sync = new();
int value = 0;

Parallel.For(0, 100_000, _ =>
{
lock (sync)
{
value++;
}
});

Console.WriteLine($"Actual Result: {value}");


Теперь при вызове кода мы всегда будем получать ожидаемые результаты.

#Полезно #Многопоточность

C#Hive: Projects & Progress | Программирование

29 Dec, 04:59


#Опрос

C#Hive: Projects & Progress | Программирование

27 Dec, 04:59


🖥 Аннотация NotNull: гарантируем, что значения не будут пустыми

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

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

Польза от NotNull:
Повышает читаемость кода, т.к. ясно показывает, что обрабатываемый тип не может равняться null, что делает код более понятным и предсказуемым;
Предотвращение ошибок на этапе компиляции, что сокращает время отладки;
Способствует написанию более чистого и безопасного кода.

➡️ Аннотация параметра метода
void Foo([NotNull] string str)
{
// Безопасно, так как str не должен быть null
Console.WriteLine(str.Length);
}


Если мы попытаемся передать null в качестве аргумента, то компилятор выдаст предупреждение на 2 строке:
string line = null;
Foo(line);


➡️ Аннотация возвращаемого значения из метода
[return: NotNull]
string GetName()
{
// Код гарантирует, что метод всегда вернёт не null значение
return "John Doe";
}


➡️ Аннотация в обобщениях
Подобную логику носит ещё и ограничение notnull в обобщениях, которое указывает, что аргумент типа должен быть типом, не допускающим значение null.

В отличие от других ограничений, если аргумент типа нарушает ограничение notnull, компилятор генерирует предупреждение.

Пример обобщения:
class NotNullContainer<T> where T : notnull
{

}


Ловим предупреждение:
NotNullContainer<Thread?> notNullContainer = new();


➡️ Тогда почему мы можем сбилдить явную ошибку?
Как итог, атрибут и ограничение является индикатором намерений, а не механизмом принудительного предотвращения null. Он помогает разработчику и инструментам анализа кода, но сам по себе не гарантирует отсутствие NullReferenceException во время выполнения.

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

#Полезно #Attribute #NotNull #Generics

C#Hive: Projects & Progress | Программирование

25 Dec, 14:59


Решение задачи к посту.

int InterpolationSearch(int[] array, int key)
{
if (array == null) throw new ArgumentNullException();
if (array.Length == 0) throw new ArgumentOutOfRangeException();

return FindIndex(array, key, 0, array.Length - 1);
}

int FindIndex(int[] array, int key, int leftBorder, int rightBorder)
{
int leftValue = array[leftBorder];
if (leftValue == key) return leftBorder;

int rightValue = array[rightBorder];
if (rightValue == key) return rightBorder;

if (leftValue > key || rightValue < key) return -1;

int index = leftBorder + (key - leftValue) * (rightBorder - leftBorder) / (rightValue - leftValue);
int currentValue = array[index];

return currentValue == key
? index
: currentValue < key
? FindIndex(array, key, index + 1, rightBorder)
: FindIndex(array, key, leftBorder, index - 1);
}


#Задача #Решение #Полезно #Рекурсии

C#Hive: Projects & Progress | Программирование

25 Dec, 04:59


🖥 Задача: интерполяционный поиск (⭐️⭐️)

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

int[] array = { 1, 2, 4, 6, 7, 89, 123, 231, 1000, 1235 };
InterpolationSearch(array, key: 123);

👉 6

int[] array = { 19, 44, 98, 102, 256, 321, 322, 404, 666, 809, 821, 911, 1000 };
InterpolationSearch(array, 911);

👉 11

int[] array = { 37079, 174110, 313442, 523486, 584060, 639340, 664540, 695685, 741422, 764826 };
InterpolationSearch(array, 584060);

👉 4

int[] array = { 1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 6, 6, 7, 7, 9, 9 };
InterpolationSearch(array, 8);

👉 -1

int[] array = { 2225, 5788, 9802, 10635, 13442, 16229, 16877, 18325, 19957, 21697, 22683, 28405, 33063, 33149, 39354, 42490, 53428, 55768, 61340, 62882, 64584, 66912, 71217, 71533, 78770, 79933, 79941, 81720, 85425, 91115, 91223, 97113, 99143 };
InterpolationSearch(array, 16877);

👉 6

Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.

#Задача #Lvl2

C#Hive: Projects & Progress | Программирование

15 Dec, 04:59


🖥 Null у значимых типов, оператор ?. и ??

В отличие от ссылочных типов переменным/параметрам значимых типов нельзя напрямую присвоить значение null. Однако нередко бывает удобно, чтобы переменная/параметр значимого типа могли принимать значение null, Например, получаем числовое значение из базы данных, которое в БД может отсутствовать. То есть, если значение в БД есть > получим число, если нет > получим null.

Чтобы присвоить переменной/параметру значимого типа значение null, после названия типа указывается знак вопроса ?:
int? val = null;
Console.WriteLine(val);


Здесь переменная val представляет не просто тип int, а тип int?, то есть тип, переменные/параметры которого могут принимать как значения типа int, так и значение null. Здесь мы передаём ей значение null. Но также можно передать и значение типа int:
int? val = null;
IsNull(val); // True

val = 22;
IsNull(val); // False

bool IsNull(int? obj) => obj is null;


Стоит отметить, что фактически запись int? является упрощённой формой использования структуры Nullable<T>. Параметр T в угловых скобках представляет универсальный параметр, вместо которого в программе подставляется конкретный тип данных. Следующие виды определения переменных будут эквивалентны:
int? number1 = 5;
Nullable<int> number2 = 5;


➡️ Оператор условного null
Иногда при работе с объектами, которые принимают значение null, мы можем столкнуться с ошибкой, где мы пытаемся обратиться к объекту, а этот объект равен null. Например, пусть у нас есть следующая система классов:
class Person
{
public Company? Company { get; set; }
}

class Company
{
public string? WebSite { get; set; }
}


Объект Person представляет человека. Его свойство Company представляет компанию, где человек работает. Но человек может не работать, поэтому свойство Company имеет тип Company?, то есть может иметь значение null.

Класс Company в свою очередь содержит свойство WebSite, которое представляет сайт компании. Но у компании может и не быть собственного сайта. Поэтому это свойство имеет тип string?, то есть также допускает значение null.

Допустим, нам надо вывести на консоль сайт компании, где работает человек (если он работает и если у компании есть сайт). На первый взгляд, мы можем написать следующую конструкцию:
void PrintWebSite(Person? person)
{
if (person != null)
{
if (person.Company != null)
{
if (person.Company.WebSite != null)
{
Console.WriteLine(person.Company.WebSite);
}
}
}
}


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

Чтобы конструкцию сильно упростить, есть оператор условного null (Null-Conditional Operator) — оператор ?.:
объект?.компонент


Если объект не равен null, то происходит обращение к компоненту объекта (полю/свойству/методу). Если объект представляет значение null, обращение к компоненту метода не происходит.

Применим оператор, отрефакторив код:
void PrintWebSite(Person? person) => Console.WriteLine(person?.Company?.WebSite);


➡️ Оператор ??
Он называется оператором null-объединения. Применяется для установки значений по умолчанию для типов, которые допускают значение null:
левый_операнд ?? правый_операнд


Оператор возвращает левый операнд, если операнд не равен null, иначе возвращается правый операнд. При этом левый операнд должен принимать null. Посмотрим на примере:
string? text = null;
string name = text ?? "Tom"; // Равно Tom, так как text равен null
Console.WriteLine(name); // Tom

int? id = 200;
int personid = id ?? 1; // Равно 200, так как id не равен null
Console.WriteLine(personid); // 200


Также можно использовать производный оператора ??=, который выполняет аналогичную задачу:
string? text = null;
text ??= "Sam"; // Будет равен Sam

int? id = 100;
id ??= 1; // Будет равен 100


#Полезно #Nullable #ValueTypes

C#Hive: Projects & Progress | Программирование

13 Dec, 04:59


#Опрос

C#Hive: Projects & Progress | Программирование

11 Dec, 05:01


*Если бы контрацептивы были человеком*

#Юмор

C#Hive: Projects & Progress | Программирование

09 Dec, 14:59


Решение задачи к посту.

void CombSort(int[] values)
{
double factor = 1.247;
double step = values.Length - 1;

while (step >= 1)
{
for (int i = 0; i + step < values.Length; i++)
{
ref int v1 = ref values[i];
ref int v2 = ref values[(int)(i + step)];
if (v1 > v2) Swap(ref v1, ref v2);
}

step /= factor;
}
}

void Swap(ref int first, ref int second)
{
int temp = first;
first = second;
second = temp;
}


#Задача #Решение #Полезно

C#Hive: Projects & Progress | Программирование

09 Dec, 04:59


🖥 Задача: сортировка расчёской (⭐️)

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

int[] arr = { 35, 1, 73, 8998, 534, -124, 89, 572, 9, 61 };
CombSort(arr);

👉 [-124, 1, 9, 35, 61, 73, 89, 534, 572, 8998]

int[] arr = { 100, 90, 80, 70, 60, 50, 40, 30, 20, 1 };
CombSort(arr);

👉 [1, 20, 30, 40, 50, 60, 70, 80, 90, 100]

int[] arr = { 4025, 740, 8628, 8790, 18 };
CombSort(arr);

👉 [18, 740, 4025, 8628, 8790]

int[] arr = { -101, 101, -4, -88, 909, -1007 };
CombSort(arr);

👉 [-1007, -101, -88, -4, 101, 909]

Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.

#Задача #Lvl1

C#Hive: Projects & Progress | Программирование

29 Nov, 04:59


🖥 Конструкция try..catch..finally

Глядя на результаты последнего опроса, становится ясно, что не каждый до конца понимает работу конструкции try..catch..finally. Ввиду этого ниже мы разберём детальнее эту особенность языка C#.

Иногда при выполнении программы возникают ошибки, которые трудно предусмотреть или предвидеть, а иногда вовсе невозможно. Например, при передачи файла по сети может неожиданно оборваться сетевое подключение. такие ситуации называются исключениями. Для обработки таких ситуаций предназначена конструкция try..catch..finally:
try
{

}
catch
{

}
finally
{

}


При использовании данной конструкции вначале выполняются все инструкции в блоке try. Если в этом блоке не возникло исключений, то после его выполнения начинает выполняться блок finally. И затем конструкция try..catch..finally завершает свою работу.

Если же в блоке try вдруг возникает исключение, то обычный порядок выполнения останавливается, и среда CLR начинает искать блок catch, который может обработать данное исключение. Если нужный блок catch найден, то он выполняется, и после его завершения выполняется блок finally.

➡️ Разбор на примере
int x = 5;
int y = x / 0;
Console.WriteLine($"Результат: {y}");
Console.WriteLine("Конец программы");


Здесь происходит деление числа на 0, что приведёт к генерации исключения. И при запуске приложения в режиме отладки мы увидим в Visual Studio окошко, которое информирует об исключении. Единственное, что нам останется — это завершить выполнение программы.

Чтобы избежать подобного аварийного завершения программы, следует использовать для обработки исключений конструкцию try..catch..finally. Так, перепишем пример следующим образом:
try
{
int x = 5;
int y = x / 0;
Console.WriteLine($"Результат: {y}");
}
catch
{
Console.WriteLine("Возникло исключение!");
}
finally
{
Console.WriteLine("Блок finally");
}
Console.WriteLine("Конец программы");


Здесь также возникнет исключение в блоке try, по той же причине. И дойдя до строки деления на ноль выполнение программы остановится. CLR найдёт блок catch и передаст управление этому блоку. После блока catch будет выполняться блок finally:
Возникло исключение!
Блок finally
Конец программы


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

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

➡️ Блок finally
Именно из-за этого блока бОльшая часть ответов в последнем опросе является ошибочным.

Особенности блока:
Выполняется в любом случае (при успешном выполнении try; при выбрасывании исключения; при передачи управления по break или return);
Блок необязателен;
Если есть finally, блоки catch необязательны;
Выполняется последним, после блока try и всех выполняемых блоков catch.

Если говорить кратко, то данный блок выполняется всегда перед выходом из всей конструкции try..catch..finally.

#Полезно #Exception #TryCatch

C#Hive: Projects & Progress | Программирование

27 Nov, 14:59


#Опрос

C#Hive: Projects & Progress | Программирование

25 Nov, 04:59


🖥 Await внутри конструкции lock

Сегодня рассмотрим вопрос с собеседования, где предоставлен следующий код:
object sync = new();

lock (sync)
{
await MethodAsync();
}

async Task MethodAsync()
{
// our code
// ...
}


Что будет с этим кодом и почему? Под спойлером ниже подробно разобран ответ, поэтому не спеши его смотреть, если интересно подумать и ответить самостоятельно.

Сперва отмечу, что данный код не скомпилируется. Можно подумать, что для команды разработчиков компилятора это слишком сложно/невозможно реализовать, но нет. Тот факт, что энтузиасты из stackoverflow пытались обойти это ограничение — прямое тому подтверждение. Скорее это невероятно плохая идея, потому это не допускается, чтобы избежать ошибок. Ожидание внутри блокировки — это рецепт создания взаимоблокировок (deadlocks).

➡️ Инверсия блокировок
Между моментом, когда await возвращает управление вызывающему потоку и моментом, когда метод возобновляет работу — выполняется произвольный код. Этот произвольный код может снимать блокировки, которые приводят к инверсии порядка блокировок и, следовательно, к взаимоблокировкам.

➡️ Выполнение в разных потоках
Более того, обычно вы снова возобновляете работу в потоке, который выполнил await, но это не обязательно. В сложных сценариях код может возобновиться в другом потоке, что не приведёт к разблокировке объекта sync исходным потоком.


#Полезно #Async #Await #Собеседование

C#Hive: Projects & Progress | Программирование

23 Nov, 14:59


Решение задачи к посту.

void ShakerSort(int[] values)
{
int left = 0;
int right = values.Length - 1;
bool needContinue;

do
{
needContinue = false;

for (int i = left; i < right; i++)
{
if (values[i] > values[i + 1])
{
Swap(ref values[i], ref values[i + 1]);
needContinue = true;
}
}
right--;

for (int i = right; i > left; i--)
{
if (values[i] < values[i - 1])
{
Swap(ref values[i], ref values[i - 1]);
needContinue = true;
}
}
left++;
} while (needContinue);
}

void Swap(ref int first, ref int second)
{
int temp = first;
first = second;
second = temp;
}


#Задача #Решение #Полезно

C#Hive: Projects & Progress | Программирование

23 Nov, 04:59


🖥 Задача: шейкерная сортировка (⭐️)

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

int[] arr = { 94, 1, 0, -100, 88, -3, 18 };
ShakerSort(arr);

👉 [-100, -3, 0, 1, 18, 88, 94]

int[] arr = { 5, 8, 7, 1, 14, 2, 13, 9, 11, 6, 10, 15, 12, 3, 4 };
ShakerSort(arr);

👉 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

int[] arr = { 2, 3, 4, 1 };
ShakerSort(arr);

👉 [1, 2, 3, 4]

int[] arr = { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
ShakerSort(arr);

👉 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.

#Задача #Lvl1

C#Hive: Projects & Progress | Программирование

13 Nov, 04:59


📈 Обогащаемся знаниями!

#Дайджест полезного материала за последнее время.

Задачи
:
Угадай число
Сжатие данных без потерь (часть №1)
Извлечение подчисла
Деление слов на слоги
Магическая матрица
Пузырьковая сортировка

Опросы — что будет выведено на экран:
№1
№2
№3
№4
№5
№6
№7

Полезное:
Рефлексия на примерах
Метод AsParallel от LINQ
Рефлексия и атрибуты
Частичные классы и методы
Собственные операторы преобразования типов
Разница между throw и throw ex при обработке исключений
Разбор основ с делегатами
Анонимные методы
События
Ковариантность и контравариантность делегатов
Основные типы делегатов и замыкания
Span и многомерные массивы

Самые важные хэштеги:
#Фриланс #Полезно #Задача #Опрос #LINQ #Проект

C#Hive: Projects & Progress | Программирование

11 Nov, 04:59


#Опрос

C#Hive: Projects & Progress | Программирование

09 Nov, 04:59


🖥 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

C#Hive: Projects & Progress | Программирование

07 Nov, 14:59


Решение задачи к посту.

void BubbleSort<T>(T[] arr, bool byDescending) where T : struct, IComparable<T>
{
int end = arr.Length;
int targetSort = byDescending ? -1 : 1;

while (end != 0)
{
end--;

for (int i = 0; i < end; i++)
{
if (arr[i].CompareTo(arr[i + 1]) == targetSort)
{
T temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
}
}
}
}


#Задача #Решение #Полезно

C#Hive: Projects & Progress | Программирование

07 Nov, 04:59


🖥 Задача: пузырьковая сортировка (⭐️)

Написать обобщённый метод, который принимает массив любого значимого типа и булевое значение, отвечающее за сортировку по убыванию/возрастанию. Метод должен реализовать алгоритм пузырьковой сортировки исходного массива. Ниже приведены примеры.

int[] array = { 4, 1, 2, 6, 3, 5, 2 };
BubbleSort<int>(arr: array, byDescending: false);

👉 [1, 2, 2, 3, 4, 5, 6]

long[] array = { 55, 11, 41763609734, 63, 8, -336, 128, 95, 0 };
BubbleSort(array, true);

👉 [41763609734, 128, 95, 63, 55, 11, 8, 0, -336]

float[] array = { -1.23f, 3.14f, 9 };
BubbleSort(array, true);

👉 [9, 3,14, -1,23]

DateTime[] array = { new DateTime(2012, 12, 21), new DateTime(2024, 11, 7), DateTime.Parse("14.8.2021") };
BubbleSort(array, false);

👉 [21.12.2012 00:00:00, 14.08.2021 00:00:00, 07.11.2024 00:00:00]

Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.

#Задача #Lvl1

C#Hive: Projects & Progress | Программирование

28 Oct, 05:01


🖥 Основные типы делегатов и замыкания

В .NET есть несколько встроенных делегатов, которые используются в различных ситуациях. И наиболее используемыми являются Action, Predicate и Func.

➡️ Action
Делегат представляет собой некоторое действие, которое ничего не возвращает, то есть в качестве возвращаемого типа имеет тип void:
public delegate void Action()
public delegate void Action<in T>(T obj)


Обобщённый делегат имеет ряд перегруженных версий. Каждая версия принимает разное число параметров: от Action<in T> до Action<in T1, in T2, ... in T16>. Таким образом можно передать до 16 значений в метод.

Как правило, этот делегат передаётся в качестве параметра метода и предусматривает вызов определённых действий в ответ на произошедшее действие:
DoOperation(10, 6, Add); // 10 + 6 = 16
DoOperation(10, 6, Multiply); // 10 * 6 = 60

void DoOperation(int a, int b, Action<int, int> op) => op(a, b);
void Add(int x, int y) => Console.WriteLine($"{x} + {y} = {x + y}");
void Multiply(int x, int y) => Console.WriteLine($"{x} * {y} = {x * y}");


➡️ Predicate
Делегат представляет собой некоторое условие, который принимает один параметр и возвращает значение типа bool:
delegate bool Predicate<in T>(T obj);


Как правило, используется для сравнения, сопоставления некоторого объекта T определённому условию. В качестве выходного результата возвращается значение true, если условие соблюдено, и false, если не соблюдено:
Predicate<int> isPositive = (int x) => x > 0;

Console.WriteLine(isPositive(20));
Console.WriteLine(isPositive(-20));


➡️ Func
Делегат представляет собой функцию, которая возвращает результат действия и может принимать параметры:
public delegate TResult Func<out TResult>()
public delegate TResult Func<in T, out TResult>(T arg)


Он также имеет различные формы: от Func<out T> до Func<in T1, in T2, ... in T16, out TResult>, т.е. может принимать до 16 параметров.

Данный делегат также часто используется в качестве параметра в методах:
int result = DoOperation(6, DoubleNumber);
Console.WriteLine(result); // 12

result = DoOperation(6, SquareNumber);
Console.WriteLine(result); // 36

int DoOperation(int n, Func<int, int> operation) => operation(n);
int DoubleNumber(int n) => 2 * n;
int SquareNumber(int n) => n * n;


➡️ Замыкания
Замыкание — это объект функции, который запоминает своё лексическое окружение даже в том случае, когда она выполняется вне своей области видимости.

Технически, замыкание включает:
Внешняя функция, которая определяет некоторую область видимости и в которой определены некоторые переменные и параметры;
Лексическое окружение — переменные и параметры, которые определены во внешней функции;
Вложенная функция, которая использует переменные и параметры внешней функции.

Рассмотрим реализацию замыканий через локальные функции:
var fn = Outer(); // Возвращает метод Inner()

// Вызываем внутреннюю функцию Inner()
fn(); // 6
fn(); // 7
fn(); // 8

Action Outer() // Метод или внешняя функция
{
int x = 5; // Лексическое окружение — локальная переменная
return Inner; // Возвращаем локальную функцию

void Inner() // Локальная функция
{
x++; // Операции с лексическим окружением
Console.WriteLine(x);
}
}


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

➡️ Применение параметров в замыкании
Кроме внешних переменных к лексическому окружению также относятся параметры окружающего метода:
var fn = Multiply(5);

Console.WriteLine(fn(5)); // 25
Console.WriteLine(fn(6)); // 30
Console.WriteLine(fn(7)); // 35

Func<int, int> Multiply(int n)
{
return Inner;

int Inner(int m) => n * m;
}


Как итог, при вызове метода Multiply определяется переменная fn, которая получает локальную функцию Inner и её лексическое окружение — значение параметра n (в нашем случае n = 5).

#Полезно #Delegate

C#Hive: Projects & Progress | Программирование

26 Oct, 04:59


#Опрос

C#Hive: Projects & Progress | Программирование

24 Oct, 04:59


🖥 Ковариантность и контравариантность делегатов

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

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

Для рассмотрения ковариантности и контравариантности будем использовать иерархию следующих классов:
class Message
{
public string Text { get; }
public Message(string text) => Text = text;

public virtual void Print() => Console.WriteLine($"Message: {Text}");
}

class EmailMessage : Message
{
public EmailMessage(string text) : base(text) { }

public override void Print() => Console.WriteLine($"Email: {Text}");
}

class SmsMessage : Message
{
public SmsMessage(string text) : base(text) { }

public override void Print() => Console.WriteLine($"Sms: {Text}");
}


➡️ Ковариантность
Позволяет передать делегату метод, возвращаемый тип которого является производным от возвращаемого типа делегата. То есть если у делегата возвращаемый тип Message, то метод может иметь в качестве возвращаемого типа класс EmailMessage:
// Делегату с базовым типом передаём метод с производным типом
MessageBuilder messageBuilder = WriteEmailMessage; // Ковариантность
Message message = messageBuilder("Hello");

message.Print(); // Email: Hello

EmailMessage WriteEmailMessage(string text) => new EmailMessage(text);

delegate Message MessageBuilder(string text);


Благодаря ковариантности делегат MessageBuilder может указывать на метод, который возвращает объект производного типа.

➡️ Контравариантность
Позволяет присвоить делегату метод, тип параметра которого является более универсальным по отношению к типу параметра у делегата:
// Делегату с производным типом передаём метод с базовым типом
EmailReceiver emailBox = ReceiveMessage; // Контравариантность

emailBox?.Invoke(new EmailMessage("Welcome")); // Email: Welcome

void ReceiveMessage(Message message) => message.Print();

delegate void EmailReceiver(EmailMessage message);


Кажется, что здесь есть противоречие, т.е. использование более универсального типа вместо более производного. В реальности в делегат при его вызове мы всё равно можем передать только тип EmailMessage, а любой объект этого типа является объектом типа Message, который используется в методе.

➡️ Обобщённые делегаты
Обобщённые делегаты тоже могут быть ковариантными и контравариантными, что даёт больше гибкости в их использовании.

Рассмотрим ковариантный обобщённый делегат:
// Возвращает более конкретный тип
MessageBuilder<EmailMessage> emailMessageWriter = (t) => new EmailMessage(t);
// Возвращает более общий тип
MessageBuilder<Message> messageBuilder = emailMessageWriter; // Ковариантность
Message message = messageBuilder("hello Tom"); // Вызов делегата

message.Print(); // Email: hello Tom

delegate T MessageBuilder<out T>(string text);


Благодаря ключевому слову out мы можем присвоить делегату с типом <Message> делегат с типом <EmailMessage>.

Рассмотрим контравариантный обобщённый делегат:
// Принимает объект более общего типа
MessageReceiver<Message> messageReceiver = (Message message) => message.Print();
// Принимает объект более конкретного типа
MessageReceiver<EmailMessage> emailMessageReceiver = messageReceiver; // Контравариантность

messageReceiver(new Message("Hello World!")); // Message: Hello World!
messageReceiver(new EmailMessage("Hello World!")); // Email: Hello World!

delegate void MessageReceiver<in T>(T message);


Использование ключевого слова in позволяет присвоить делегату с производным типом <EmailMessage> делегат с базовым типом <Message>.

❗️Если грубо обобщить, ковариантность — от более конкретного к более общему типу (EmailMessage > Message), а контравариантность — от более общего к более конкретному типу (Message > EmailMessage).

#Полезно #Delegate #Generics #Params

C#Hive: Projects & Progress | Программирование

22 Oct, 14:59


Решение задачи к посту.

void MagicMatrix(int size)
{
if (size < 2) throw new ArgumentOutOfRangeException("Размер матрицы слишком мал");

var matrix = Create(size);
Print(matrix, size);
}

int[,] Create(int size)
{
int[,] matrix = new int[size, size];
Random random = new();
const int targetSum = 100;

for (int row = 0; row < size; row++)
{
for (int column = 0; column < size; column++)
{
int columnsSum = 0;
for (int i = row - 1; i != -1; i--) columnsSum += matrix[i, column];

if (row + 1 == size)
{
matrix[row, column] = targetSum - columnsSum;
continue;
}

int rowsSum = 0;
for (int i = column - 1; i != -1; i--) rowsSum += matrix[row, i];

int maxValue = targetSum + 1 - columnsSum - rowsSum;
matrix[row, column] = column + 1 == size
? targetSum - rowsSum
: maxValue < 2
? 0
: random.Next(maxValue);
}
}

return matrix;
}

void Print(int[,] matrix, int size)
{
for (int row = 0; row < size; row++)
{
Console.WriteLine();
for (int column = 0; column < size; column++) Console.Write($"{matrix[row, column]}\t");
}
}


#Задача #Решение #Полезно

C#Hive: Projects & Progress | Программирование

22 Oct, 04:59


🖥 Задача: магическая матрица (⭐️)

Написать метод, который принимает размер (N^2), генерирует магическую матрицу и выводит результат на экран. Магическая матрица — это двухмерный массив, в котором сумма элементов в каждом ряду и столбце равна числу 100. Ниже приведены примеры.

MagicMatrix(6);

👉 93 1 5 1 0 0
👉 2 3 74 3 15 3
👉 1 3 2 19 53 22
👉 2 17 0 55 0 26
👉 2 73 0 0 0 25
👉 0 3 19 22 32 24

MagicMatrix(2);

👉 89 11
👉 11 89

MagicMatrix(3);

👉 42 50 8
👉 33 16 51
👉 25 34 41

MagicMatrix(4);

👉 95 1 1 3
👉 0 51 5 44
👉 2 20 34 44
👉 3 28 60 9

Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.

#Задача #Lvl1

C#Hive: Projects & Progress | Программирование

20 Oct, 05:00


🖥 События

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

➡️ Назначение событий
Для более точного понимания, напишем следующий код для консольного приложения:
Console.CancelKeyPress += Console_CancelKeyPress;

while (true) await Task.Delay(1000);

void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e)
{
Console.Beep();
}


В первой строке мы присвоили делегат событию класса Console. Задача этого делегата простая — дать сигнал. Таким образом мы сообщаем классу Console, чтобы при перехвате комбинации клавиш Ctrl+C он воспроизвёл все наши инструкции в методе Console_CancelKeyPress.

Теперь запустим выполнение, откроем консоль и нажмём Ctrl+C. Выполнив это мы услышим саунд пикер (звуковой сигнал). Для того события и нужны, их задача — это выполнение внешнего кода при возникновении тех или иных событий. В нашем примере, событие срабатывает при нажатии клавиш Ctrl+C или Ctrl+Break.

➡️ Определение и вызов событий
События объявляются в классе с помощью ключевого слова event, после которого указывается тип делегата, который представляет событие:
delegate void Handler(string message);
event Handler Notify;


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

В качестве примера, предоставим событие, которое будет происходить по истечению 5 секунд после вызова:
public delegate void Notify();

public class AlarmClock
{
public event Notify AlarmSounded;

public void Start()
{
Thread.Sleep(TimeSpan.FromSeconds(5));
AlarmSounded?.Invoke();
}
}


Если данное событие будет внедрено в библиотеке №1, то разработчик, который её написал, может не знать, что именно требуется сделать после истечения 5 секунд и требуется ли вообще.

Теперь, если нам потребуется связать библиотеку №1 с нашей программой, то мы можем поступить следующим образом:
public class MainProgram
{
public static void Main()
{
AlarmClock alarmClock = new AlarmClock();
alarmClock.AlarmSounded += WakeUp;
alarmClock.Start();
}

public static void WakeUp() => Console.WriteLine("Waking Up!");
}


После вызова метода alarmClock.Start() через 5 секунд в консоль будет выведено:
Waking Up!


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

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

#Полезно #Delegate #Events

C#Hive: Projects & Progress | Программирование

12 Oct, 04:59


Классический заказчик на фрилансе.

#Юмор

C#Hive: Projects & Progress | Программирование

10 Oct, 04:59


#Опрос

C#Hive: Projects & Progress | Программирование

08 Oct, 05:01


🖥 Делегаты: анонимные методы

С делегатами тесно связаны анонимные методы. Анонимные методы используются для создания экземпляров делегатов.

Определение анонимных методов начинается с ключевого слова delegate, после которого идёт в скобках список параметров и тело метода в фигурных скобках:
MessageHandler handler = delegate (string mes)
{
Console.WriteLine(mes);
};
handler("hello world!");

delegate void MessageHandler(string message);


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

Другой пример анонимных методов — передача в качестве аргумента для параметра, который представляет делегат:
ShowMessage("hello!", delegate (string mes) { Console.WriteLine(mes); });

static void ShowMessage(string message, MessageHandler handler)
{
handler.Invoke(message);
}

delegate void MessageHandler(string message);


Если анонимный метод использует параметры, то они должны соответствовать параметрам делегата. Если для анонимного метода не требуется параметров, то скобки с параметрами опускаются. При этом, даже если делегат принимает несколько параметров, то в анонимном методе можно вовсе опустить параметры:
MessageHandler handler = delegate { Console.WriteLine("анонимный метод"); };
handler("hello world!"); // анонимный метод

delegate void MessageHandler(string message);


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

Как и обычные методы, анонимные могут возвращать результат:
Operation operation = delegate (int x, int y) { return x + y; };
int result = operation(4, 5);
Console.WriteLine(result); // 9

delegate int Operation(int x, int y);


При этом, анонимный метод имеет доступ ко всем переменным, определённым во внешнем коде:
int z = 8;
Operation operation = delegate (int x, int y) { return x + y + z; };

int result = operation(4, 5);
Console.WriteLine(result); // 17

delegate int Operation(int x, int y);


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

#Полезно #Delegate

C#Hive: Projects & Progress | Программирование

06 Oct, 14:59


Решение задачи к посту.

string[] Syllabify(string word)
{
// Гласные и два звука в сочетании со звуком 'й'
char[] vowels = new[] { 'а', 'о', 'у', 'и', 'ы', 'э', 'е', 'ё', 'я', 'ю' };

word = word.ToLower();
int totalSyllables = word.Count(vowels.Contains);
if (totalSyllables < 2) return new[] { word };
string[] syllables = new string[totalSyllables];

int start = 0;
for (int s = 0; s < totalSyllables; s++)
{
for (int i = start; i < word.Length; i++)
{
syllables[s] += word[i];

bool nextHasVowel = i + 1 >= word.Length ? false : vowels.Contains(word[i + 1]);
bool after1HasVowel = i + 2 >= word.Length ? false : vowels.Contains(word[i + 2]);
bool currentSyllableHasVowel = syllables[s].Count(vowels.Contains) == 1;

if (currentSyllableHasVowel && (nextHasVowel || after1HasVowel))
{
start = i + 1;
break;
}
}
}

return syllables;
}


#Задача #Решение #Полезно

C#Hive: Projects & Progress | Программирование

06 Oct, 10:01


Нельзя просто так взять и не программировать баги 👌

#Юмор

C#Hive: Projects & Progress | Программирование

06 Oct, 04:59


🖥 Задача: деление слов на слоги (⭐️)

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

Syllabify("Многообразие");

👉 [мно, го, об, ра, зи, е]

Syllabify("джигурда");

👉 [джи, гур, да]

Syllabify("Научный");

👉 [на, уч, ный]

Syllabify("эль");

👉 [эль]

Syllabify("Экземпляр");

👉 [эк, земп, ляр]

Syllabify("Достопримечательность");

👉 [дос, топ, ри, ме, ча, тель, ность]

Syllabify("машина");

👉 [ма, ши, на]

Syllabify("скрепка");

👉 [скреп, ка]

Syllabify("ОЛОВО");

👉 [о, ло, во]

Syllabify("яЛта");

👉 [ял, та]

Syllabify("Трудоёмкость");

👉 [тру, до, ём, кость]

Syllabify("яблоко");

👉 [яб, ло, ко]

Syllabify("кресло");

👉 [крес, ло]

Syllabify("ЯНА");

👉 [я, на]

Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.

#Задача #Lvl1

C#Hive: Projects & Progress | Программирование

26 Sep, 04:59


🖥 Делегаты: разбор основ

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

➡️ Определение делегатов
Для объявления делегата используется ключевое слово delegate, после которого идёт возвращаемый тип, название и параметры. Например:
delegate void Foo(int param);


По сути, мы определили новый тип в своей сборке, представляющий собой делегат, который в качестве возвращаемого типа имеет тип void (т.е. ничего не возвращает) и принимает аргумент типа int.

Рассмотрим применение этого делегата:
Foo foo = Info; // Создаём переменную делегата и присваиваем этой переменной адрес метода
foo(1); // Вызываем метод. На экран будет выведено "Value is 1"

void Info(int value) => Console.WriteLine($"Value is {value}");

delegate void Foo(int param);


Сам вызов делегата производится подобно вызову метода.

➡️ Место определения делегата
Если мы определяем делегат в программах верхнего уровня (top-level program), которую по умолчанию представляет файл Program.cs, то, как и другие типы, делегат определяется в конце кода. Но делегат можно определять и внутри класса:
public class Program
{
public delegate void Message();

static void Main()
{
// Наш код
// ...
}
}


Либо вне класса:
public delegate void Message();

public class Program
{
static void Main()
{
// Наш код
// ...
}
}


➡️ Добавление методов в делегат
В примерах выше переменная делегата указывала на один метод. В реальности же, делегат может указывать на множество методов, которые имеют ту же сигнатуру и возвращаемый тип. Все методы в делегате попадают в специальный список вызова — invocation list. При вызове делегата все методы из этого списка последовательно вызываются. И мы можем добавлять в этот список не один, а несколько методов:
Message message = Hello;
message += HowAreYou;
message();

void Hello() => Console.WriteLine("Hello");
void HowAreYou() => Console.WriteLine("How are you?");

delegate void Message();


Следует учесть, что мы можем добавить ссылку на один и тот же метод несколько раз, и в списке вызова делегата тогда будет несколько ссылок на один и то же метод. Соответственно, при вызове делегата добавленный метод будет вызываться столько раз, сколько он был добавлен:
Message message = Hello;
message += HowAreYou;
message += Hello;
message += Hello;

message();


Консольный вывод:
Hello
How are you?
Hello
Hello


Подобным образом мы можем удалять методы из делегата:
Message message = Hello;
message += HowAreYou;
message(); // Вызываются методы Hello и HowAreYou

message -= HowAreYou; // Удаляем метод HowAreYou из делегата
message(); // Вызывается метод Hello


Отмечу, что при удалении метода может сложиться ситуация, что в делегате не будет методов, и тогда переменная будет иметь значение null. Поэтому перед вызовом рекомендуется проверка на null:
if (message != null) message();


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

➡️ Вызов делегата
В примерах выше, делегат вызывался как обычный метод:
foo(1);
message();


Другой способ вызова делегата представляет метод Invoke():
Message mes = Hello;
mes.Invoke(); // Вызывается метод Hello

void Hello() => Console.WriteLine("Hello");

delegate void Message();


Если делегат принимает параметры, то в метод Invoke() передаются значения для этих параметров.

Следует учитывать, что если делегат пуст (равен null), то при вызове такого делегата мы получим исключение. Поэтому при вызове всегда лучше проверять, не равен ли он null. Либо можно использовать метод Invoke() и оператор условного null:
mes?.Invoke(); // Ошибки не будет, делегат просто не вызывается


#Полезно #Delegate

C#Hive: Projects & Progress | Программирование

24 Sep, 05:01


#Опрос

C#Hive: Projects & Progress | Программирование

22 Sep, 04:59


🖥 Обработка исключений: разница между throw и throw ex

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

Вспомним, что исключения — это объекты, производные от System.Exception класса. Они представляют ошибки, возникающие во время выполнения приложения. Когда возникает исключение, среда выполнения ищет подходящий catch блок для обработки исключения. Если такой блок не найден, приложение аварийно завершает работу.

➡️ Так в чём всё же разница throw и throw ex?
Оператор throw повторно выдаёт текущее исключение, не изменяя трассировку стека. Это означает, что все исходные детали исключения, включая трассировку стека, сохраняются. Трассировка стека имеет решающее значение для отладки, поскольку она показывает точный путь, который код прошёл до того, как было выдано исключение, предоставляя информацию о том, что пошло не так.

➡️ Убедимся на примере
Напишем следующую простую иерархию вызовов с использованием throw:
Foo();

void Foo()
{
try
{
Bar();
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}
}

void Bar()
{
try { int x = int.Parse("one"); }
catch (Exception ex) { throw; }
}


В методе Bar() мы намеренно провоцируем ошибку конвертации, чтобы здесь же словить исключение в блоке catch. Далее ключевым словом throw мы выбрасываем текущее исключение назад в стек вызовов, чтобы уже метод Foo() обработал данную ошибку и вывел в консоль стек вызовов.

Таким образом, в консоль будет выведено примерно следующее:
   at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type)
at System.Int32.Parse(String s)
at Program.<<Main>$>g__Bar|0_1() in C:\...\ConsoleApp1\Program.cs:line 30
at Program.<<Main>$>g__Foo|0_0() in C:\...\ConsoleApp1\Program.cs:line 20


Стек вызовов нам сообщает, что на 30 строке и возникло исключение. В моём коде на этом месте как раз находится наша провокация:
try { int x = int.Parse("one"); }


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

Теперь немного изменим метод Bar() с использованием throw ex:
void Bar()
{
try { int x = int.Parse("one"); }
catch (Exception ex) { throw ex; }
}


После чего посмотрим на стек вызовов:
   at Program.<<Main>$>g__Bar|0_1() in C:\...\ConsoleApp1\Program.cs:line 31
at Program.<<Main>$>g__Foo|0_0() in C:\...\ConsoleApp1\Program.cs:line 20


Теперь наша ошибка возникает, якобы, на 31 строке, хотя настоящая ошибка была вызвана всё также 30 строкой. Это может показаться безобидным, однако это сбрасывает трассировку стека в точку, где throw ex была вызвана. Это означает, что вы теряете исходную трассировку стека, которая часто необходима для диагностики основной причины ошибки.

➡️ Заключение
Понимая разницу между throw и throw ex — рекомендуется всегда использовать throw для повторной генерации исключения, чтобы не потерять ценную отладочную информацию. Цель состоит в том, чтобы сделать приложение максимально надёжным и предоставить чёткую и полезную информацию, когда что-то пойдет не так.

#Полезно #Exception #TryCatch

C#Hive: Projects & Progress | Программирование

20 Sep, 14:59


Решение задачи к посту.

public static class IntExtensions
{
public static int Subint(this int value, int startIndex) => SubInteger(value, startIndex, null);

public static int Subint(this int value, int startIndex, int length) => SubInteger(value, startIndex, length);

private static int SubInteger(int value, int startIndex, int? length)
{
var nums = ExtractNums(value);

if (startIndex >= nums.Length) throw new ArgumentOutOfRangeException(nameof(startIndex));

if (length == null) return ConcatNums(nums.AsSpan(startIndex));
return ConcatNums(nums.AsSpan(startIndex, (int)length));
}

private static int[] ExtractNums(int value)
{
if (value < 10) return new int[] { value };
List<int> nums = new(2);

while (value != 0)
{
value = Math.DivRem(value, 10, out int rem);
nums.Add(rem);
}

Reverse(nums);
return nums.ToArray();
}

private static void Reverse(List<int> nums)
{
// Swap
for (int start = 0, end = nums.Count - 1; start < end; start++, end--)
{
int value = nums[start];
nums[start] = nums[end];
nums[end] = value;
}
}

private static int ConcatNums(Span<int> nums)
{
int value = 0;

foreach (int num in nums) value = value * 10 + num;

return value;
}
}


#Задача #Решение #Полезно #Extensions

C#Hive: Projects & Progress | Программирование

20 Sep, 04:59


🖥 Задача: извлечение подчисла (⭐️⭐️)

Написать два метода расширения для структуры Int32, которые будут извлекать подчисло из данного экземпляра, по аналогии с методом String.Substring(). Ниже приведены примеры.

int subint = 9.Subint(0, 0);

👉 0

int subint = 88788.Subint(2);

👉 788

int subint = int.MaxValue.Subint(9);

👉 7

int subint = 404.Subint(startIndex: 1);

👉 4

int subint = 2024_09_16.Subint(startIndex: 0, length: 4);

👉 2024

int subint = 123456.Subint(2, 2);

👉 34

Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.

#Задача #Lvl2

C#Hive: Projects & Progress | Программирование

10 Sep, 04:59


🖥 Собственные операторы преобразования типов

Мы знаем, что существуют явные и неявные преобразования примитивных типов. Например:
int x = 50;
byte y = (byte)x; // Явное преобразование от int к byte
int z = y; // Неявное преобразование от byte к int


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

Конструкция оператора преобразования:
public static ключевое_слово operator возвращаемый_тип(исходный_тип arg)

Метод всегда должен быть public static;
Далее ключевое_слово, либо explicit для явного преобразования, либо implicit для неявного преобразования;
Затем ключевое слово operator и возвращаемый_тип, в который надо преобразовать объект;
В скобках, в качестве параметра, передаётся объект, который надо преобразовать.

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

➡️ Практический пример
Рассмотрим преобразования, к примеру, из одного составного типа в другой составной тип. Определим 2 класса:
public class Timer
{
public int Hours { get; }
public int Minutes { get; }
public int Seconds { get; }

public Timer(int hours, int minutes, int seconds)
{
Hours = hours;
Minutes = minutes;
Seconds = seconds;
}

public override string ToString() => $"{Hours}:{Minutes}:{Seconds}";
}

public class Counter
{
public int Seconds { get; }

public Counter(int seconds) => Seconds = seconds;

private const int secInOneHour = 3600;
private const int secInOneMinute = 60;

public static implicit operator Counter(Timer timer)
{
int hours = timer.Hours * secInOneHour;
int minutes = timer.Minutes * secInOneMinute;
return new(hours + minutes + timer.Seconds);
}
public static explicit operator Timer(Counter counter)
{
int hours = counter.Seconds / secInOneHour;
int minutes = (counter.Seconds % secInOneHour) / secInOneMinute;
int seconds = counter.Seconds % secInOneMinute;
return new(hours, minutes, seconds);
}
}


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

Например, 3675 секунд — это 1 час, 1 минута и 15 секунд. Зная это, применим операции преобразования:
Counter counter1 = new(3675);
Timer timer = (Timer)counter1; // Реализация explicit (явная)
Console.WriteLine(timer); // 1:1:15

Counter counter2 = timer; // Реализация implicit (неявная)
Console.WriteLine(counter2.Seconds); // 3675


#Полезно #Operators

C#Hive: Projects & Progress | Программирование

08 Sep, 05:02


#Опрос

C#Hive: Projects & Progress | Программирование

06 Sep, 04:59


🖥 Частичные классы и методы

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

Например, определим в проекте два файла с кодом. Не столь важно как эти файлы будут называться. Например, ClassBase.cs и ClassAdditional.cs. В одном из этих файлов (без разницы в каком именно) определим следующий класс:
public partial class ClassA
{
public void Foo()
{
Console.WriteLine("Method Foo");
}
}


А в другом файле определим следующий класс:
public partial class ClassA
{
public void Bar()
{
Console.WriteLine("Method Bar");
}
}


Таким образом, два файла в проекте содержат определение одного и того же класса ClassA, которые содержат два разных метода. Оба определённых здесь класса являются частичными.

Затем мы можем использовать все методы класса ClassA:
ClassA classA = new();
classA.Foo();
classA.Bar();


➡️ Частичные методы
Разделяемые методы практически аналогичны разделяемым классам: в одной части разделяемого класса объявляют сигнатуру метода, а в другой части разделяемого класса объявляют тело. Разделяемый метод должен начинаться с ключевого слова patrial.

Например, изменим классы ClassA. Первый класс:
public partial class ClassA
{
public partial void Tester();
}


Второй класс:
public partial class ClassA
{
public partial void Tester()
{
Console.WriteLine("Method Tester");
}
}


В первом классе определён метод Tester(). Причём на момент определения первого класса нам неизвестно, что представляет собой этот метод и какие действия он будет выполнять. Однако известен список его параметров.

➡️ Глобальный смысл таких возможностей
На практике с этим сталкиваешься редко, поэтому многие могут даже не знать об этих фичах. Для понимания смысла, стоит уточнить пару ситуаций, когда желательно разделение определения класса.

Классы разделяют при:
1. Объявление класса по отдельным файлам позволяет нескольким программистам одновременно работать над ним;
2. Вы можете добавить код в класс, не создавая исходный файл, который включает автоматически созданный источник. Visual Studio использует этот подход при создании форм Windows Forms, кода оболочки веб-службы и т.д. Можно создать код, который использует эти классы, без необходимости изменения файла, созданного в Visual Studio.

#Полезно

C#Hive: Projects & Progress | Программирование

04 Sep, 14:59


Решение задачи к посту.

void CompressByRLE(ReadOnlySpan<char> chars)
{
StringBuilder sb = new();
char separator = chars[0];
sbyte counter = 0;

foreach (char sym in chars)
{
if (sym == separator && counter != sbyte.MaxValue)
{
counter++;
continue;
}

sb.Append($"{counter}{separator}");
separator = sym;
counter = 1;
}
sb.Append($"{counter}{separator}");

ShowResult(chars.Length, ref sb);
}

void ShowResult(double sourceLength, ref StringBuilder sb)
{
Console.WriteLine($"Результат сжатия: {sb}");
Console.WriteLine($"Символов в сжатой последовательности: {sb.Length}");
Console.WriteLine($"Символов в исходной последовательности: {sourceLength}");
Console.WriteLine($"Данные были сжаты в ~{sourceLength / sb.Length} раз");
}


#Задача #Решение #Полезно

C#Hive: Projects & Progress | Программирование

04 Sep, 04:59


🖥 Задача: сжатие данных без потерь (часть №1) (⭐️⭐️)

Написать метод, который принимает строку, сжимает её по принципу RLE и выводит результат на экран. Ниже приведены примеры.

CompressByRLE("WWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWBWWWWWWWWWWWWWW");

👉 Результат сжатия: 9W3B24W1B14W
👉 Символов в сжатой последовательности: 12
👉 Символов в исходной последовательности: 51
👉 Данные были сжаты в ~4,25 раз

CompressByRLE("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");

👉 Результат сжатия: 127A127A2A
👉 Символов в сжатой последовательности: 10
👉 Символов в исходной последовательности: 256
👉 Данные были сжаты в ~25,6 раз

CompressByRLE("SSSSOOOEEERROOOAAYYYYYDDDDOEUUUUUWWWWJJJORRUUUUUUUUUUXXXKHHHHHHMMMMMMGGGLLLLLLLJJJJ");

👉 Результат сжатия: 4S3O3E2R3O2A5Y4D1O1E5U4W3J1O2R10U3X1K6H6M3G7L4J
👉 Символов в сжатой последовательности: 47
👉 Символов в исходной последовательности: 83
👉 Данные были сжаты в ~1,7659574468085106 раз

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

#Задача #Lvl2