Переделка классов Ruby для Crystal — КиберПедия 

Кормораздатчик мобильный электрифицированный: схема и процесс работы устройства...

Архитектура электронного правительства: Единая архитектура – это методологический подход при создании системы управления государства, который строится...

Переделка классов Ruby для Crystal

2022-09-29 25
Переделка классов Ruby для Crystal 0.00 из 5.00 0 оценок
Заказать работу

Если вы разработчик Ruby, преобразование классов — как раз та
область, где различия между языками особенно заметны. Прогулка по мостику от Ruby до Crystal с разъяснением сообщений об ошибках, встреченных по пути, позволяет подробнее разглядеть, как подход Кристалла к типам данных меняет ход дела. (Если вы не являетесь разработчиком Ruby, то можете свободно переходить к следующему разделу.)

Вот незатейливый класс языка Ruby:

classes_and_structs/mineral.rb

class Mineral

attr_reader:name,:hardness,:crystal_struct

 

def initialize(name, hardness, crystal_struct)

@name = name

@hardness = hardness

@crystal_struct = crystal_struct

 

   end

   end

def mineral_with_crystal_struct(crstruct, minerals)

minerals.find { |m| m.crystal_struct == crstruct }

end

 

def longest_name(minerals)

minerals.map { |m| m.name }.max_by { |name| name.size }

end

Теперь давайте добавим немного проверочных данных и посмотрим,
как это сработает:

classes_and_structs/mineral.rb

 minerals = [

Mineral.new("gold", 1, 'cubic'),

Mineral.new("topaz", 8, 'orthorombic'),

Mineral.new("apatite", 5, 'hexagonal'),

Mineral.new("wolframite", 4.5, 'monoclinic'),

Mineral.new("calcite", 3, 'trigonal'),

Mineral.new("diamond", 10, 'cubic'),

]

min = mineral_with_crystal_struct('hexagonal', minerals)

puts "#{min.crystal_struct} - #{min.name} - #{min.hardness}"

  # => hexagonal - apatite - 5

 

puts longest_name(minerals)

  # => wolframite

Запуск этой программы в терминале с помощью команды

$ ruby mineral.rb демонстрирует следующее:

  apatite - hexagonal - 5

wolframite

 

Вроде бы все нормально, но что, если в вашем массиве нет минерала
с указанной кристаллической структурой?

classes_and_structs/mineral.rb

# Runtime error:

min = mineral_with_crystal_struct('triclinic', minerals)

puts "#{min.crystal_struct} - #{min.name} - #{min.hardness}"

# 3.5_mineral.rb:39:in `<main>': undefined method  'crystal_struct'

 # for nil:NilClass (NoMethodError)

 

Ruby "выстреливает" ошибкой времени выполнения, потому что метод find возвращает nil, когда не может найти совпадение. Пренебрежение проверкой возвращаемого нулевого значения (nil) не всегда имеет столь очевидные последствия.

Crystal предпочел бы избавить вас от этих проблем. Переделывая исходный код Ruby для языка Crystal, вы столкнетесь с множеством ошибок. Не волнуйтесь — это всё во благо, и вы научитесь ценить
характер Кристалла!

Теперь посмотрим, как Crystal справится с этим: сохраните наш код
Ruby в файле "mineral.cr" и смонтируйте командой $ crystal mineral.cr. Синтаксис Crystal — это не совсем синтаксис Ruby, поэтому вы сразу
же столкнетесь с ошибкой:

<= Syntax error in mineral.cr:20: unterminated char literal,

use double quotes for strings

Mineral.new("gold", 1, 'cubic'),

^

 

ЯП Руби допускает как одиночные, так и двойные кавычки для строк,
а Crystal — нет! Вам нужно будет заменить все строки с одинарной кавычкой на строки с двойной кавычкой и затем скомпилировать снова. Следующая ошибка сообщает о другом различии:

 

Error in  mineral.cr:2: undefined method 'attr_reader'

attr_reader:name,:hardness,:crystal_struct

^~~~~~~~~~~

 

Crystal использует ключевое слово getter (фактически это макрос, см. §"Кристаллизуйте ваш код с помощью Макросов") вместо "attr_reader", setter вместо "attr_writer", и property вместо "attr_accessor". Есть несколько других незначительных различий между Руби и Кристаллом,
но не так уж много. Подробнее написано в Приложении 2 "Порти-
рование кода Ruby на Crystal".

Вы можете назначить имя для свойства. Это не обязательно должен
быть символ. Замена attr_reader на "getter" и ещё одна (третья) компиляция выдает еще одну ошибку, которая теперь указывает нам
на существенное различие с Руби. Это сообщение является подробным
и многословным. Не волнуйтесь — мы покажем его лишь один раз:

<= 

Error in  mineral.cr:20:

instantiating 'Mineral:Class#new(String, Int32, String)'

Mineral.new("gold", 1, "cubic"),

^~~

in mineral.cr:5:

Не удается определить тип переменной экземпляра "@ name" для Mineral.

Тип переменной экземпляра, если он явно не прописан с помощью
"@ name: Type", будет определен в зависимости от присваиваемых ей
(на протяжении всей программы) значений.

 

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

1. " @ name = 1 " (или другие символьные константы), подходящий
тип подбирается путем логичных умозаключений.

2. " @ name = Type.new ", тип явно задается словом перед точкой (Название.new).

3. " @ name = Type.method ", где метод ("method") возвращает примечание с названием типа, из него выводится тип.

4. '@ name = arg', где 'arg' — это аргумент метода с закреплен-
ным типом ('Type'), здесь тип однозначно зафиксирован заранее.

5. '@ name = arg', где 'arg' является аргументом метода с неопределен-ным значением, тип выводится с использованием вышеупомянутых способов 1, 2 и 3.

6. "@ name = неИнициализированный Тип", где тип явно указан
последним словом в кавычках (Type).

7. "@ name = LibSome.func", где "LibSome" — это библиотека, а тип логически вытекает из func.

8. "LibSome.func (out @ name)", где "LibSome" является библиотекой, а тип выводится из аргумента функции func.

Все прочие присвоения значений (не совпадающие с этими 8 фор-матами) не влияют на тип переменной экземпляра.

 

Can't infer the type of instance variable '@name' of  Mineral

(Не удается определить тип переменной экземпляра '@name' для "Mineral")

@name = name

^~~~~

 

Здесь Crystal не может достоверно выяснить тип переменной эк-
земпляра @name, и ему нужно, чтобы мы указали его. Мы указали
@name = name, и этого недостаточно. Можно исправить это, объявив
тип @name следующим образом: "getter name: String". Вы должны
сделать это для других свойств таким же манером:

  getter name: String

  getter hardness: Int32

  getter crystal_struct: String

 

Всё же появляется еще одна ошибка времени компиляции:

 <= Error in  mineral.cr:25: instantiating

      'Mineral:Class#new(String, Float64, String)'

                     Mineral.new("wolframite", 4.5, "monoclinic"),

^~~

in mineral.cr:8: instance variable '@hardness' of Mineral

  must be Int32, not Float64

@hardness = hardness

^~~~~~~~~

Итак, вольфрамит имеет твердость 4,5 и, как видите, это не целочисленное значение. Вы можете заменить объявление "@hardness"
на "getter hardness: Float64", но затем компилятор жалуется, что
твердость для других минералов все еще Int32. Лучше преобразовать единицы твердости в числа с плавающей точкой.

Другой прогон компиляции показывает нам новую ошибку среди данных структурного шестигранника:

 

 <= Error in  mineral.cr:31: undefined method 'crystal_struct'

            for Nil (compile-time type is (Mineral | Nil))

puts "#{min.crystal_struct} - #{min.name} - #{min.hardness}"

^~~~

Rerun with --error-trace  to show a complete error trace

 

Это напомнит вам о нулевой (nil) ошибке во время исполнения кода, которую вы уже получали от Ruby. Crystal дает вам больше подробной информации, когда вы компилируете следующим образом:

$ crystal mineral.cr --error-trace

  # //ошибка, также называемая Nil-trace:

Nil trace:

mineral.cr:30

min = mineral_with_crystal_struct("hexagonal", minerals)

^~~

mineral.cr:30

min = mineral_with_crystal_struct("hexagonal", minerals)

^~~~~~~~~~~~~~~~~~~~~~~~~~~

mineral.cr:13

def mineral_with_crystal_struct(crstruct, minerals)

^~~~~~~~~~~~~~~~~~~~~~~~~~~

mineral.cr:14

minerals.find { |m| m.crystal_struct == crstruct }

^~~~

/opt/crystal/src/enumerable.cr:409

def find(if_none = nil)

/opt/crystal/src/enumerable.cr:410

each do |elem|

^

/opt/crystal/src/enumerable.cr:413

if_none

^~~~~~~

/opt/crystal/src/enumerable.cr:409

def find(if_none = nil)

^

Трассировка Nil откатывается через исходный код (начиная от метки неопределенного метода) до того места, где появился тип-нарушитель.
И это произошло в методе find, в файле "enumerable.cr":

def find(if_none = nil)

Этот код показывает, что find возвращает значение "nil" в качестве значения по умолчанию на тот случай, когда ничего не найдено.

 

▪ Кристалл написан на ЯП Crystal ▪

 

Кстати, стандартная библиотека Кристалла целиком реализована возможностями самого Crystal. Если угодно, вы можете посмотреть как Crystal кодирует все методы Enumerable, на сайте

https://github.com/crystal-lang/crystal/blob/master/src/enumerable.cr. Дерзайте, ученье подождет.

 

Компилятор сигнализирует о возможном возникновении исключения
с нулевой ссылкой, но без запуска программного кода. Это позволяет избежать жалоб клиентов на жуткие ошибки (ну, по крайней мере, на ошибки такого рода). Это характерная особенность Crystal.

Сообщение об ошибке также подсказывает, что типом времени компиляции является "Mineral | Nil". Это тип объединения (см. § Использование массивов): обычно переменная min ссылается на
объект Mineral, но если в совокупности ваших данных нет минерала, соответствующего этой специфической кристаллической структуре, min равно нулевому (nil) значению. Crystal проверяет, доступен ли каждый метод, вызываемый с помощью "min", для всех типов внутри объединенного типа. Если нет — бац: ошибка.

Вы можете исправить ее тем же путем, что используется в ЯП Ruby:

  if min

puts "#{min.crystal_struct} - #{min.name} - #{min.hardness}"

else

puts "No mineral found with this crystal structure!"

end

 

Если это выглядит странно для вас, пересмотрите нашу дискуссию об истинности и фальши в разделе "Управление ходом исполнения". Когда min равно nil, это соответствует false и первая ветвь конструкции if не выполняется. Но, оказавшись внутри первой ветви, мы понимаем, что
тип min это Mineral, а не специфический тип nil. В другой ветви всё наоборот, типом будет nil, а не "Mineral".

Наконец, все работает и мы получим тот же результат, что и в Ruby:

apatite - hexagonal - 5

  wolframite

Теперь давайте применим замечательный краткий синтаксис кода (заимствованный из CoffeyScript).

Вместо:

  def initialize(name, hardness, crystal_struct)

@name = name

@hardness = hardness

@crystal_struct = crystal_struct

end

вы можете писать:

 def initialize(@name, @hardness, @crystal_struct)

end

 

Переменные экземпляра обретают свои значения непосредственно при создании объекта, @name принимает “gold”, @hardness получает значение 1 и так далее:

Mineral.new("gold", 1.0, "cubic")

 

Можно также прописать свойства в методе initialize вместо использования формулы "getter", например:

def initialize(@name: String, @hardness: Float64, @crystal_struct: String)

Это не так уж плохо, верно ведь?

Ниже полностью приведен код Crystal для нашей программы:

classes_and_structs/mineral.cr

class Mineral

getter name: String

getter hardness: Float64

getter crystal_struct: String

def initialize(@name, @hardness, @crystal_struct)

end

end

def mineral_with_crystal_struct(crstruct, minerals)

minerals.find { |m| m.crystal_struct == crstruct }

end

 

def longest_name(minerals)

minerals.map { |m| m.name }.max_by { |name| name.size }

end

 

minerals = [

Mineral.new("gold", 1.0, "cubic"),

Mineral.new("topaz", 8.0, "orthorombic"),

Mineral.new("apatite", 5.0, "hexagonal"),

Mineral.new("wolframite", 4.5, "monoclinic"),

Mineral.new("calcite", 3.0, "trigonal"),

Mineral.new("diamond", 10.0, "cubic"),

]

min = mineral_with_crystal_struct("hexagonal", minerals)

if min

puts "#{min.crystal_struct} - #{min.name} - #{min.hardness}"

else

puts "No mineral found with this crystal structure!"

end

# => hexagonal - apatite - 5

min = mineral_with_crystal_struct("triclinic", minerals)

if min

puts "#{min.crystal_struct} - #{min.name} - #{min.hardness}"

else

puts "No mineral found with this crystal structure!"

end

 # => "No mineral found with this crystal structure!"

 

puts longest_name(minerals)

# => wolframite

Crystal уверенно ведет вас и ожидает более качественного и скрупулезного кодирования, чем Ruby. При использовании языка Ruby необходимо больше полагаться на совершенство вашей тестовой подсистемы, внедренной в исходный код, чтобы обнаруживать и предотвращать наиболее вероятные ошибки.

 

Структурирование класса

В предыдущем параграфе и в разделе "Организация кода в классах и модулях" вы видели простой класс Минералы. Вот исходный код этого класса, так сказать, собственной персоной, без всякой дополнительной логики:

class Mineral

getter name: String

getter hardness: Float64

getter crystal_struct: String

def initialize(@name, @hardness, @crystal_struct) # constructor

end

end

Этот класс имеет три переменные экземпляра, доступные только для чтения "name", "hardness" и "crystal_struct". Предоставление им типов навязано компилятором Crystal. Но также вы можете расставить типы данных, используя метод initialize:

classes_and_structs/classes.cr

class Mineral

getter name, hardness, crystal_struct

 

 def initialize(@name: String, @hardness: Float64, @crystal_struct: String)

 end

end

 

Значения по умолчанию можно распределить следующим образом:

def initialize(@name: String = "unknown",...)

end

Некоторые люди используют символы вроде:hardness для имено-
вания свойства, но это не обязательно. Свойство с неустановленным
типом должно иметь значение по умолчанию. Или вы можете дать
ему значение при инициализации (initialize). Вам не нужно определять переменные в начале класса.

Здесь метод new создаёт объект Mineral:

min1 = Mineral.new("gold", 1.0, "cubic")

min1 # => #<Mineral:0x271cf00 @crystal_struct="cubic",

      # => @hardness=1.0, @name="gold">

min1.object_id # => 41012992 == 0x271cf00

typeof(min1) # => Mineral # compile-time type

min1.class # => Mineral # run-time type

Mineral.class # => Class # all classes have type Class

Запомните, new — это метод класса, который создается автоматически для каждого класса. Он резервирует место в оперативной памяти, вызывает метод initialize, а затем возвращает вновь созданный объект. Объект создается в так называемой "куче" (общем массиве свободной
ОП), и он снабжен уникальным идентификатором (object_id), сообщающим адрес ячейки памяти. Когда он получает новое имя или передается методу, фактически передается лишь ссылка на объект.

Это означает, что объект сам изменится в том случае, если будет
изменен в методе. Если вы не уверены, какие типы данных примет
ваш метод "initialize", вы также можете использовать групповые типы вроде "T", как это делается в классе Mineralg:

classes_and_structs/classes.cr

class Mineralg(T)

getter name

 

def initialize(@name: T)

 end

end

 

min = Mineralg.new("gold")

min2 = Mineralg.new(42)

min3 = Mineralg(String).new(42)

 # => Error: no overload matches 'Mineralg(String).new' with type Int32

 

Именование переменных экземпляра сопровождается приписыванием префикса "@". Для переменных класса используйте символы "@@", например @@planet, откуда и происходят наши минеральные породы. Все объекты, построенные с помощью этого класса, будут иметь общий доступ к этой переменной, и ее значение будет одинаковым для каждого такого объекта. (Впрочем подклассы, которые вы увидите в следующем разделе, все получают собственную копию переменной класса со значением, общим для всех членов отдельного подкласса.)

Чтобы дать имя свойству, которое может изменяться, например, "quantity" (количество) в следующем примере, припишите перед ним ключевое слово "property". Для свойств, которые доступны лишь для записи и потому не считываются, используйте префикс "setter", на-
пример, id в нижеследующем исходном коде.

Наша попытка вывести на дисплей нечитаемое свойство завершается ошибкой:

classes_and_structs/classes.cr

class Mineral

@@planet = "Earth"

 

getter name, hardness, crystal_struct

setter id

property quantity: Float32

 

def initialize(@id: Int32, @name: String, @hardness: Float64,

  @crystal_struct: String)

@quantity = 0f32

end

 

def self.planet

@@planet

end

end

min1 = Mineral.new(101, "gold", 1.0, "cubic")

min1.quantity = 453.0f32 # => 453.0

min1.id # => Error: undefined method  'id' for Mineral

Mineral.planet # => "Earth"

 

min2 = min1.dup

min1 == min2 # =>  false

 

Необходимо удостовериться, что ваши свойства всегда будут инициализированы либо в методе инициализации (initialize), либо
после вызова метода new. Названия методов, вызываемых в самом
классе, предваряют префиксом "self.", как это и было сделано в вышеприведенном примере с методом "planet".

Используйте метод dup для создания "упрощенной" копии объекта: копия min2 является другим объектом, но если оригинал содержит поля, которые сами являются объектами, то они не войдут в состав копии.
Если же вам нужна подробная копия, вам следует определить метод
clone. Вы можете также дополнительно написать метод "finalize" для класса, который будет автоматически вызываться, когда объект подбирается мусоросборщиком:

def finalize

puts  "Bye bye from this #{self}!"

end

Но это создает лишнюю работу для процесса сбора мусора. Вы должны использовать его, только если вы хотите освободить занятые внешними библиотеками ресурсы, которые сборщик мусора Crystal не освободит самостоятельно.

Добавьте этот фрагмент кода, чтобы увидеть работу завершающего метода, но будьте осторожны: вы исчерпаете свободную память вашего компьютера, копая столько золота. Так что сохраните все, что вам
нужно, прежде чем запустить его.

 

loop do

Mineral.new(101, "gold", 1.0, "cubic")

end

 

Как и в Ruby или в C#, вы можете повторно открыть класс, что оз-
начает создание дополнительных определений этого класса: ведь все
они объединены в один класс. Это также работает для встроенных
классов. Насколько это круто — определять свои собственные новые методы для существующих классов, таких как String или Array? (Да, иногда такой подход дерзко называют "обезьяний патчинг", и это не всегда хорошая идея.)

 

 

Ваш черёд № 1

 

1. Сотрудник: Создайте класс Employee с извлекателем (т.н. "getter")
name и свойством age. Создайте объект класса Employee и попытайтесь изменить его имя.

2. Приращение: Создайте класс Increment со свойством amount и
двумя версиями метода increment: один, который добавляет 1 к
amount, и другой, который добавляет значение "inc_amount".

 

Применение наследования

 

Как и во всех объектно-ориентированных языках и прежде всего в
Ruby, в Crystal предусмотрено одиночное наследование, обозначаемое: подкласс < суперкласс. Перемещение свойств и методов, объединен-
ных в несколько классов, в один суперкласс позволяет всем им
совместно использовать функциональные возможности. Таким обра-
зом, в подклассе вы можете использовать все переменные экземпляра
и все методы суперкласса, включая конструкторы.

Это можно увидеть в следующем примере, где PDFDocument наследует initialize, "name" и print из класса Document:

classes_and_structs/inheritance.cr

class Document

property name

 

def initialize(@name: String)

end

 

def print

puts "Hi, I'm printing #{@name}"

end

end

 

class PDFDocument < Document

end

 

doc = PDFDocument.new("Salary Report Q4 2018")

doc.print # => Hi, I'm printing Salary Report Q4 2018

Вы можете также переопределить любой унаследованный метод в подклассе. Если подкласс определяет собственные методы initialize для инициализации, они больше не наследуются. Если вы хотите исполь-зовать функциональность суперкласса после его переопределения,
можно вызвать любой метод суперкласса с помощью "super":

classes_and_structs/inheritance.cr

class PDFDocument < Document

def initialize(@name: String, @company: String)

end

 

def print

super

puts "From company #{@company}"

   end

end

 

# doc = PDFDocument.new("Salary Report Q4 2018")

 # => Error: wrong number of arguments for 'PDFDocument.new' (given 1,

            # => expected 2)

doc = PDFDocument.new("Salary Report Q4 2018", "ACME")

doc.print

 

# => Hi, I'm printing Salary Report Q4 2018

# From company ACME

 

Система типов языка Crystal дает вам больше возможностей здесь.
Вместо перестановки типов можно формировать специализированные методы с использованием ограничений для типов данных, например "print" в PDFDocument:

classes_and_structs/inheritance.cr

class PDFDocument < Document

def initialize(@name: String, @company: String)

end

 

def print(date: Time)

puts "Printing #{@name}"

puts "From company #{@company} at date #{date}"

 end

end

doc = PDFDocument.new("Salary Report Q4 2018", "ACME")

doc.print(Time.now)

 

# => Printing Salary Report Q4 2018

# From company ACME at date 2017-05-25 12:12:45 +0200

 

 

Использование абстрактных классов и виртуальных типов

ЯП Ruby, в отличие от Java или C#, не имеет собственной поддержки интерфейсов и абстрактных классов. И в Ruby, и в Crystal концепция интерфейса реализована посредством модулей, как вы увидите в следующей главе. Но в Кристалле также существует понятие абстракт-
ного класса, так что если вы Руби-кодер, то ожидающий впереди
материал будет вам в новинку.

Не всем классам суждено производить объекты, и абстрактные классы являются хорошим примером. Вместо этого они служат концеп-
туальным планом для подклассов, чтобы через них реализовать свои методы. Здесь вы видите класс Rect (описывающий прямоугольники), вынужденный реализовать все абстрактные методы из класса Shape:

classes_and_structs/inheritance.cr

abstract class Shape

abstract def area

abstract def perim

end

class Rect < Shape

def initialize(@width: Int32, @height: Int32)

end

 

def area

@width * @height

end

 

def perim

2 * (@width + @height)

 end

end

s = Shape.new # => can't instantiate abstract class Shape

Rect.new(3, 6).area # => 18

 

Если один из методов (скажем, perim) не реализован, компилятор
выдает ошибку, похожую на следующее:

«error:  abstract `def Shape#perim()` must be implemented by Rect»

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

classes_and_structs/virtual.cr

class Document

end

class PDFDocument < Document

def print

puts "PDF header"

end

end

 

class XMLDocument < Document

def print

puts "XML header"

end

end

 class Report

getter doc

 

def initialize(@name: String, @doc: Document)

end

end

salq4 = Report.new "Salary Report Q4", PDFDocument.new

taxQ1 = Report.new "Tax Report Q1", XMLDocument.new

Этот виртуальный тип указывается компилятором как тип Document+, подразумевая тем самым, что все типы наследуют от класса Document,
в том числе и сам Document. Он вступает в игру в ситуациях вроде
той, что приводится ниже, где вы ожидаете, что "d" относится к объединенному типу данных (PDFDocument | XMLDocument):

  if 4 < 5

d = PDFDocument.new

else

d = XMLDocument.new

end

typeof(d) # => Document

Но, вопреки ожиданиям, d соответствует типу Document. Внутри компилятор использует его как виртуальный тип Document+ вместо объединенного типа (PDFDocument | XMLDocument), поскольку типы объединений быстро становятся очень сложными в классовых
иерархиях. Если вызвать метод в подклассе, созданном для Document, возникает ошибка:

 «salq4.doc.print # => Error: undefined method  'print' for Document»

Чтобы устранить эту ошибку, просто сделайте класс Document абстрактным.

classes_and_structs/virtual.cr

abstract class Document

end

salq4.doc.print # => PDF header

Ваш черёд № 2

Фигура: Подкласс Shape с классами Square и Circle. (Подсказка: Примените PI из модуля Math, используя запись: "include Math".)

 

Управление видимостью

Видимые объекты можно прочитать или даже изменить с помощью внешнего кода, и это является заурядной причиной ошибок и сюр-
призов. Ограничение видимости (т.н. инкапсуляция кода) очень часто применяется в объектно-ориентированных языках. Все это связано с концепцией пространства имен в программном коде — некой (номенклатурной) области в коде, которая обозначена определенным именем. Класс является простым примером: всё внутри описания класса формирует часть его пространства имен.

По умолчанию объект действительно будет узнаваемым для всего пространства имен, в котором он определен: он является обще-
доступным в пределах этого пространства имен, но при этом неви-
димым и недоступным вне этого пространства имен. Мы поговорим об этом более подробно в следующей главе, посвященной модулям.

В Crystal методы по умолчанию являются публичными. То есть они пригодны для использования внутри и снаружи того класса, в котором
они определены. Чтобы ограничить видимость, вы можете подставить к формулировке метода специальное слово: либо "private" (намекает на приватность), либо "protected" (то есть защищенный).

 

 

Объявляем метод частным

Частные методы больше похожи на вспомогательные: их можно использовать только внутри самого класса или в его подклассах, и их нельзя вызвать для объекта.

classes_and_structs/private.cr

class Document

property name

 

def initialize(@name: String)

end

 

private def print(message)

puts message

end

 

def printing

print "Hi, I'm printing #{@name}"

# self.print "Printing with self does not work"

# => Error: private method  'print' called for Document

end

end

 

class PDFDocument < Document

def printing

super

print "End printing PDFDocument"

end

end

 

doc = Document.new("Salary Report Q4 2018")

doc.printing # => Hi, I'm printing Salary Report Q4 2018

pdoc = PDFDocument.new("Financial Report Q4 2018")

pdoc.printing # =>

# Hi, I'm printing  Financial Report Q4 2018

# End printing PDFDocument

# doc.print("test") # => Error: private method  'print' called for Document

 

Такие типы, как перечисления (enums), тоже могут быть частными.
Тогда вы можете их использовать только внутри того пространства
имен, в котором они определены.

И далее точно так же — вы можете использовать только методы или
типы верхнего уровня, которые помечены как приватные в текущем файле с исходным кодом. Можно также использовать служебное слово "private" с классами, модулями и константами, а также с псевдонимами
и библиотеками, которые вы увидите позже.

 

Охраняемые методы

Защищенные методы устроены неcколько хитрее: они могут делать те
же вещи, что и их приватные собратья, но ещё они могут вызываться
для объекта. Этот объект должен быть того же типа, что и текущий (используемый сейчас) тип данных, например класс, в котором мы те-
перь находимся, или класс из того же пространства имен. А вот пример, чтобы сделать вышесказанное более конкретным:

classes_and_structs/protected.cr

class Document

property name

def initialize(@name: String)

end

protected def print(message)

puts message

end

 

def printing

print "Hi, I'm printing #{@name}"

self.print("This works too: self is a Document")

 doc = Document.new("Taxes")

doc.print("This also: doc is a Document")

   end

end

class BankAccount < Document

def printing

doc = Document.new ("TestDoc")

doc.print "inside BankAccount"

end

end

class BankAccount2

def printing

doc = Document.new ("TestDoc")

doc.print "inside BankAccount2"

end

end

doc2 = Document.new "Audit 2017"

doc2.printing

# => Hi, I'm printing Audit 2017

# => This works too: self is a Document

# => This also: doc is a Document

 doc2.print "Audit" # => Error: protected method 'print' called for Document

ba = BankAccount.new "test"

ba.printing # => inside BankAccount

ba2 = BankAccount2.new

 # ba2.printing # => Error: protected method  'print' called for Document

Защищенный метод print работает внутри класса BankAccount, который сам является подклассом, произошедшим от Document, и потому он существует в том же пространстве имен. Но этот метод не работает
внутри класса BankAccount2, лежащего за пределами пространства
имен класса Document, и в результате возникает ошибка:

 "error: protected method 'print' called for Document"

 

Перегружаемые операторы

Перегрузка, которую вы наблюдали в предыдущем упражнении, также работает для операторов. Следующий фрагмент исходного кода демонстрирует перегрузку оператора "==". Допустим, что два образца минералов считаются равнозначными, когда у них одинаковый ID (т.е. идентификатор).

Они оба должны принадлежать классу Minerals, поэтому мы используем здесь self в качестве ограничителя типов. Та же логика выражается в применении метода "compare":

classes_and_structs/classes.cr

class Mineral

getter id, name, hardness, crystal_struct

property quantity: Float32

def initialize(@id: Int32, @name: String, @hardness: Float64,

@crystal_struct: String)

@quantity = 0f32

end

 

def ==(other: self) # self is Mineral

id == other.id

end

 

def ==(other)

false

end

 

def self.compare(m1: self, m2: self)

m1.id == m2.id

end

end

m1 = Mineral.new(101, "gold", 1.0, "cubic")

m2 = Mineral.new(108, "gold", 1.0, "cubic")

m3 = Mineral.new(101, "gold", 1.0, "cubic")

m1 == m2 # => false

m1 == m3 # => true

Mineral.compare(m1, m2) # => false

 

Обратите внимание, что синтаксические схемы Ruby вроде class << self, используемые с внутриклассовыми методами, недопустимы в ЯП Crystal. Внутриклассовые методы Кристалла — это методы с префиксом self. Теперь спросите себя: а может ли класс иметь несколько методов инициализации? (В Ruby ответ будет "нет".)

И да, такое бывает в Crystal — это возможно благодаря перегрузке. Вот пример, чтобы убедить вас:

classes_and_structs/classes.cr

class Mineral

getter id, name, hardness, crystal_struct

property quantity: Float32

def initialize(@id: Int32, @name: String, @hardness: Float64,

@crystal_struct: String)

@quantity = 0f32

end

 

def initialize(@id: Int32)

@quantity = 0f32

@name = "rock"

@hardness = 0

@crystal_struct = "unknown"

end

end

m1 = Mineral.new(101, "gold", 1.0, "cubic")

m4 = Mineral.new(42)

            # => #<Mineral:0x271bd40

# @crystal_struct ="unknown",

# @hardness =0,

# @id =42,

# @name ="rock",

   # @quantity =0

>

▪ Программа ▪

 

В Ruby программа верхнего уровня представляет собой экземпляр (с именем main) класса Object:

  # Ruby code!

puts self  # main

puts self.class  # Object

 

Если обходиться с ней как с кодом Crystal, мы получим ошибку компилятора: "здесь отсутствует self (т.е. его нет в зоне видимости)".
Но внутри класса self — это всего лишь название класса.

Но внутри экземплярного метода self — это "текущий экземпляр", как
и в языке Ruby:

__classes_and_structs/program.cr__

def display

puts "Top-level display"

end

class Mineral

puts self # => Mineral

getter name

getter hardness

getter crystal_struct

 

def initialize(@name: String, @hardness: Float64,

    @crystal_struct: String)

end

 

def display

::display # => Top-level display

p self # => <Mineral:0x271cf00 @crystal_struct="cubic",

                               #  @hardness=1.0, @name="gold">

end

end

 

min1 = Mineral.new("gold", 1.0, "cubic")

min1.display

 

Для вызова метода верхнего уровня (такого как display) в методе внутри класса принято добавлять к названию префикс — пару двоеточий (::).
В ЯП Crystal нет классического main, но зато есть своего рода ано-
нимное первичное пространство имен, называемое "the Program", где живут такие методы, как puts, p, raise, spawn и другие, а с ними и некоторые макросы.

Программа Crystal — это пространство имен, в котором можно опре-делять (и вызывать) методы, типы, константы, локализованные в
файле переменные, классы... и много чего ещё.

 

 

Работа со структурами

Объекты, созданные из классов, занимают память в "общей куче" и сборщику мусора приходится освобождать эту область памяти. Как вы
уже видели ранее на примере кода цикла в разделе "Структурирование класса", создание множества объектов может быстро истощить ресурсы ЭВМ. Для повышения производительности в таких случаях можно использовать своего рода упрощенную разновидность классов, которая называется "struct" и наследует признаки от класса Struct.

Структуры помещаются в память стека, поэтому структура копируется
(а не перемещается), когда её переназначают или передают в метод. Другими словами: осуществляется передача по значению. На первый взгляд, они очень похожи на классы: имеют свойства, конструкторы и методы, и даже могут быть универсальными.

Наш следующий пример определяет структуру для обустройства пользовательских данных:

classes_and_structs/structs.cr

struct User

property name, age

 

def initialize(@name: String, @age: Int32)

end

def print

puts "#{age} - #{name}"

 end

end

d = User.new("Donald", 42)

d.name # => Donald

d.age = 78

d.print # => 78 – Donald

▪ Изменение структур в методах ▪

Поскольку структуры копируются при передаче, вам нужно подумать
о возвращаемом значении и его переопределении после возврата.
В следующем фрагменте метод "no_change" не изменяет структуру, работает только метод change:

__classes_and_structs/structs.cr__

def no_change(user)

user.age = 50

end

 

def change(user)

user.age = 50

user

end

 

d = User.new("Donald", 78)

d.print # => 78 - Donald

no_change(d)

d.print # => 78 - Donald

d = change(d)

d.print # => 50 – Donald

Структуры лучше всего подходят для неизменяемых (называемых так-
же неизменными) порций данных, особенно когда структура небольшая
и у вас есть несколько таких. В качестве примера можно привести стандартную библиотеку, где комплексные числа реализованы посредством структуры Complex.

Наследование также может быть задано для структуры, но только от абстрактной структуры.

▪ Используйте структуры для производительности ▪

 

Попробуйте превратить класс в структуру в вашем программном
коде и проверьте, повысится ли производительность. Если программа работает быстрее и это не вредит функциональности, напрашивается выбор в пользу структуры.

Ваш черёд № 3

 Vec2D - Перегрузка оператора для структуры: Предположим, что вам нужно добавить много двухмерных векторов. Определите структуру
Vec2D для выполнения этого действия и перегрузите оператор "+".
Затем определите ещё несколько похожих структур и работайте с этими векторами.

(Подсказка: Ограничьте операцию "+", пользуясь self для добавления дополнительных структур, т.е. клонов Vec2D.)

 

Осмотр иерархии типов

Теперь уже вы, вероятно, подозреваете, что за каждой программой
Crystal стоит целая иерархия классов. Её можно даже визуализировать
с помощью инструмента Кристальной Иерархии. Например:

$ crystal tool hierarchy virtual.cr

показывает все классы и структуры из вашей программы, а также стандартную библиотеку ЯП Crystal. Если вы хотите лишь фрагмент
этого дерева (например, только ваши собственные классы), используйте флаг -e:

$ crystal tool hierarchy -e Document virtual.cr

Этот запрос производит следующий результат:

 

Давайте сделаем схему для наиболее важных типов, которая включа-
ет в себя и классы, и структуры (например, Int32), которые могут быть абстрактными или обычными. Каждому сдвигу вправо соответствует очередной субклассовый слой (т.е. подкласс), как показано на рисунке ниже.

 

 

Первичным суперклассом является, конечно, Object, от которого
каждый объект наследует совокупность методов, в том числе таких как "==" и "to_s". Далее, у нас есть резкое отличие между объектами в
ветви наследования от Value (которые создаются в памяти стека) и
веткой наследования от Reference, объекты которой размещаются в
"куче общей памяти".

Память стека реагирует гораздо быстрее обще-кучной памяти и не нуждается в сборщике мусора, но все значения передаются методом копирования; и следует помнить, что размер стековой памяти огра-
ничен. Использование общей памяти позволяет программе пройтись по ссылкам, указывающим на оригинальные значения, не касаясь самих значений, а "мусор" будет собран автоматически. Память в "общей куче" обычно используется для так


Поделиться с друзьями:

Своеобразие русской архитектуры: Основной материал – дерево – быстрота постройки, но недолговечность и необходимость деления...

Индивидуальные очистные сооружения: К классу индивидуальных очистных сооружений относят сооружения, пропускная способность которых...

История развития пистолетов-пулеметов: Предпосылкой для возникновения пистолетов-пулеметов послужила давняя тенденция тяготения винтовок...

Историки об Елизавете Петровне: Елизавета попала между двумя встречными культурными течениями, воспитывалась среди новых европейских веяний и преданий...



© cyberpedia.su 2017-2024 - Не является автором материалов. Исключительное право сохранено за автором текста.
Если вы не хотите, чтобы данный материал был у нас на сайте, перейдите по ссылке: Нарушение авторских прав. Мы поможем в написании вашей работы!

0.516 с.