Изображение к статье создано при помощи нейросети Kandinsky
Всем привет.
В этой статье мы постараемся затронуть следующие темы, относящиеся к разработке программ на C#:
- перегрузка операторов (на примере перегрузки операторов +, -, /, *)
- индексаторы (не основная тема статьи, но индексатор будет применён в примере рассматриваемого кода)
- перечислители и реализация интерфейса IEnumerable
Предполагается, что читатель уже имеет некоторый опыт разработки программ на C#, поскольку в примерах кода ниже помимо перечисленных тем также будет рассмотрена и тема универсальных классов и методов. Если вы ещё не сталкивались с этими понятиями при разработке на C#, то могу порекомендовать официальную документацию, например, эту статью.
В контексте данной статьи прежде всего будет сделан упор на перегрузку операторов, и мы напишем консольную программу на C#, демонстрирующую возможности перегрузки операторов в языке C#.
В качестве задачи мы реализуем следующую несложную концепцию: предположим, что мы хотим в программе на C# уметь определять некоторые множества чисел, например, множество чисел, тип которых int, decimal или long и так далее. И мы хотим, чтобы эти множества поддерживали между собой операции сложения (+), вычитания (-), деления (/) и умножения (*).
Рассмотрим на примере. Пусть у нас есть 2 множества целых чисел, и имена этих множеств - A и B, а тип данных каждого элемента множества - это int:
Множество A:
Позиция элемента: | 0 | 1 | 2 | 3 | 4 | 5 |
Элемент множества в позиции: | 5 | 7 | 10 | 15 | 35 | 25 |
Как видим, в множестве всего 6 элементов, первый равен 5, и его позиция 0. Второй равен 7, и его позиция 1 и так далее.
Множество B:
Позиция элемента: | 0 | 1 | 2 | 3 | 4 | 5 |
Элемент множества в позиции: | 207 | 402 | 347 | 572 | 721 | 928 |
В этом множестве также 6 элементов, первый равен 207, и его позиция 0. Второй равен 402, и его позиция 1 и так далее.
Мы хотим сделать так, чтобы поддерживались следующие элементарные операции над множествами:
- операция A + B - должна вернуть некоторое новое множество, где каждый элемент - это сумма элементов из множеств A и B в соответствующих позициях.
- операция B - A - должна вернуть некоторое новое множество, где каждый элемент - это разность элементов из множеств B и A в соответствующих позициях. То есть из каждого элемента множества B мы будем вычитать соответствующий элемент множества A, и результат будет элементом результирующего множества.
- операция A * B - должна вернуть некоторое новое множество, где каждый элемент - это произведение элементов из множеств A и B в соответствующих позициях.
- операция B / A - должна вернуть некоторое новое множество, где каждый элемент - это результат деления элемента B на элемент множества A в соответствующих позициях.
Одним из условий, которые мы наложим на операции над нашими множествами, будет являться количество элементов в множествах: оно должно быть одинаковым. Если посмотреть на таблицы с элементами из множеств A и B, что указаны выше, понятно, почему это условие необходимо: если в одном из множеств будет больше или наоборот меньше элементов, чем в другом, то у нас "не найдется" пары для подобных элементов, чтобы произвести нужную операцию.
Также, напомню, мы хотим уметь задавать аналогичным образом множества для чисел и других типов данных, например, decimal. Т.е. главное, чтобы элементами наших множеств были числа, поддерживающие, как в арифметике, операции сложения, вычитания, умножения и деления. Т.е., например, типы данных char или bool выходят за рамки нашей задачи.
Посмотрим, как можно реализовать подобную задумку на C#.
Прежде всего нужно в среде разработки создать новый проект с типом "Консольное приложение (Майкрософт)", а в качестве языка выбрать C#. Имя проекта указываем OperatorOverloadingExample.
После создания нового проекта для нас будет также создано пространство имён с таким же именем OperatorOverloadingExample, и далее все создаваемые в рамках статьи классы будут созданы в этом пространстве имён.
Начнём мы с того, что напишем интерфейс IBoxedNumber, который будет универсальным, т.е. он будет принимать параметр T определённого типа, и будет описывать некоторое упакованное число. Упакованное число - это по сути просто число типа T, которое будет хранить реализация данного интерфейса (пару реализаций мы напишем чуть ниже). Итак, интерфейс IBoxedNumber выглядит следующим образом:
/// <summary>
/// Интерфейс представляет некоторое упакованное число заданного типа T
/// </summary>
/// <typeparam name="T">параметр числового типа, определящий тип данных упакованного числа</typeparam>
public interface IBoxedNumber<T> {
/// <summary>
/// Получить значение упакованного числа
/// </summary>
/// <returns></returns>
T GetValue();
/// <summary>
/// Установить значение для упакованного числа
/// </summary>
/// <param name="value">значение для упаковки</param>
void SetValue(T value);
/// <summary>
/// Установить значение для упакованного числа из другого экземпляра, реализующего интерфейс IBoxedNumber
/// </summary>
/// <param name="number"></param>
void SetValue(IBoxedNumber<T> number);
/// <summary>
/// Возвращает другое число, возникающее в результате сложения текущего упакованного числа
/// с заданным в параметре <paramref name="other"/>
/// </summary>
/// <param name="other">другое упакованное число, с которым нужно сложить текущее</param>
/// <returns>новое упакованное число, являющееся результатом сложения текущего с переданным в параметре <paramref name="other"/></returns>
IBoxedNumber<T> Add(IBoxedNumber<T> other);
/// <summary>
/// Возвращает другое число, возникающее в результате сложения текущего упакованного числа
/// с заданным в параметре <paramref name="other"/>
/// </summary>
/// <param name="other">другое упакованное число, которое нужно вычесть из текущего</param>
/// <returns>новое упакованное число, являющееся результатом вычитания числа, переданного в параметре <paramref name="other"/> из текущего числа</returns>
IBoxedNumber<T> Subtract(IBoxedNumber<T> other);
/// <summary>
/// Возвращает другое число, возникающее в результате умножения текущего упакованного числа
/// на заданное в параметре <paramref name="other"/>
/// </summary>
/// <param name="other">другое упакованное число, на которое нужно умножить текущее</param>
/// <returns>новое упакованное число, являющееся результатом умножения текущего числа на переданное в параметре <paramref name="other"/></returns>
IBoxedNumber<T> Multiply(IBoxedNumber<T> other);
/// <summary>
/// Возвращает другое число, возникающее в результате деления текущего упакованного числа
/// на заданное в параметре <paramref name="other"/>
/// </summary>
/// <param name="other">другое упакованное число, на которое нужно разделить текущее</param>
/// <returns>новое упакованное число, являющееся результатом деления текущего числа на переданное в параметре <paramref name="other"/></returns>
IBoxedNumber<T> Divide(IBoxedNumber<T> other);
}
Я предоставил комментарии для самого интерфейса и каждого его метода, поэтому детально описывать его методы не буду. Теперь мы определим первую реализацию интерфейса - класс с именем BoxedInteger:
/// <summary>
/// Представляет собой упакованное число типа int
/// </summary>
public class BoxedInteger : IBoxedNumber<int> {
private int val;
public BoxedInteger(int value) {
val = value;
}
public IBoxedNumber<int> Add(IBoxedNumber<int> other) {
return new BoxedInteger(val + other.GetValue());
}
public IBoxedNumber<int> Divide(IBoxedNumber<int> other) {
if (other.GetValue() == 0) {
throw new DivideByZeroException("Невозможно делить на 0, значение аргумента 'other' равно 0.");
}
return new BoxedInteger(val / other.GetValue());
}
public int GetValue() {
return val;
}
public IBoxedNumber<int> Multiply(IBoxedNumber<int> other) {
return new BoxedInteger(val * other.GetValue());
}
public void SetValue(int i) {
val = i;
}
public void SetValue(IBoxedNumber<int> number) {
val = number.GetValue();
}
public IBoxedNumber<int> Subtract(IBoxedNumber<int> other) {
return new BoxedInteger(val - other.GetValue());
}
public override string? ToString() {
return val.ToString();
}
}
Как видим, класс содержит единственное приватное поле val с типом int. Это и есть наше упакованное число типа int, и ниже мы научимся помещать подобные типы в сами множества.
Аналогичным образом зададим вторую реализацию интерфейса - класс BoxedDecimal, которое будет просто хранить упакованное число с типом decimal:
/// <summary>
/// Представляет собой упакованное число типа decimal
/// </summary>
public class BoxedDecimal : IBoxedNumber<decimal> {
private decimal val;
public BoxedDecimal(decimal value) {
val = value;
}
public IBoxedNumber<decimal> Add(IBoxedNumber<decimal> other) {
return new BoxedDecimal(val + other.GetValue());
}
public IBoxedNumber<decimal> Divide(IBoxedNumber<decimal> other) {
if (other.GetValue() == 0) {
throw new DivideByZeroException("Невозможно делить на 0, значение аргумента 'other' равно 0.");
}
return new BoxedDecimal(val / other.GetValue());
}
public decimal GetValue() {
return val;
}
public IBoxedNumber<decimal> Multiply(IBoxedNumber<decimal> other) {
return new BoxedDecimal(val * other.GetValue());
}
public void SetValue(decimal value) {
val = value;
}
public void SetValue(IBoxedNumber<decimal> number) {
val = number.GetValue();
}
public IBoxedNumber<decimal> Subtract(IBoxedNumber<decimal> other) {
return new BoxedDecimal(val - other.GetValue());
}
public override string? ToString() {
return val.ToString();
}
}
Теперь давайте напишем класс ElementEnumerator<T, V>, который будет представлять собой перечислитель, реализующий стандартный интерфейс IEnumerator. Этот перечислитель нам нужен для следующей цели: в дальнейшем мы хотели бы, чтобы наши множества могли поддерживать перечисление их элементов. Это удобно, когда мы захотим использовать экземпляр множества в цикле foreach и пробегать по его элементам. Класс принимает два параметра типа - T - это тип самих элементов, используемых перечислителем и V - тип данных для передачи его в качестве аргумента типа для интерфейса IBoxedNumber:
/// <summary>
/// Перечислитель для элементов типа IBoxedNumber<<typeparamref name="V"/>>
/// </summary>
/// <typeparam name="T">тип элементов для перечислителя</typeparam>
/// <typeparam name="V">тип данных для элемента IBoxedNumber перечислителя</typeparam>
public class ElementEnumerator<T, V> : IEnumerator<T> where T : IBoxedNumber<V> {
/// <summary>
/// Массив элементов для данного перечислителя
/// </summary>
private T[] values;
/// <summary>
/// Текущая позиция элемента
/// </summary>
private int pos = -1;
/// <summary>
/// Конструктор
/// </summary>
/// <param name="values">инициализированный массив значений для данного перечислителя</param>
/// <exception cref="InvalidOperationException"></exception>
public ElementEnumerator(T[] values) {
if (values == null) {
throw new InvalidOperationException();
}
this.values = values;
}
/// <summary>
/// Перемещается на следующую позицию и возвращает true, если не был достигнут конец массива элементов
/// </summary>
/// <returns></returns>
public bool MoveNext() {
pos++;
return values != null && (pos < values.Length);
}
/// <summary>
/// Сброс позиции для перечислителя
/// </summary>
public void Reset() {
pos = -1;
}
/// <summary>
/// Очищает ресурсы, связанные с перечислителем
/// </summary>
public void Dispose() {
}
/// <summary>
/// Возвращает текущий элемент
/// </summary>
object IEnumerator.Current {
get {
return Current;
}
}
/// <summary>
/// Возвращает текущий элемент
/// </summary>
public T Current {
get {
try {
return values[pos];
}
catch (IndexOutOfRangeException) {
throw new InvalidOperationException();
}
}
}
}
Далее давайте создадим класс пользовательского исключения, которое нам пригодится на следующем шаге, когда мы будем писать класс для самого множества.
Пусть класс нашего пользовательского исключения называется InvalidElementSetStateException:
/// <summary>
/// Исключение, которое может возникнуть при достижении
/// некорректного состояния в работе класса ElementSet
/// </summary>
[Serializable]
public class InvalidElementSetStateException : Exception {
public InvalidElementSetStateException() {
}
public InvalidElementSetStateException(string message) : base(message) {
}
public InvalidElementSetStateException(string message, Exception innerException) : base(message, innerException) {
}
}
Как видим, он наследуется от системного класса Exception. Это исключение будет выбрасываться в случаях, когда наше множество будет переходить в некоторое некорректное, неподдерживаемое состояние, и работу программы необходимо прерывать.
Итак, весь предшествующий набор классов мы подготовили, и теперь нам осталось написать класс для реализации самой концепции множества, поддерживающего требуемые для нас операции сложения, вычитания, умножения и деления. Назовём этот класс ElementSet<T, V>:
/// <summary>
/// Описывает множество упакованных чисел
/// </summary>
/// <typeparam name="T">параметр типа, который реализует интерфейс IBoxedNumber<typeparamref name="V"/></typeparam>
/// <typeparam name="V">параметр типа, который используется для упакованного числа</typeparam>
public class ElementSet<T, V> : IEnumerable<T> where T : IBoxedNumber<V> {
/// <summary>
/// Массив элементов множества
/// </summary>
private readonly T[] elements;
/// <summary>
/// Статическое поле, хранящее функцию, возвращающую начальное значение для элементов множества
/// </summary>
private static Func<T>? valueGetter;
/// <summary>
/// Возвращает массив из текущих элементов множества
/// </summary>
public T[] Values {
get {
return elements;
}
}
/// <summary>
/// Возвращает длину текущего множества
/// </summary>
public int Length {
get {
return elements == null ? 0 : elements.Length;
}
}
/// <summary>
/// Статический метод, устанавливающий функцию <paramref name="valueGetter"/>, предоставляющую начальные значения для элементов множества.
/// </summary>
/// <param name="valueGetter">ссылка на функцию, возвращающую <typeparamref name="T"/> в качестве начальных значений для элементов множества</param>
public static void SetValueGetter(Func<T> valueGetter) {
ElementSet<T, V>.valueGetter = valueGetter;
}
/// <summary>
/// Конструктор множества. Создаёт экземпляр множества заданной начальной ёмкости <paramref name="initialCapacity"/>.
/// Параметр <paramref name="initialCapacity"/> должен быть больше 0.
/// Перед созданием множества должен быть вызван статический метод SetValueGetter, который задаёт исходные значения для элементов множества.
/// </summary>
/// <param name="initialCapacity">начальная ёмкость множества при его создании</param>
/// <exception cref="InvalidElementSetStateException">выбрасывается, если параметр <paramref name="initialCapacity"/> меньше или равен 0, либо если не был вызван
/// статический метод SetValueGetter до вызова данного конструктора</exception>
public ElementSet(int initialCapacity) {
if (initialCapacity <= 0) {
throw new InvalidElementSetStateException("Начальная ёмкость 'initialCapacity' для множества должна быть больше 0.");
}
if (valueGetter == null) {
throw new InvalidElementSetStateException("Провайдер начального значения 'valueGetter' не был установлен.");
}
elements = new T[initialCapacity];
for (int i = 0; i < elements.Length; i++) {
T retrievedValue = valueGetter.Invoke();
if (retrievedValue == null) {
throw new InvalidElementSetStateException("Некорректное состояние множества при попытке инициализации. 'valueGetter' вернул null.");
}
elements[i] = retrievedValue;
}
}
/// <summary>
/// Конструктор множества. Создаёт экземпляр множества по переданному массиву элементов с типом <typeparamref name="T"/>.
/// Массив должен быть инициализирован и не может являться пустой ссылкой (null).
/// Длина массива также не может быть равна 0, он должен содержать элементы.
/// </summary>
/// <param name="initialValues">массив исходных значений для создания экземпляра множества</param>
/// <exception cref="ArgumentNullException">выбрасывается, если входной аргумент <paramref name="initialValues"/> равен null</exception>
/// <exception cref="InvalidElementSetStateException">выбрасывается, если длина входного массива <paramref name="initialValues"/> равна 0</exception>
public ElementSet(T[] initialValues) {
if (initialValues == null) {
throw new ArgumentNullException("initialValues", "Аргумент не может быть равен null!");
}
if (initialValues.Length <= 0) {
throw new InvalidElementSetStateException("Длина исходного массива 'initialValues' должна быть больше 0.");
}
elements = new T[initialValues.Length];
for (int i = 0; i < elements.Length; i++) {
elements[i] = initialValues[i];
}
}
/// <summary>
/// Индексатор для множества. Позволяет получать доступ к элементам множества по индексу
/// </summary>
/// <param name="i"></param>
/// <returns></returns>
public IBoxedNumber<V> this[int i] {
get { return elements[i]; }
set { elements[i].SetValue(value); }
}
/// <summary>
/// Выполняет заданное действие <paramref name="action"/> для всех элементов текущего множества
/// </summary>
/// <param name="action">действие, которое требуется выполнить для каждого элемента текущего множества</param>
/// <exception cref="ArgumentNullException">выбрасывается в случае, если входной аргумент равен null</exception>
public void ForEach(Action<T> action) {
if (action == null) {
throw new ArgumentNullException("action", "Выполняемое для всех элементов множества действие 'action' не может быть равно null!");
}
if (elements != null && elements.Length > 0) {
foreach (T el in elements) {
action.Invoke(el);
}
}
}
/// <summary>
/// Проверяет входные аргументы операции перед её выполнением.
/// Условия:
/// 1) оба множества не должны быть равны null. Если хотя бы одно множество равно null, будет выброшено исключение ArgumentNullException.
/// 2) длина множеств <paramref name="first"/> и <paramref name="second"/> должна быть одинаковой. Если это не так, будет выброшено исключение InvalidElementSetStateException
/// </summary>
/// <param name="first">первое множество, для которого выполняется проверка его валидности перед выполнением операции</param>
/// <param name="second">второе множество, для которого выполняется проверка его валидности перед выполнением операции</param>
/// <exception cref="ArgumentNullException">выбрасывается в случае, когда хотя бы один из аргументов <paramref name="first"/> или <paramref name="second"/> равен null</exception>
/// <exception cref="InvalidElementSetStateException">выбрасывается в случае, когда длины множеств, заданных аргументами <paramref name="first"/> и <paramref name="second"/>, различаются</exception>
private static void ValidateInputArguments(ElementSet<T, V> first, ElementSet<T, V> second) {
if (first == null) {
throw new ArgumentNullException("first", "Аргумент не может быть равен null!");
}
if (second == null) {
throw new ArgumentNullException("second", "Аргумент не может быть равен null!");
}
if (first.Length != second.Length) {
throw new InvalidElementSetStateException("Длины множеств 'first' и 'second' должны быть одинаковыми.");
}
}
/// <summary>
/// Возвращает перечислитель для данного множества
/// </summary>
/// <returns></returns>
public IEnumerator<T> GetEnumerator() {
return new ElementEnumerator<T, V>(elements);
}
/// <summary>
/// Возвращает перечислитель для данного множества
/// </summary>
/// <returns></returns>
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
/// <summary>
/// Поддержка операции сложения между двумя множествами. Перегрузка оператора +
/// </summary>
/// <param name="first">первое слагаемое. первое множество, к которому будет прибавлено второе множество <paramref name="second"/></param>
/// <param name="second">второе слагаемое. второе множество, прибавляемое к первому <paramref name="first"/></param>
/// <returns>новое множество, каждый элемент которого является результатом операции сложения между множествами <paramref name="first"/> и <paramref name="second"/></returns>
public static ElementSet<T, V> operator +(ElementSet<T, V> first, ElementSet<T, V> second) {
ValidateInputArguments(first, second);
ElementSet<T, V> resultingSet = new ElementSet<T, V>(first.Length);
for (int i = 0; i < first.Length; i++) {
resultingSet[i].SetValue(first[i].Add(second[i]));
}
return resultingSet;
}
/// <summary>
/// Поддержка операции вычитания между двумя множествами. Перегрузка оператора -
/// </summary>
/// <param name="first">первое множество, из которого вычитается второе множество <paramref name="second"/></param>
/// <param name="second">второе множество, которое вычитается из первого</param>
/// <returns>новое множество, каждый элемент которого является результатом операции вычитания между множествами <paramref name="first"/> и <paramref name="second"/></returns>
public static ElementSet<T, V> operator -(ElementSet<T, V> first, ElementSet<T, V> second) {
ValidateInputArguments(first, second);
ElementSet<T, V> resultingSet = new ElementSet<T, V>(first.Length);
for (int i = 0; i < first.Length; i++) {
resultingSet[i].SetValue(first[i].Subtract(second[i]));
}
return resultingSet;
}
/// <summary>
/// Поддержка операции умножения между двумя множествами. Перегрузка оператора *
/// </summary>
/// <param name="first">первое множество, которое умножается на второе множество <paramref name="second"/></param>
/// <param name="second">второе множество, участвующее в операции умножения</param>
/// <returns>новое множество, каждый элемент которого является результатом операции умножения между множествами <paramref name="first"/> и <paramref name="second"/></returns>
public static ElementSet<T, V> operator *(ElementSet<T, V> first, ElementSet<T, V> second) {
ValidateInputArguments(first, second);
ElementSet<T, V> resultingSet = new ElementSet<T, V>(first.Length);
for (int i = 0; i < first.Length; i++) {
resultingSet[i].SetValue(first[i].Multiply(second[i]));
}
return resultingSet;
}
/// <summary>
/// Поддержка операции деления между двумя множествами. Перегрузка оператора /
/// </summary>
/// <param name="first">первое множество, которое является делимым в операции деления на второе множество <paramref name="second"/></param>
/// <param name="second">второе множество, которое является делителем в операции деления</param>
/// <returns>возвращает результат (частное от деления первого множества <paramref name="first"/> на второе <paramref name="second"/>)</returns>
public static ElementSet<T, V> operator /(ElementSet<T, V> first, ElementSet<T, V> second) {
ValidateInputArguments(first, second);
ElementSet<T, V> resultingSet = new ElementSet<T, V>(first.Length);
for (int i = 0; i < first.Length; i++) {
resultingSet[i].SetValue(first[i].Divide(second[i]));
}
return resultingSet;
}
}
Как видите, для каждого метода я написал документацию в самом классе, поэтому думаю, что эти комментарии поясняют его внутреннее устройство. Но на некоторых моментах мы остановимся чуть подробнее:
- обратите внимание, что реализация арифметических операций над множеством выполняется посредством статических методов класса, и возвращаемый тип этих методов также ElementSet<T, V>. Методы должны быть именно статическими, это важно. Так работает перегрузка операторов. Также отмечу, что подобным образом мы могли бы перегрузить и другие доступные в языке C# операторы, которые поддерживают перегрузку операторов. Список перегружаемых и неперегружаемых операторов языка я нашёл здесь.
- также обратите внимание, что у нашего класса ElementSet<T, V> есть индексатор, и для него используется специальная конструкция с ключевым словом this. Индексатор позволит нам осуществлять доступ до элементов множества через квадратные скобки (пример того, как это делается, мы посмотрим ниже по тексту статьи).
- класс ElementSet<T, V> реализует интерфейс IEnumerable с указанием типа T, используемого для элементов множества. Это позволит нашему классу множества возвращать экземпляр ранее созданного перечислителя и использовать экземпляр множества в циклах foreach для перебора их элементов.
- в классе Elementset<T, V> мы также определили 2 параметризованных конструктора - для того, чтобы дать возможность пользователю этого класса по-разному создать экземпляр множества. Мы сможем задавать как множество из одинаковых начальных элементов, указав лишь его начальную ёмкость (конструктор с единственным параметром initalCapacity), либо же передав ему инициализированный массив значений с типом T. Вы можете задаться вопросом: "а как же конструкторы, принимающие на вход коллекции?" Например, если хочется создать множество на основе списка элементов. Ответ прост: нет предела совершенству, но для лаконичности кода, я описал лишь два примера конструктора. Остальные варианты конструкторов предлагаю реализовать читателю.
- статический метод ValidateInputArguments позволяет выполнить типовые валидационные проверки над операндами перед тем, как мы начнём пытаться выполнить ту или иную операцию над ними.
На этом код для реализации множества и операций над множествами готов, и пора перейти к программе-примеру.
Напишем в основном файле Program.cs, который был создан для нас ещё на этапе создания проекта, следующий код:
using OperatorOverloadingExample;
// ----------------------------------------------------------------------------------------
// Пример работы с множествами упакованных decimal-значений и операций над ними
// ----------------------------------------------------------------------------------------
PrintExampleTitle("Пример работы с множествами упакованных decimal-значений и операций над ними:");
IBoxedNumber<decimal>[] firstDecimalElements = new IBoxedNumber<decimal>[] {
new BoxedDecimal(0.4M),
new BoxedDecimal(10.2M),
new BoxedDecimal(7.7M)
};
IBoxedNumber<decimal>[] secondDecimalElements = new IBoxedNumber<decimal>[] {
new BoxedDecimal(50.4M),
new BoxedDecimal(20.4M),
new BoxedDecimal(652.789M)
};
ElementSet<IBoxedNumber<decimal>, decimal>.SetValueGetter(() => new BoxedDecimal(0.0M));
ElementSet<IBoxedNumber<decimal>, decimal> firstDecimalSet = new(firstDecimalElements);
ElementSet<IBoxedNumber<decimal>, decimal> secondDecimalSet = new(secondDecimalElements);
PrintElementSet("Элементы первого decimal-множества (A):", firstDecimalSet);
PrintElementSet("Элементы второго decimal-множества (B):", secondDecimalSet);
Console.WriteLine("==== Операции над decimal-множествами: ====");
PrintElementSetOperationResult("Сложение двух decimal-множеств (A + B) - это новое множество:", () => firstDecimalSet + secondDecimalSet);
PrintElementSetOperationResult("Вычитание двух decimal-множеств (B - A) - это новое множество:", () => secondDecimalSet - firstDecimalSet);
PrintElementSetOperationResult("Умножение двух decimal-множеств (A * B) - это новое множество:", () => firstDecimalSet * secondDecimalSet);
PrintElementSetOperationResult("Деление двух decimal-множеств (B / A) - это новое множество:", () => secondDecimalSet / firstDecimalSet);
// ----------------------------------------------------------------------------------------
// Пример работы с множествами упакованных int-значений и операций над ними
// ----------------------------------------------------------------------------------------
PrintExampleTitle("Пример работы с множествами упакованных int-значений и операций над ними:");
IBoxedNumber<int>[] firstIntElements = new IBoxedNumber<int>[] {
new BoxedInteger(5),
new BoxedInteger(7),
new BoxedInteger(10),
new BoxedInteger(15),
new BoxedInteger(35),
new BoxedInteger(25),
};
IBoxedNumber<int>[] secondIntElements = new IBoxedNumber<int>[] {
new BoxedInteger(207),
new BoxedInteger(402),
new BoxedInteger(347),
new BoxedInteger(572),
new BoxedInteger(721),
new BoxedInteger(928),
};
ElementSet<IBoxedNumber<int>, int>.SetValueGetter(() => new BoxedInteger(0));
ElementSet<IBoxedNumber<int>, int> firstIntSet = new(firstIntElements);
ElementSet<IBoxedNumber<int>, int> secondIntSet = new(secondIntElements);
PrintElementSet("Элементы первого int-множества (A):", firstIntSet);
PrintElementSet("Элементы второго int-множества (B):", secondIntSet);
Console.WriteLine("==== Операции над int-множествами: ====");
PrintElementSetOperationResult("Сложение двух int-множеств (A + B) - это новое множество:", () => firstIntSet + secondIntSet);
PrintElementSetOperationResult("Вычитание двух int-множеств (B - A) - это новое множество:", () => secondIntSet - firstIntSet);
PrintElementSetOperationResult("Умножение двух int-множеств (A * B) - это новое множество:", () => firstIntSet * secondIntSet);
PrintElementSetOperationResult("Деление двух int-множеств (B / A) - это новое множество:", () => secondIntSet / firstIntSet);
Console.WriteLine("==== Прямой доступ до элементов: ====");
Console.WriteLine("Первый элемент int-множества A (индекс 0): " + firstIntSet[0]);
Console.WriteLine("Пятый элемент int-множества B (индекс 4): " + secondIntSet[4]);
Console.WriteLine("Третий элемент decimal-множества A (индекс 2): " + firstDecimalSet[2]);
// Распечатает информацию о примере
static void PrintExampleTitle(string title) {
Console.WriteLine("----------------------------------------------------------------------------------------");
Console.WriteLine(title);
Console.WriteLine("----------------------------------------------------------------------------------------");
}
// Распечатает элементы множества на консоли
static void PrintElementSet<T>(string title, ElementSet<IBoxedNumber<T>, T> elementSet) {
Console.WriteLine(title);
elementSet.ForEach((el) => Console.Write(el + "; "));
Console.WriteLine();
}
// Распечатает результирующее множество после выполнения заданной операции над двумя другими множествами
static void PrintElementSetOperationResult<T>(string title, Func<ElementSet<IBoxedNumber<T>, T>> operation) {
Console.WriteLine(title);
ElementSet<IBoxedNumber<T>, T> targetSet = operation.Invoke();
foreach (IBoxedNumber<T> boxedNumber in targetSet) {
Console.Write(boxedNumber.GetValue() + "; ");
}
Console.WriteLine();
}
Можно увидеть, что вначале мы создаём два массива с именами firstDecimalElements и secondDecimalElements, причём тип обоих массивов - это нами созданный тип IBoxedNumber с указанием типа decimal в качестве типа данных для хранения упакованных чисел.
Далее мы вызываем статический метод SetValueGetter для нашего класса множества, а в аргументе передаём ему лямбда-выражение, которое возвращает новый экземпляр BoxedDecimal с упакованным внутри числом 0.0M:
ElementSet<IBoxedNumber<decimal>, decimal>.SetValueGetter(() => new BoxedDecimal(0.0M));
Это требуется при начальной инициализации множества: множеству нужно уметь понимать, чем проинициализировать его элементы на этапе первичного создания множества, например, в случае, если будет вызван лишь конструктор с передачей начальной ёмкости, а не самих исходных элементов.
Дальше мы создаём два множества, передавая в них инициализированные массивы decimal-чисел:
ElementSet<IBoxedNumber<decimal>, decimal> firstDecimalSet = new(firstDecimalElements);
ElementSet<IBoxedNumber<decimal>, decimal> secondDecimalSet = new(secondDecimalElements);
Следующими шагами мы выводим просто сами элементы множеств, а также результаты различных операций над множествами:
PrintElementSet("Элементы первого decimal-множества (A):", firstDecimalSet);
PrintElementSet("Элементы второго decimal-множества (B):", secondDecimalSet);
Console.WriteLine("==== Операции над decimal-множествами: ====");
PrintElementSetOperationResult("Сложение двух decimal-множеств (A + B) - это новое множество:", () => firstDecimalSet + secondDecimalSet);
PrintElementSetOperationResult("Вычитание двух decimal-множеств (B - A) - это новое множество:", () => secondDecimalSet - firstDecimalSet);
PrintElementSetOperationResult("Умножение двух decimal-множеств (A * B) - это новое множество:", () => firstDecimalSet * secondDecimalSet);
PrintElementSetOperationResult("Деление двух decimal-множеств (B / A) - это новое множество:", () => secondDecimalSet / firstDecimalSet);
Методы PrintElementSet и PrintElementSetOperationResult - это несложные статические методы, которые выводят сами элементы множества (PrintElementSet), а также выводят результат арифметической операции, выполненной над двумя множествами (PrintElementSetOperationResult).
Примерно то же самое мы делаем на примере операций с множествами, хранящими значения с типом данных int.
И в самом конце, но перед определением статических методов, мы видим работу индексатора, поддержанного в классе ElementSet<T, V>. За счёт наличия индексатора в классе мы можем удобно получать доступ к нужному элементу множества через квадратные скобки:
Console.WriteLine("==== Прямой доступ до элементов: ====");
Console.WriteLine("Первый элемент int-множества A (индекс 0): " + firstIntSet[0]);
Console.WriteLine("Пятый элемент int-множества B (индекс 4): " + secondIntSet[4]);
Console.WriteLine("Третий элемент decimal-множества A (индекс 2): " + firstDecimalSet[2]);
Попробуйте теперь собрать решение в среде разработки и запустить программу. На экране консоли должен быть следующий результат, как и у меня:
----------------------------------------------------------------------------------------
Пример работы с множествами упакованных decimal-значений и операций над ними:
----------------------------------------------------------------------------------------
Элементы первого decimal-множества (A):
0,4; 10,2; 7,7;
Элементы второго decimal-множества (B):
50,4; 20,4; 652,789;
==== Операции над decimal-множествами: ====
Сложение двух decimal-множеств (A + B) - это новое множество:
50,8; 30,6; 660,489;
Вычитание двух decimal-множеств (B - A) - это новое множество:
50,0; 10,2; 645,089;
Умножение двух decimal-множеств (A * B) - это новое множество:
20,16; 208,08; 5026,4753;
Деление двух decimal-множеств (B / A) - это новое множество:
126; 2; 84,77779220779220779220779221;
----------------------------------------------------------------------------------------
Пример работы с множествами упакованных int-значений и операций над ними:
----------------------------------------------------------------------------------------
Элементы первого int-множества (A):
5; 7; 10; 15; 35; 25;
Элементы второго int-множества (B):
207; 402; 347; 572; 721; 928;
==== Операции над int-множествами: ====
Сложение двух int-множеств (A + B) - это новое множество:
212; 409; 357; 587; 756; 953;
Вычитание двух int-множеств (B - A) - это новое множество:
202; 395; 337; 557; 686; 903;
Умножение двух int-множеств (A * B) - это новое множество:
1035; 2814; 3470; 8580; 25235; 23200;
Деление двух int-множеств (B / A) - это новое множество:
41; 57; 34; 38; 20; 37;
==== Прямой доступ до элементов: ====
Первый элемент int-множества A (индекс 0): 5
Пятый элемент int-множества B (индекс 4): 721
Третий элемент decimal-множества A (индекс 2): 7,7
Заключение
Как видите, за счёт поддержки возможностей перегрузки операторов в языке C# мы смогли поддержать требуемые нам операции над множествами, а при помощи индексаторов мы теперь умеем получать быстро любой элемент множества.
Напоследок я обращу внимание на то, где именно нам пригодился перечислитель, который мы поддержали для класса множества, а также тот факт, что наш класс для множества реализует интерфейс IEnumerable<T>. В методе PrintElementSetOperationResult мы смогли указать экземпляр множества с именем targetSet в цикле foreach. Если бы перечислителя не было, а также класс множества не реализовал интерфейс IEnumerable<T>, то подобный цикл foreach у нас бы просто не скомпилировался:
static void PrintElementSetOperationResult<T>(string title, Func<ElementSet<IBoxedNumber<T>, T>> operation) {
Console.WriteLine(title);
ElementSet<IBoxedNumber<T>, T> targetSet = operation.Invoke();
foreach (IBoxedNumber<T> boxedNumber in targetSet) {
Console.Write(boxedNumber.GetValue() + "; ");
}
Console.WriteLine();
}
В этом преимущество поддержки перечислителя для нашего класса ElementSet<T, V>.
В качестве упражнения я предложу читателям дополнить класс множества ElementSet<T, V>, написав в нём недостающие параметризованные конструкторы, которые могли бы принимать списки значений, а не только массивы.
Т.е. задача для читателя в том, чтобы можно было писать нечто подобное (сейчас наш класс не поддерживает создание экземпляра множества по указанному списку, и данный код не скомпилируется):
List<int> myIntList = new List<int>() { 1, 2, 3, 4, 5 };
ElementSet<IBoxedNumber<int>, int> mySetFromList = new(myIntList);
Также предлагаю читателю поупражняться и расширить возможности создаваемых множеств: попробуйте создать другие-классы наследники для интерфейса IBoxedNumber<T>. Например, помимо созданных в этой статье BoxedInteger и BoxedDecimal создайте свою реализацию BoxedLong, которая бы хранила упакованное число с типом long. И ещё, что можно оптимизировать в рассмотренном классе множества - это внутреннее содержимое его методов для перегрузки операторов. Если присмотреться к ним внимательно, то они выглядят уж очень однотипно и содержат похожий код. Разница лишь во внутренней операции, которая выполняется с элементами двух входных множеств (операндов операции), - это один из методов Add, Subtract, Divide и Multiply. Реализацию проверки валидности входных операндов, а также цикл по элементам множества можно было бы вынести в какой-то один метод. Предлагаю также выполнить подобную оптимизацию и рефакторинг кода читателям.
Напоследок прикладываю ссылку на полностью готовый пример решения, разработанного в рамках этой статьи:
https://allineed.ru/our-products/download/4-allineed-ru-examples/27-csharp-elementset-example-demo
Спасибо за внимание и удачи.