Кормораздатчик мобильный электрифицированный: схема и процесс работы устройства...
Архитектура электронного правительства: Единая архитектура – это методологический подход при создании системы управления государства, который строится...
Топ:
Процедура выполнения команд. Рабочий цикл процессора: Функционирование процессора в основном состоит из повторяющихся рабочих циклов, каждый из которых соответствует...
Методика измерений сопротивления растеканию тока анодного заземления: Анодный заземлитель (анод) – проводник, погруженный в электролитическую среду (грунт, раствор электролита) и подключенный к положительному...
Комплексной системы оценки состояния охраны труда на производственном объекте (КСОТ-П): Цели и задачи Комплексной системы оценки состояния охраны труда и определению факторов рисков по охране труда...
Интересное:
Распространение рака на другие отдаленные от желудка органы: Характерных симптомов рака желудка не существует. Выраженные симптомы появляются, когда опухоль...
Что нужно делать при лейкемии: Прежде всего, необходимо выяснить, не страдаете ли вы каким-либо душевным недугом...
Наиболее распространенные виды рака: Раковая опухоль — это самостоятельное новообразование, которое может возникнуть и от повышенного давления...
Дисциплины:
2022-09-29 | 25 |
5.00
из
|
Заказать работу |
|
|
Если вы разработчик 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 - Не является автором материалов. Исключительное право сохранено за автором текста.
Если вы не хотите, чтобы данный материал был у нас на сайте, перейдите по ссылке: Нарушение авторских прав. Мы поможем в написании вашей работы!