Crystal Programming. Введение на основе проекта в создание эффективных, безопасных и читаемых веб-приложений и приложений CLI - Джордж Дитрих
Шрифт:
Интервал:
Закладка:
Все данные внутри объекта хранятся в переменных экземпляра; их имена всегда начинаются с символа @. Существует несколько способов определить переменную экземпляра для класса, но одно правило является фундаментальным: их тип должен быть известен. Тип может быть либо указан явно, либо синтаксически выведен компилятором.
Начальное значение переменной экземпляра может быть задано либо внутри метода initialize, либо непосредственно в теле класса. В последнем случае он ведет себя так, как если бы переменная была инициализирована в начале метода initialize. Если переменная экземпляра не назначена ни в одном методе initialize, ей неявно присваивается значение nil.
Тип переменной будет определяться из каждого присвоения ей в классе, из всех методов. Но имейте в виду, что их тип может зависеть только от литеральных значений или типизированных аргументов и больше ни от чего. Давайте посмотрим несколько примеров:
class Point
def initialize(@x : Int32, @y : Int32)
end
end
origin = Point.new(0, 0)
В этом первом случае класс Point указывает, что его объекты имеют две целочисленные переменные экземпляра. Метод initialize будет использовать свои аргументы, чтобы предоставить им начальное значение:
class Cat
@birthday = Time.local
def adopt(name : String)
@name = name
end
end
my_cat = Cat.new
my_cat.adopt("Tom")
Теперь у нас есть класс, описывающий кошку. У него нет метода initialize, поэтому он ведет себя так, как если бы он был пустым. Переменная @birthday назначается Time.local. Это происходит внутри этого пустого метода initialize при создании нового экземпляра объекта. Предполагается, что тип является экземпляром Time, поскольку Time.local вводится так, чтобы всегда возвращать его. Переменная @name получает строковое значение из типизированного аргумента, но нигде не имеет начального значения, поэтому ее тип — String? (это также можно представить как String | Nil).
Обратите внимание, что выведение переменной экземпляра из аргумента работает только в том случае, если параметр указан явно, а переменной экземпляра присваивается непосредственно значение. Следующий пример недействителен:
class Person
def initialize(first_name, last_name)
@name = first_name + " " + last_name
end
end
person = Person.new("John", "Doe")
В этом примере переменная @name создается путем объединения двух аргументов с пробелами между ними. Здесь тип этой переменной невозможно определить без более глубокого анализа типов двух параметров и результата вызова метода +. Даже если бы аргументы были явно типизированы как String, информации все равно было бы недостаточно, поскольку метод + для строк может быть переопределен где-то в коде, чтобы возвращать какой-либо другой произвольный тип. В подобных случаях необходимо объявить тип переменной экземпляра:
class Person
@name : String
def initialize(first_name, last_name)
@name = first_name + " " + last_name
end
end
В качестве альтернативы можно использовать буквальную интерполяцию строки, поскольку она гарантированно всегда создает строку:
class Person
def initialize(first_name, last_name)
@name = "#{first_name} #{last_name}"
end
end
В любой ситуации допускается явное объявление типа переменной экземпляра, возможно, для ясности.
Примечание
Вы можете задаться вопросом, почему компилятор не анализирует всю программу и каждый вызов метода, чтобы самостоятельно определить типы каждой переменной экземпляра, как он уже делает это для локальных переменных? Компилятор делал именно это в первые дни, но эта функция была удалена, поскольку этот анализ был слишком дорогим с точки зрения производительности и делал инкрементальную компиляцию невозможной в будущем. Существующие правила вывода переменных экземпляра в большинстве случаев успешны, и их редко приходится вводить.
Переменные экземпляра представляют частное состояние объекта, и ими следует манипулировать только с помощью методов внутри класса. Их можно раскрыть через геттеры и сеттеры. Доступ к переменным экземпляра можно получить извне с помощью синтаксиса obj.@ivar, но это не рекомендуется.
Создание геттеров и сеттеров
В Crystal нет специальной концепции метода получения или установки свойств объекта; вместо этого они построены на основе функций, о которых мы уже узнали. Допустим, у нас есть человек, у которого есть переменная экземпляра имени:
class Person
def initialize(@name : String)
end
end
Мы уже можем создать нового человека и проверить его:
person = Person.new("Tony")
p person
Но было бы неплохо иметь возможность написать что-то вроде следующего, как если бы @name был доступен:
puts "My name is #{person.name}"
person.name — это просто вызов метода name объекта person. Помните, что круглые скобки необязательны для вызовов методов. Мы можем пойти дальше и создать именно этот метод:
class Person
def name
@name
end
end
Теперь вызов person.name действителен, как если бы переменная экземпляра была доступна извне. В качестве дополнительного преимущества будущий рефакторинг может изменить внутреннюю структуру объекта и переопределить этот метод, не затрагивая пользователей. Это настолько распространено, что специально для этого существует служебный макрос:
class Person
getter name
end
Предыдущие два фрагмента ведут себя одинаково. Макрос-получатель создает метод, предоставляющий переменную экземпляра. Его также можно комбинировать с объявлением типа или начальным значением:
class Person
getter name : String
getter age = 0
getter height : Float64 = 1.65
end
Несколько геттеров могут быть созданы в одной строке:
class Person
getter name : String, age = 0, height : Float64 = 1.65
end
Для сеттеров логика очень похожа. Имена методов Crystal могут заканчиваться символом = для обозначения установщика. Если у него один параметр, его можно вызвать с помощью удобного синтаксиса:
class Person
def name=(new_name)
puts "The new name is #{new_name}"
end
end
Этот метод name= можно вызвать следующим образом:
person = Person.new("Tony")
person.name = "Alfred"
Последняя строка представляет собой просто вызов метода и не меняет значение переменной экземпляра @name. Это то же самое, что написать person.name=("Alfred"), как если бы = была любая другая буква. Мы можем воспользоваться этим, чтобы написать метод