C#Hive: Projects & Progress | Программирование
Open in Telegram
Сообщество единомышленников C#: решаем задачи, учимся, развиваемся и общаемся вместе. Советы по работе на фрилансе, готовые проекты, код ревью, рекомендации и исследования. Вопросы/сотрудничество: @tel_phil9
Show more1 780
Subscribers
No data24 hours
-17 days
-1030 days
Posts Archive
🖥 Ковариантность и контравариантность делегатов
Понятия ковариантности и контравариантности связаны с возможностью использовать в приложении вместо некоторого типа другой тип, который находится ниже или выше в иерархии наследования.
Делегаты также могут быть ковариантными и контравариантными. Ковариантность делегата предполагает, что возвращаемым типом может быть производный тип. Контравариантность делегата предполагает, что типом параметра может быть более универсальный тип.
Для рассмотрения ковариантности и контравариантности будем использовать иерархию следующих классов:
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Решение задачи к посту.
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");
}
}
#Задача #Решение #Полезно🖥 Задача: магическая матрица (⭐️)
Написать метод, который принимает размер (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🖥 События
События позволяют создавать приложения, реагирующие на действия пользователя или внешние события. Они сигнализируют системе о том, что произошло определённое действие, и если нам надо отследить эти действия, то как раз мы можем применять события.
➡️ Назначение событий
Для более точного понимания, напишем следующий код для консольного приложения:
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
Из примера выше, какой метод соответствует делегату Operation?
🖥 Делегаты: анонимные методы
С делегатами тесно связаны анонимные методы. Анонимные методы используются для создания экземпляров делегатов.
Определение анонимных методов начинается с ключевого слова 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Решение задачи к посту.
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;
}
#Задача #Решение #ПолезноНельзя просто так взять и не программировать баги 👌
#Юмор
ХОЧЕШЬ ПОВЫСИТЬ ГРЕЙД В 2024 ГОДУ? 🚀
Чтобы стать Senior C# разработчиком сегодня, нужно не только знать язык программирования и фреймворки. Нужно уметь строить гибкую архитектуру приложения, которую легко тестировать и менять под задачи бизнеса. Стань экспертом в построении гибкой архитектуры приложения!
👉 Стартуем 7 октября.
Курс ведет действующий архитектор и Principal Engineer Кирилл Ветчинкин.
Ты научишься:
✅ Разбивать приложение на слои в соответствии с Clean Architecture
✅ Формировать Domain Model и применять тактические паттерны DDD
✅ Реализовывать Use Case как Command/Query
✅ Делать синхронные и асинхронные интеграции, не загрязняя ядро приложения
✅ Писать 3 вида тестов для разных слоев приложения
Полная программа ТУТ 👉 https://microarch.ru/courses/ddd?utm_source=posev&utm_medium=erid:2Vtzqw9UVi9&utm_campaign=6
А главное — ты с нуля разработаешь и запустишь микросервис, который максимально приближен к реальности "Диспетчеризация заказов на курьеров". Это будет крутым проектом в портфолио или основой для рабочих задач.
А еще:
✅ Проверим все домашки
✅ Поддержим в чате
✅ Проведем живые разборы
✅ Ответим на все вопросы
📕 Сертификат об участии по итогам прохождения курса.
🔥 Не откладывай свой рост на потом: https://microarch.ru/courses/ddd?utm_source=posev&utm_medium=erid:2Vtzqw9UVi9&utm_campaign=6
Реклама. ИП Ветчинкин К.Е. ИНН: 773376451099 Erid: 2Vtzqw9UVi9
🖥 Задача: деление слов на слоги (⭐️)
Написать метод, который принимает слово, делит его на слоги и возвращает их в виде массива. Метод должен обрабатывать русское слово, также допускается небольшая погрешность (пес-ня и пе-сня считается допустимым делением), однако основные правила деления должны быть соблюдены. Ниже приведены примеры.
Syllabify("Многообразие");
👉 [мно, го, об, ра, зи, е]
Syllabify("джигурда");
👉 [джи, гур, да]
Syllabify("Научный");
👉 [на, уч, ный]
Syllabify("эль");
👉 [эль]
Syllabify("Экземпляр");
👉 [эк, земп, ляр]
Syllabify("Достопримечательность");
👉 [дос, топ, ри, ме, ча, тель, ность]
Syllabify("машина");
👉 [ма, ши, на]
Syllabify("скрепка");
👉 [скреп, ка]
Syllabify("ОЛОВО");
👉 [о, ло, во]
Syllabify("яЛта");
👉 [ял, та]
Syllabify("Трудоёмкость");
👉 [тру, до, ём, кость]
Syllabify("яблоко");
👉 [яб, ло, ко]
Syllabify("кресло");
👉 [крес, ло]
Syllabify("ЯНА");
👉 [я, на]
Пишите варианты в комментариях. Решение будет сегодня вечером новым постом в канале.
#Задача #Lvl1🖥 Делегаты: разбор основ
Делегаты представляют объекты, которые указывают на методы. То есть делегаты — это указатели (ссылки) на методы, с помощью которых мы можем вызывать данные методы. По сути, это типобезопасный указатель на функцию, который хранит адрес метода и позволяет вызвать его без необходимости вызывать метод напрямую.
➡️ Определение делегатов
Для объявления делегата используется ключевое слово 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Из примера выше, что будет выведено на экран?
🖥 Обработка исключений: разница между 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
Решение задачи к посту.
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🖥 Задача: извлечение подчисла (⭐️⭐️)
Написать два метода расширения для структуры 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🖥 Собственные операторы преобразования типов
Мы знаем, что существуют явные и неявные преобразования примитивных типов. Например:
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Из примера выше, сколько раз "Process..." будет выведено в консоль?
