Создано при помощи нейросети Kandinsky

Пример на C# с перегрузкой операторов, индексатором и реализацией IEnumerable для операций над множествами

User Rating: 0 / 5

Изображение к статье создано при помощи нейросети Kandinsky

Всем привет.

В этой статье мы постараемся затронуть следующие темы, относящиеся к разработке программ на C#:

  • перегрузка операторов (на примере перегрузки операторов +, -, /, *)
  • индексаторы (не основная тема статьи, но индексатор будет применён в примере рассматриваемого кода)
  • перечислители и реализация интерфейса IEnumerable

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

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

В качестве задачи мы реализуем следующую несложную концепцию: предположим, что мы хотим в программе на C# уметь определять некоторые множества чисел, например, множество чисел, тип которых intdecimal или 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&lt;<typeparamref name="V"/>&gt;
    /// </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

Спасибо за внимание и удачи.

Яндекс.Метрика