В своих программах часто приходится работать с вещественными числами. Для их хранения в c# предусмотрены такие вещественные типы, как float, double и decimal. Однако не для кого не секрет, что все вещественные числа в компьютере имеют ограниченную точность. Например, если объявить целую переменную, присвоить ей значение равное двум, а затем извлечь квадратный корень и результат возвести в квадрат, то мы получим число, несколько отличающееся от двух. Чтобы не быть голословным, напишем небольшую программу.
static void Main(string[] args) { int a = 2; double result = Math.Pow(Math.Sqrt(a), 2); Console.WriteLine(result); }
- Результат в консоли:
- Результат в отладчике:
- Класс Fraction
- Поля
- Конструкторы
- Арифметические операции над дробями
- Поиск наименьшего общего знаменателя
- Метод нахождения наибольшего общего делителя
- Метод нахождения наименьшего общего кратного
- Перегрузка арифметических операторов
- Операции сравнения
- Перегрузка операторов «==» и «!=»
- Перегрузка операторов «>», «<«, «>=», «<=»
Результат в консоли:
#image.jpg
Результат в отладчике:
Результаты показывают, хоть в консоль и вывелось точное значение (из-за особенностей работы метода Console.WriteLine), в действительности оно не такое уж и точное.
Таким образом, мы имеем некоторую погрешность, которая зачастую только мешает, поэтому я предлагаю написать новый тип (а точнее класс) «Дробь», в котором будут реализованы все (ну или почти) действия с дробями. Итак начнем!
Класс Fraction
Поля
В классе будет три поля:
- числитель
- знаменатель
- знак
public sealed class Fraction { private int numerator; // Числитель private int denominator; // Знаменатель private int sign; // Знак }
После того, как поля описаны, переходим к конструктору.
Конструкторы
Я предлагаю дать возможность создавать дроби двумя способами:
- явно указав числитель и знаменатель
- указав лишь один числитель, подразумевая, что в знаменателе будет единица
Как Вы успели заметить, ни в одном конструкторе не указывается знак. Сделал я это преднамеренно, с той целью, чтобы во время создания дроби не возникало вопросов насчет знаков числителя и знаменателя. Иными словами, знак будет определяться знаком произведения числителя на знаменатель.
public Fraction(int numerator, int denominator) { if (denominator == 0) { throw new DivideByZeroException("В знаменателе не может быть нуля"); } this.numerator = Math.Abs(numerator); this.denominator = Math.Abs(denominator); if (numerator * denominator < 0) { this.sign = -1; } else { this.sign = 1; } } // Вызов первого конструктора со знаменателем равным единице public Fraction(int number) : this(number, 1) { }
Арифметические операции над дробями
Мы должны написать класс так, чтобы операции над дробями можно было выполнять так же, как и с обычными числами, то есть нам необходимо перегрузить операторы. Для начала мы реализуем сами методы операций, а затем обернем их методами, переопределяющие операторы.
Поиск наименьшего общего знаменателя
Для выполнения операций сложения и вычитания дробей нам понадобится приводить их к общему знаменателю, причем к наименьшему. Наименьшим общим знаменателем двух дробей является наименьшее общее кратное (НОК) их знаменателей. В свою очередь поиск НОК сводится к поиску наибольшего общего делителя (НОД).
Обобщив все вышесказанное, получаем, что нам нужно реализовать два метода:
- возвращающий НОК двух чисел
- возвращающий НОД двух чисел
Метод нахождения наибольшего общего делителя
// Возвращает наибольший общий делитель (Алгоритм Евклида) private static int getGreatestCommonDivisor(int a, int b) { while (b != 0) { int temp = b; b = a % b; a = temp; } return a; }
Метод нахождения наименьшего общего кратного
// Возвращает наименьшее общее кратное private static int getLeastCommonMultiple(int a, int b) { // В формуле опушен модуль, так как в классе // числитель всегда неотрицательный, а знаменатель -- положительный // ... // Деление здесь -- челочисленное, что не искажает результат, так как // числитель и знаменатель делятся на свои делители, // т.е. при делении не будет остатка return a * b / getGreatestCommonDivisor(a, b); }
После написания этих двух методов, можно приступать к написанию методов сложения и вычитания. Однако ввиду того, что действия при этих операциях в основном схожи, то я предлагаю написать один метод, который будет возвращать результат сложения или вычитания дробей, в зависимости от того, какую функцию мы передадим.
// Метод создан для устранения повторяющегося кода в методах сложения и вычитания дробей. // Возвращает дробь, которая является результатом сложения или вычитания дробей a и b, // В зависимости от того, какая операция передана в параметр operation. // P.S. использовать только для сложения и вычитания private static Fraction performOperation(Fraction a, Fraction b, Func<int, int, int> operation) { // Наименьшее общее кратное знаменателей int leastCommonMultiple = getLeastCommonMultiple(a.denominator, b.denominator); // Дополнительный множитель к первой дроби int additionalMultiplierFirst = leastCommonMultiple / a.denominator; // Дополнительный множитель ко второй дроби int additionalMultiplierSecond = leastCommonMultiple / b.denominator; // Результат операции int operationResult = operation(a.numerator * additionalMultiplierFirst * a.sign, b.numerator * additionalMultiplierSecond * b.sign); return new Fraction(operationResult, a.denominator * additionalMultiplierFirst); }
Перегрузка арифметических операторов
// Перегрузка оператора "+" для случая сложения двух дробей public static Fraction operator +(Fraction a, Fraction b) { return performOperation(a, b, (int x, int y) => x + y); } // Перегрузка оператора "+" для случая сложения дроби с числом public static Fraction operator +(Fraction a, int b) { return a + new Fraction(b); } // Перегрузка оператора "+" для случая сложения числа с дробью public static Fraction operator +(int a, Fraction b) { return b + a; } // Перегрузка оператора "-" для случая вычитания двух дробей public static Fraction operator -(Fraction a, Fraction b) { return performOperation(a, b, (int x, int y) => x - y); } // Перегрузка оператора "-" для случая вычитания из дроби числа public static Fraction operator -(Fraction a, int b) { return a - new Fraction(b); } // Перегрузка оператора "-" для случая вычитания из числа дроби public static Fraction operator -(int a, Fraction b) { return b - a; } // Перегрузка оператора "*" для случая произведения двух дробей public static Fraction operator *(Fraction a, Fraction b) { return new Fraction(a.numerator * a.sign * b.numerator * b.sign, a.denominator * b.denominator); } // Перегрузка оператора "*" для случая произведения дроби и числа public static Fraction operator *(Fraction a, int b) { return a * new Fraction(b); } // Перегрузка оператора "*" для случая произведения числа и дроби public static Fraction operator *(int a, Fraction b) { return b * a; } // Перегрузка оператора "/" для случая деления двух дробей public static Fraction operator /(Fraction a, Fraction b) { return a * b.GetReverse(); } // Перегрузка оператора "/" для случая деления дроби на число public static Fraction operator /(Fraction a, int b) { return a / new Fraction(b); } // Перегрузка оператора "/" для случая деления числа на дробь public static Fraction operator /(int a, Fraction b) { return new Fraction(a) / b; } // Перегрузка оператора "унарный минус" public static Fraction operator -(Fraction a) { return a.GetWithChangedSign(); } // Перегрузка оператора "++" public static Fraction operator ++(Fraction a) { return a + 1; } // Перегрузка оператора "--" public static Fraction operator --(Fraction a) { return a - 1; }
Как Вы наверняка успели заметить, в только что написанном коде используются два неописанных ранее методов:
- GetReverse — возвращает дробь, обратную данной
- GetWithChangedSign — возвращает дробь с противоположным знаком
// Возвращает дробь, обратную данной private Fraction GetReverse() { return new Fraction(this.denominator * this.sign, this.numerator); } // Возвращает дробь с противоположным знаком private Fraction GetWithChangedSign() { return new Fraction(-this.numerator * this.sign, this.denominator); }
Операции сравнения
Для перегрузки операторов сравнения необходимо переопределить методы Equals и GetHashCode. Первый будет возвращать значение истины, если заданный объект равен текущему. Второй же метод возвращает хэш-код для текущего объекта.
// Мой метод Equals public bool Equals(Fraction that) { Fraction a = this.Reduce(); Fraction b = that.Reduce(); return a.numerator == b.numerator && a.denominator == b.denominator && a.sign == b.sign; } // Переопределение метода Equals public override bool Equals(object obj) { bool result = false; if (obj is Fraction) { result = this.Equals(obj as Fraction); } return result; } // Переопределение метода GetHashCode public override int GetHashCode() { return this.sign * (this.numerator * this.numerator + this.denominator * this.denominator); }
Перегрузка операторов «==» и «!=»
// Перегрузка оператора "Равенство" для двух дробей public static bool operator ==(Fraction a, Fraction b) { // Приведение к Object необходимо для того, чтобы // можно было сравнивать дроби с null. // Обычное сравнение a.Equals(b) в данном случае не подходит, // так как если a есть null, то у него нет метода Equals, // следовательно будет выдано исключение, а если // b окажется равным null, то исключение будет вызвано в // методе this.Equals Object aAsObj = a as Object; Object bAsObj = b as Object; if (aAsObj == null || bAsObj == null) { return aAsObj == bAsObj; } return a.Equals(b); } // Перегрузка оператора "Равенство" для дроби и числа public static bool operator ==(Fraction a, int b) { return a == new Fraction(b); } // Перегрузка оператора "Равенство" для числа и дроби public static bool operator ==(int a, Fraction b) { return new Fraction(a) == b; } // Перегрузка оператора "Неравенство" для двух дробей public static bool operator !=(Fraction a, Fraction b) { return !(a == b); } // Перегрузка оператора "Неравенство" для дроби и числа public static bool operator !=(Fraction a, int b) { return a != new Fraction(b); } // Перегрузка оператора "Неравенство" для числа и дроби public static bool operator !=(int a, Fraction b) { return new Fraction(a) != b; }
Хорошо. Сравнивать на равенство дроби уже умеем. Теперь сделаем так, чтобы можно было выяснить, какая из двух дробей больше. Как и ранее, пишем вспомогательный метод.
// Метод сравнения двух дробей // Возвращает 0, если дроби равны // 1, если this больше that // -1, если this меньше that private int CompareTo(Fraction that) { if (this.Equals(that)) { return 0; } Fraction a = this.Reduce(); Fraction b = that.Reduce(); if (a.numerator * a.sign * b.denominator > b.numerator * b.sign * a.denominator) { return 1; } return -1; }
Перегрузка операторов «>», «<«, «>=», «<=»
// Перегрузка оператора ">" для двух дробей public static bool operator >(Fraction a, Fraction b) { return a.CompareTo(b) > 0; } // Перегрузка оператора ">" для дроби и числа public static bool operator >(Fraction a, int b) { return a > new Fraction(b); } // Перегрузка оператора ">" для числа и дроби public static bool operator >(int a, Fraction b) { return new Fraction(a) > b; } // Перегрузка оператора "<" для двух дробей public static bool operator <(Fraction a, Fraction b) { return a.CompareTo(b) < 0; } // Перегрузка оператора "<" для дроби и числа public static bool operator <(Fraction a, int b) { return a < new Fraction(b); } // Перегрузка оператора "<" для числа и дроби public static bool operator <(int a, Fraction b) { return new Fraction(a) < b; } // Перегрузка оператора ">=" для двух дробей public static bool operator >=(Fraction a, Fraction b) { return a.CompareTo(b) >= 0; } // Перегрузка оператора ">=" для дроби и числа public static bool operator >=(Fraction a, int b) { return a >= new Fraction(b); } // Перегрузка оператора ">=" для числа и дроби public static bool operator >=(int a, Fraction b) { return new Fraction(a) >= b; } // Перегрузка оператора "<=" для двух дробей public static bool operator <=(Fraction a, Fraction b) { return a.CompareTo(b) <= 0; } // Перегрузка оператора "<=" для дроби и числа public static bool operator <=(Fraction a, int b) { return a <= new Fraction(b); } // Перегрузка оператора "<=" для числа и дроби public static bool operator <=(int a, Fraction b) { return new Fraction(a) <= b; }
По сути на этом можно и остановиться, но я добавлю метод Reduce, который будет возвращать максимально сокращенную дробь, и переопределю метод ToString. Нам же нужен красивый вывод
// Возвращает сокращенную дробь public Fraction Reduce() { Fraction result = this; int greatestCommonDivisor = getGreatestCommonDivisor(this.numerator, this.denominator); result.numerator /= greatestCommonDivisor; result.denominator /= greatestCommonDivisor; return result; } // Переопределение метода ToString public override string ToString() { if (this.numerator == 0) { return "0"; } string result; if (this.sign < 0) { result = "-"; } else { result = ""; } if (this.numerator == this.denominator) { return result + "1"; } if (this.denominator == 1) { return result + this.numerator; } return result + this.numerator + "/" + this.denominator; }
Вот и все. Класс готов. Ниже представлен скрин работы программы, в которой тестируются все публичные методы.