Реализация класса «Дробь» на C#

В своих программах часто приходится работать с вещественными числами. Для их хранения в c# предусмотрены такие вещественные типы, как float, double и decimal. Однако не для кого не секрет, что все вещественные числа в компьютере имеют ограниченную точность. Например, если объявить целую переменную, присвоить ей значение равное двум, а затем извлечь квадратный корень и результат возвести в квадрат, то мы получим число, несколько отличающееся от двух. Чтобы не быть голословным, напишем небольшую программу.

		static void Main(string[] args)
{
int a = 2;
double result = Math.Pow(Math.Sqrt(a), 2);
Console.WriteLine(result);
}

Результат в консоли:

#image.jpg

Результат в отладчике:

Реализация класса "Дробь" на C# (С#)

Результаты показывают, хоть в консоль и вывелось точное значение (из-за особенностей работы метода Console.WriteLine), в действительности оно не такое уж и точное.

Таким образом, мы имеем некоторую погрешность, которая зачастую только мешает, поэтому я предлагаю написать новый тип (а точнее класс) "Дробь", в котором будут реализованы все (ну или почти) действия с дробями. Итак начнем!

Класс Fraction

Поля

В классе будет три поля:

  • числитель
  • знаменатель
  • знак
public sealed class Fraction
{
private int numerator;				// Числитель
private int denominator;			// Знаменатель
private int sign;					// Знак
}

После того, как поля описаны, переходим к конструктору.

Конструкторы

Я предлагаю дать возможность создавать дроби двумя способами:

  1. явно указав числитель и знаменатель
  2. указав лишь один числитель, подразумевая, что в знаменателе будет единица

Как Вы успели заметить, ни в одном конструкторе не указывается знак. Сделал я это преднамеренно, с той целью, чтобы во время создания дроби не возникало вопросов насчет знаков числителя и знаменателя. Иными словами, знак будет определяться знаком произведения числителя на знаменатель.

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. Нам же нужен красивый вывод Реализация класса "Дробь" на C# (С#)

// Возвращает сокращенную дробь
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;
}

Вот и все. Класс готов. Ниже представлен скрин работы программы, в которой тестируются все публичные методы.

#image.jpg

1 отзыв на “Реализация класса «Дробь» на C#

  1. Артем Овечко

    Большое спасибо за выложенный код - я уже готовился писать все с нуля, а нужно еще писать лабу по реализации сиплекс-метода решений ЗЛП. А так сэкономил кучу времени.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *