Приватные члены и еще немного о работе конструкторов

В предыдущей версии Rational, мы использовали простую схему инициализации числителя и знаменателя. Как результат, числитель и знаменатель запросто могли быть больше, чем это необходимо. К примеру, 66/42 вполне могут быть нормализованны в эквивалентную форму 11/7, но наш спроектированный конструктор это сделать не мог:

 new Rational(66,42)
 res0: Rational = 66/42

Для нормализации нам необходимо разделить числитель и знаменатель на их наибольший общий делитель(НОД). В приведенном выше примере, наибольший общий делитель число 6. Другими словами, наибольший общий делитель двух чисел это наибольшее целое, на который делятся без остатка оба числа. Разделение обоих числителя и знаменателя на 6 дает нам сокращенную форму 11/7.

Нахождение НОД известная и популярная в арифметике операция. Наибольший общий делитель двух чисел существует и однозначно определён, если хотя бы одно из чисел m или n не ноль. В нашем случае это условие обязательно выполняется(поскольку знаменатель не может быть нулем), значит, НОД есть всегда. Существует несколько способов найти НОД. Наиболее популярным является алгоритм Евклида, хотя справедливости ради стоит от метить, что алгоритм был известен еще по «Топикам» Аристотеля.

В «Началах» Евклида он описан дважды, в VII книге для нахождения наибольшего общего делителя двух натуральных чисел и в X книге для нахождения наибольшей общей меры двух однородных величин. В обоих случаях дано геометрическое описание алгоритма, для нахождения «общей меры» двух отрезков.

В нашем же случае, измененная версия Rational становится такой:

 class Rational(n: Int, d: Int) {
   require(d != 0)
   private val g = gcd(n.abs, d.abs)

   val num: Int = n / g
   val denum: Int = d / g


   def this(n: Int) = this (n, 1)

   override def toString = num + "/" + denum

   def add(that: Rational): Rational =
     new Rational(
       num * that.denum + that.num * denum,
       denum * that.denum
       )

   def lessThan(that: Rational): Boolean =
     this.num * that.denum < that.num * this.denum

   def max(that: Rational): Rational =
     if (lessThan(that)) that else this

   private def gcd(a: Int, b: Int): Int =
     if (b == 0) a else gcd(b, a % b)
 }

В этой версии Rational мы добавили приватное поле — g, и изменили процедуру инициализации числителя и знаменателя. Поскольку g приватное поле, доступ к нему можно получить только из методов класса Rational. Нет ни одного способа получить доступ к этому полю снаружи класса. Эта техника называется «инкапсуляция», когда детали реализации класса скрыты внутри, и не касаются прикладного программиста.

Как видно в примере, мы также определили приватный метод, который находит НОД. Он также может быть вызван только из методов класса Rational. В нашем случае он отвечает за инициализацию правильным значением приватного поля g. Ключевое слово private называется «модификатором доступа». Scala имеет несколько модификаторов доступа, их подробное рассмотрение выходит за рамки этой темы. “Модификатор доступа по умолчанию” в Scala — public. Он никогда не пишется явно. Можно сказать, что это имплицидный модификатор доступа. Если Вы не указываете модификатор доступа, Scala всегда подразумевает, что это поле или метод для свободного доступа. Если Вы используете private, таким образом Вы говорите Scala, что Вы хотите использовать реализацию полей, методов или других конструкций (например, внутренних классов) только изнутри проектируемого класса.

В нашем случае мы разделили реализацию метода gcd ( такие приватные методы еще называют helper method) с инициализацией числителя и знаменателя с точки зрения «оптимизации» кода. Ведь если бы мы вызывали метод напрямую, без промежуточного члена g, нам пришлось бы производить вычисления дважды.

Предварительная подготовка значений числителя и знаменателя в виде приведения их к положительному значению помогает нам гарантировать положительное значение НОД.

Обратите внимание, что Scala в процессе выполнения операций в конструкторе придерживается алгоритмического подхода, а именно последовательности выполняемых операций. Это означает, что если Вы переставите инициализацию g ПОСЛЕ инициализации num и denum, то компилятор Scala НОРМАЛЬНО произведет компиляцию, но в процессе исполнения выдаст деление на ноль.

С точки зрения компилятора поле будет объявлено в момент создания экземпляра и к нему можно обратиться сразу, но оно не было инициализированно значением, которое Вы подразумевали в своем синтаксисе. Это очень важная особенность Scala, позволяющая избежать неадекватного толкования циклических использований полей и методов. Единственным, что не принадлежит конструктору по умолчанию это описание всех методов в классе.

Таким образом, с точки зрения Scala, описание класса состоит из конструктора по умолчанию, который работает последовательно, от операции к операции, совмещенного с описанием и инициализацией полей и методов. В этом случае, мы понимаем, что описание метода gcd в принципе может находится в любом месте класса, но не описание поля g. Эта особенность описания классов в Scala важна до чрезвычайности, и вполне логична.

 scala> new Rational(66,42)
 res0: Rational = 11/7
Опубликовано 19 октября 2010 г.

Лахтин Станислав Евгеньевич

Ваш комментарий