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

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

Open in Telegram

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

Show more
1 780
Subscribers
No data24 hours
-17 days
-1030 days
Posts Archive
0% осуждения, 100% понимания. #Юмор

Код из Quake III Arena. Магическое число, побитовые операции, метод Ньютона — настоящая программистская поэзия, но на деле — пример тонкой оптимизации: вычисление обратного квадратного корня.
float Q_rsqrt(float number)
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = *(long *)&y;
    i  = 0x5f3759df - (i >> 1);
    y  = *(float *)&i;
    y  = y * (threehalfs - (x2 * y * y));

    return y;
}
Для тех, кто интересуется играми и разработкой игр — канал Кот и Код. Небольшой разбор этого кода с пояснениями — по ссылке на пост. А также другие полезные заметки с примерами по Unity и коду по тегу #техничка@cat_and_code

🖥 Разбор Strategy паттерна Strategy (Стратегия) — паттерн проектирования, относящийся к поведенческим шаблонам, который опре
🖥 Разбор Strategy паттерна Strategy (Стратегия) — паттерн проектирования, относящийся к поведенческим шаблонам, который определяет набор алгоритмов, инкапсулирует каждый из них и обеспечивает их взаимозаменяемость. В зависимости от ситуации можно легко заменить один используемый алгоритм другим. При этом замена алгоритма происходит независимо от объекта, который использует данный алгоритм. Когда использовать стратегию? Когда есть несколько родственных классов, которые отличаются поведением. Можно задать один основной класс, а разные варианты поведения вынести в отдельные классы и при необходимости их применять; Когда необходимо обеспечить выбор из нескольких вариантов алгоритмов, которые можно легко менять, в зависимости от условий; Когда необходимо менять поведение объектов на стадии выполнения программы; Когда класс, применяющий определённую функциональность, ничего не должен знать о её реализации. Формальный пример шаблона:
public interface IStrategy
{
    void Algorithm();
}

public class ConcreteStrategy1 : IStrategy
{
    public void Algorithm() { }
}

public class ConcreteStrategy2 : IStrategy
{
    public void Algorithm() { }
}

public class Context
{
    public IStrategy Strategy { get; set; }

    public Context(IStrategy strategy) => Strategy = strategy;

    public void Execute() => Strategy.Algorithm();
}
Как видно, здесь есть следующие участники: Интерфейс IStrategy, который определяет метод Algorithm(). Это общий интерфейс для всех реализующих его алгоритмов. Вместо интерфейса здесь также можно было бы использовать абстрактный класс; Классы ConcreteStrategy1 и ConcreteStrategy2, которые реализуют интерфейс IStrategy, предоставляя свою версию метода Algorithm(). Подобных классов-реализаций может быть множество; Класс Context хранит ссылку на объект IStrategy и связан с интерфейсом IStrategy отношением агрегации. В данном случае, объект IStrategy заключена в свойстве, хотя также можно было бы определить приватную переменную, а для динамической установки использовать специальный метод. ➡️ Реальный пример Машины используют разные источники энергии: электричество, бензин, газ и так далее. Есть гибридные автомобили. Они похожи и отличаются преимущественно видом источника энергии. Не говоря уже о том, что мы можем изменить применяемый источник энергии, модифицировав автомобиль. И здесь вполне можно применить паттерн стратегии:
Car auto = new Car(4, "Volvo", new PetrolMove());
auto.Move();

auto.Movable = new ElectricMove();
auto.Move();

interface IMovable
{
    void Move();
}

class PetrolMove : IMovable
{
    public void Move() => Console.WriteLine("Перемещение на бензине");
}

class ElectricMove : IMovable
{
    public void Move() => Console.WriteLine("Перемещение на электричестве");
}

class Car
{
    public IMovable Movable { private get; set; }

    public Car(int passengers, string model, IMovable movable)
    {
        _passengers = passengers;
        _model = model;
        Movable = movable;
    }

    protected int _passengers;
    protected string _model;

    public void Move() => Movable.Move();
}
Вывод следующий:
Перемещение на бензине
Перемещение на электричестве
В качестве IStrategy выступает интерфейс IMovable, определяющий метод Move(). Реализация этого интерфейса происходит классами ElectricMove и PetroleMove. И данные алгоритмы использует класс Car. #Полезно #Strategy #Паттерн

Решение задачи к посту.
void Process(List<int> numbers, List<string> strings)
{
    if (numbers == null || numbers.Count == 0)
    {
        Console.WriteLine($"Нет данных для обработки ({nameof(numbers)}).");
        return;
    }
    if (strings == null || strings.Count == 0)
    {
        Console.WriteLine($"Нет данных для обработки ({nameof(strings)}).");
        return;
    }

    CalculateData(numbers);
    FilterAndTransform(strings);
}

void CalculateData(List<int> list)
{
    int sum = 0, max = list[0], min = list[0];

    foreach (int value in list)
    {
        sum += value;
        if (value > max) max = value;
        if (value < min) min = value;
    }

    Console.WriteLine($"Сумма: {sum}");
    Console.WriteLine($"Минимум: {min}");
    Console.WriteLine($"Максимум: {max}");
    Console.WriteLine($"Среднее: {(double)sum / list.Count}");
}

void FilterAndTransform(List<string> list)
{
    var filtered = list
        .Where(x => x.Length > 5 && x.StartsWith("A", StringComparison.CurrentCultureIgnoreCase))
        .Select(x => x.ToUpper());
    Console.WriteLine($"Фильтрованный: {string.Concat(filtered)}");
}
#Задача #Решение #Полезно #Рефакторинг #LINQ

🖥 Задача: рефакторинг кода (⭐️) Ниже предоставлен вырезанный из контекста блок кода. Необходимо упростить данный код, сделав его более читаемым, лаконичным и эффективным, сохранив при этом его функционал.
void Process(List<int> numbers, List<string> strings)
{
    if (numbers == null || numbers.Count == 0)
    {
        Console.WriteLine("Нет данных для обработки.");
        return;
    }

    int sum = 0;
    foreach (int number in numbers) sum += number;
    Console.WriteLine("Сумма: " + sum);

    int min = numbers[0];
    foreach (int number in numbers) if (number < min) min = number;
    Console.WriteLine("Минимум: " + min);

    int max = numbers[0];
    foreach (int number in numbers) if (number > max) max = number;
    Console.WriteLine("Максимум: " + max);

    double average = (double)sum / numbers.Count;
    Console.WriteLine("Среднее: " + average);

    if (strings == null || strings.Count == 0)
    {
        Console.WriteLine("Нет данных для обработки.");
        return;
    }

    List<string> filteredList = new();
    foreach (string item in strings) if (item.Length > 5) filteredList.Add(item.ToUpper());

    List<string> resultList = new();
    foreach (string item in filteredList) if (item.StartsWith("A")) resultList.Add(item);

    string result = "";
    for (int i = 0; i < resultList.Count; i++) result += resultList[i];
    Console.WriteLine("Фильтрованный: " + result);
}
Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале. #Задача #Lvl1

🖥 Отложенное и немедленное выполнение LINQ Есть два способа выполнения запроса LINQ: отложенное (deferred) и немедленное (im
🖥 Отложенное и немедленное выполнение 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

Из примера выше, что будет выведено на экран?
Anonymous voting

🖥 Отложенные операции Отложенные операции откладывают вычисление до момента, когда результат действительно нужен. Это позволяет оптимизировать производительность и избегать ненужных вычислений. Не отложенные операции выполняются сразу же, как только они вызываются. Ниже разберём основные способы определения отложенных операций, а также примеры распространённых сценариев. ➡️ Yield return Методы, использующие yield return, являются отложенными. Они возвращают итератор (IEnumerable или IEnumerator), который "производит" значения по мере необходимости. Пример отложенного метода:
IEnumerable<int> GenerateNumbers(int count)
{
    for (int i = 0; i < count; i++)
    {
        Console.WriteLine($"Генерация {i}");
        yield return i;
    }
}
Пока мы не запросим значения из итератора, вывод в консоль не произойдет:
var nums = GenerateNumbers(3);
Метод выполняет код до yield return при каждом вызове MoveNext у возвращённого итератора, поэтому в коде ниже мы уже получим результаты, которые ожидаем:
foreach (int num in nums) Console.WriteLine($"Получен {num}");
Консольный вывод:
Генерация 0
Получен 0
Генерация 1
Получен 1
Генерация 2
Получен 2
➡️ Класс Lazy<T> Класс специально предназначен для отложенной инициализации значений. Значение инициализируется только при первом обращении к свойству Value. Пример отложенной инициализации класса:
Lazy<MyClass> lazyMyClass = new();

class MyClass
{
    public MyClass() => Console.WriteLine("Класс создан!");
}
Класс MyClass ещё не создан, его инициализация отложена и консоль пустая. Теперь вызовем отложенную операцию:
Console.WriteLine(lazyMyClass.Value.GetHashCode());
Консольный вывод:
Класс создан!
43942917
Lazy гарантирует, что вычисление будет выполнено только один раз и по требованию, даже если мы обратимся к Value много раз. Дополнительно его можно использовать в потокобезопасной среде, но сейчас не об этом. ➡️ Делегаты и лямбда-выражения Сами по себе делегаты и лямбды не являются отложенными, но они часто используются для реализации отложенных вычислений. Пример отложенного вычисления с делегатом:
Func<int> valueGetter = () =>
{
    Console.WriteLine("Получение значения");
    return 678;
};
Лямбда-выражение определяет действие, которое не выполняется, пока не будет вызван делегат. Вычисление произойдёт только при первом вызове:
Console.WriteLine(valueGetter());
Консольный вывод:
Получение значения
678
➡️ Методы LINQ Множество методов LINQ являются отложенными. Они создают цепочки вычислений, которые не выполняются до тех пор, пока не будет запрошен результат. Более подробно разберём это уже в следующем обзоре. #Полезно

Решение задачи к посту.
void BinarySearch(int[] array, int key, bool orderedByAscending)
{
    int left = 0, right = array.Length - 1, step = 0;

    while (left <= right)
    {
        int mid = (left + right) / 2;
        int value = array[mid];

        Console.WriteLine($"Шаг {++step}: A{left} ... A{mid} = {value} ... A{right}");
        if (value == key)
        {
            Console.WriteLine($"Ключ находится по {mid} индексу");
            return;
        }

        if (orderedByAscending)
        {
            if (value > key) right = mid - 1;
            else left = mid + 1;
        }
        else
        {
            if (value < key) right = mid - 1;
            else left = mid + 1;
        }
    }

    Console.WriteLine("Ключ не найден!");
}
#Задача #Решение #Полезно

🖥 Задача: двоичный (бинарный) поиск (⭐️) Написать функцию, которая принимает отсортированный массив, ключ и статус сортировки массива (по возрастанию/убыванию). Функция должна выполнять алгоритм бинарного поиска по массиву для нахождения ключа и выводить на экран границы проверяемого массива на каждом шагу. Ниже приведены примеры.
int[] a = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
BinarySearch(array: a, key: 11, orderedByAscending: true);
👉 Шаг 1: A0 ... A5 = 6 ... A11 👉 Шаг 2: A6 ... A8 = 9 ... A11 👉 Шаг 3: A9 ... A10 = 11 ... A11 👉 Ключ находится по 10 индексу
int[] a = { 1, 4, 9, 16, 25, 36, 49, 64, 81 };
BinarySearch(a, 16, true);
👉 Шаг 1: A0 ... A4 = 25 ... A8 👉 Шаг 2: A0 ... A1 = 4 ... A3 👉 Шаг 3: A2 ... A2 = 9 ... A3 👉 Шаг 4: A3 ... A3 = 16 ... A3 👉 Ключ находится по 3 индексу
int[] a = { 9, 8, 7, 6, 5, 4, 3, 2, 1 };
BinarySearch(a, 6, false);
👉 Шаг 1: A0 ... A4 = 5 ... A8 👉 Шаг 2: A0 ... A1 = 8 ... A3 👉 Шаг 3: A2 ... A2 = 7 ... A3 👉 Шаг 4: A3 ... A3 = 6 ... A3 👉 Ключ находится по 3 индексу
int[] a = Enumerable.Range(1, 1024).ToArray();
BinarySearch(a, 2000, true);
👉 Шаг 1: A0 ... A511 = 512 ... A1023 👉 Шаг 2: A512 ... A767 = 768 ... A1023 👉 Шаг 3: A768 ... A895 = 896 ... A1023 👉 Шаг 4: A896 ... A959 = 960 ... A1023 👉 Шаг 5: A960 ... A991 = 992 ... A1023 👉 Шаг 6: A992 ... A1007 = 1008 ... A1023 👉 Шаг 7: A1008 ... A1015 = 1016 ... A1023 👉 Шаг 8: A1016 ... A1019 = 1020 ... A1023 👉 Шаг 9: A1020 ... A1021 = 1022 ... A1023 👉 Шаг 10: A1022 ... A1022 = 1023 ... A1023 👉 Шаг 11: A1023 ... A1023 = 1024 ... A1023 👉 Ключ не найден! Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале. #Задача #Lvl1

🖥 Атомарные операции, безопасность потоков и состояние гонки «Атом» происходит от греческого atomos — "неразрезаемый" и испо
🖥 Атомарные операции, безопасность потоков и состояние гонки «Атом» происходит от греческого 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}");
Теперь при вызове кода мы всегда будем получать ожидаемые результаты. #Полезно #Многопоточность

Из примера выше, какое значение более вероятно будет выведено на экран?
Anonymous voting

🖥 Аннотация NotNull: гарантируем, что значения не будут пустыми NotNull — это атрибут, который указывает, что переменная, па
🖥 Аннотация 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

Решение задачи к посту.
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);
}
#Задача #Решение #Полезно #Рекурсии

🖥 Задача: интерполяционный поиск (⭐️⭐️) Написать функцию, которая принимает отсортированный массив по возрастанию вместе с ключом и выполняет алгоритм интерполяционного поиска по массиву для нахождения индекса того элемента, который равен ключу. Код должен содержать рекурсию и возвращать индекс элемента (-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

🖥 Null у значимых типов, оператор ?. и ?? В отличие от ссылочных типов переменным/параметрам значимых типов нельзя напрямую
🖥 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