WarCraft 3: 4. Классы

WurstScript

Объявление

Классы, это простая, мощная и очень эффективная конструкция. Классы определяют наборы данных и взаимодействующие с этими данными функции. Процедуру объявления и использования нового класса проще будет показать на примере
class Point
    // Блок класса Point
    real x
    real y

function someFunc()
    Point p
    p = new Point
    p.x = 200
    p.y = 300
    destroy p
В этом примере, за счет ключевого слова class, мы объявили класс Point, с атрибутами x и y. Теперь класс Point может быть использован как тип для переменных и функций.
Внутри функции someFunc была объявлена переменная с именем p, ранее определенного типа Point. В следующей строке, с применением ключевого слова new, был создан экземпляр класса Point и присвоен переменной p.
Обращение к элементам экземпляра производится за счет оператора . который применяется в следующих двух строках, для присвоения атрибутам x и y экземпляра значений 200 и 300 соответственно.
Ну и наконец, в последней строке, используется ключевое слово destroy для уничтожения ранее созданного экземпляра.

Методы

Помимо атрибутов (подобно x и y из предыдущего примера), класс так же может содержать методы. Метод, это функция, принадлежащая классу
class MyClass
    // Блок класса Point
    function hello() // Метод класса MyClass
        print("Hello, World!")

function SomeFunc()
    MyClass p = new MyClass
    p.hello()
    destroy p
В данном примере был создан экземпляр класса MyClass и присвоен переменной p. Затем был вызван метод hello, в результате работы которого на экран будет выведено сообщение "Hello, World!". Как можете видеть, обращение к методам производится через оператор . аналогично обращению к атрибутам класса.

this

Ключевое слово this предоставляет доступ к текущему экземпляру класса внутри его методов, что позволяет обращаться к членам текущего экземпляра
class Point
    real x
    real y

    function setCoord( real x, real y )
        this.x = x
        this.y = y

function SomeFunc()
    Point p = new Point
    p.setCoord(200, 300)
В следующей строке после создания экземпляра класса Point, мы вызвали его метод setCoord, которому были переданы необходимые значения. Внутри уже метода setCoord, с применением ключевого слова this, происходит присваивание переданных значений атрибутам x и y текущего экземпляра.
Ключевое слово this является необязательным и в случае отсутствия конфликта имен может быть опущено
class Point
    real x
    real y

    function setCoord( real newX, real newY )
        x = newX
        y = newY

function SomeFunc()
    Point p = new Point
    p.setCoord(200, 300)

Конструктор

Как было показано в предыдущих примерах, для создания нового экземпляра класса необходимо использовать ключевое слово new
Point p = new Point
Wurst позволяет определить конструктор.
Конструктор — метод, используемый для создания нового экземпляра класса. Конструктор вызывается при создании экземпляра и позволяет выполнять произвольные операции, прежде чем созданный экземпляр будет возвращен. Для объявления конструктора используется ключевое слово construct за которым сразу следует список формальных параметров
class Point
    real x
    real y

    construct( real x, real y ) // Конструктор
        this.x = x
        this.y = y

function SomeFunc()
    Point p = new Point(200, 300)
В этот раз, мы объявили класс Point, заранее определив его конструктор. Теперь, при создании нового экземпляра этого класса, мы можем передать список необходимых значений, которые будут присвоены атрибутам экземпляра x и y внутри конструктора.
Вы можете определить более одного конструктора
class Point
    real x
    real y
    real z

    construct( real x, real y ) // Конструктор с 2-мя параметрами
        this.x = x
        this.y = y
        this.z = 0.0

    construct( real x, real y, real z ) // Конструктор с 3-мя параметрами
        this.x = x
        this.y = y
        this.z = z

function SomeFunc()
    Point p = new Point(200, 300)      // Будет вызван конструктор с 2-мя параметрами
    Point p = new Point(200, 300, 750) // Будет вызван конструктор с 3-мя параметрами
В этом примере класс Point имеет два конструктора, один принимает 2 параметра, второй — 3. В зависимости от типа и числа передаваемых параметров, Wurst будет автоматически принимать решение, какой именно конструктор должен быть вызван во время создания нового экземпляра.

Деструктор

Как было показано в начале главы, для уничтожения экземпляра класса используется ключевое слово destroy
destroy p
Для каждого класса вы можете определить единственный деструктор
Деструктор — метод, вызываемый автоматически, перед уничтожением экземпляра класса. Деструктор необходим для очистки экземпляра от более не используемых ресурсов — удаление юнитов, уничтожение групп, обнуление переменных и т.д. Для объявления деструктора используется ключевое слово ondestroy, без формальных параметров
class MyClass
    unit u

    ondestroy
        RemoveUnit(u)

Статические члены

Вы можете объявить атрибут или метод класса статическим.
Статические члены класса принадлежат самому классу и являются общими для всех его экземпляров, они подобны глобальным функциям и переменным. Для обращения к статическим членам класса нет необходимости создавать экземпляр класса, это осуществляется через имя самого класса. Чтобы объявить член класса статическим, используется ключевое слово static
class Point
    real x
    real y
    static int count = 0

    static function getCount() returns int
        return Point.count

    construct( real x, real y )
        this.x = x
        this.y = y
        Point.count++

    ondestroy
        Point.count--

function someFunc()
    int i = Point.getCount()
    print("The number of Points = " + I2S(i))
В данном примере, мы дополнили класс Point статическим атрибутом count целочисленного типа и статическим методом getCount.
Конструктор класса увеличивает значение статического атрибута count на 1 при создании каждого экземпляра, а деструктор, в свою очередь, уменьшает его значение на 1, при уничтожении каждого экземпляра. Таким образом производится подсчет общего числа всех существующих экземпляров класса Point. Внутри функции someFunc объявляется переменная i, которой присваивается общее количество существующих экземпляров класса Point, посредством вызова статического метода getCount, после чего оно выводится на экран в виде сообщения.

Размерные массивы внутри классов

Wurst предоставляет возможность объявлять массивы внутри классов, с заданной размерностью
// Класс, описывающий некий прямоугольник
class Rectangle
    Point array[4] points

    construct( real x, real y, real width, real height )
        points[0] = new Point(x, y)
        points[1] = new Point(x + width, y)
        points[2] = new Point(x + width, y — height)
        points[3] = new Point(x, y - height)
Прим. Пер.: следует обратить внимание на способ реализации массивов — компилятор создает новый Jass-массив под каждую ячейку массива внутри класса, то есть, массив points типа Point будет преобразован в следующее
globals
    ...
    integer array Rectangle_points_0
    integer array Rectangle_points_1
    integer array Rectangle_points_2
    integer array Rectangle_points_3
    ...
endglobals
Об этом важно помнить, так как функция обращения к ячейке массива превращается в безобразное
// Функция, генерируемая компилятором, для поиска массива, соответствующего указанной ячейке
function Rectangle_points_set takes integer instanceId, integer arrayIndex, integer value returns nothing
	if arrayIndex < 0 or arrayIndex >= 4 then
		call error("Index out of Bounds")
	elseif arrayIndex <= 1 then
		if arrayIndex <= 0 then
			set Rectangle_points_0[instanceId] = value
		else
			set Rectangle_points_1[instanceId] = value
		endif
	elseif arrayIndex <= 2 then
		set Rectangle_points_2[instanceId] = value
	else
		set Rectangle_points_3[instanceId] = value
	endif
endfunction
Следует понимать, что в случае объявления огромного массива внутри класса, например в 10 000 ячеек, мы получим 10 000 глобальных массивов и функцию установки/получения значения высотой в тысячи строк, что может привести к превышению оп-лимита.
Если вы планируете использовать большие последовательности данных внутри класса, лучше отказаться от использования обычных массивов в пользу контейнеров, например HashList из стандартной библиотеки.

Область видимости

По умолчанию, доступ к членам класса можно получить откуда угодно, они находятся в public области видимости. Вы можете переопределить область видимости члена класса используя ключевые слова
  • public — полностью открытый доступ (используется по умолчанию)
  • protected — обратиться к члену класса можно только из метода этого класса, метода наследника этого класса (см. далее) и пакета, внутри которого класс был определен
  • private — обратиться к члену класса можно только из метода этого класса
class MyClass
    real a           // открыт по умолчанию
    public real b    // тоже открыт, но явно
    protected real c // защищен, доступен только из подкласса (см. далее) и текущего пакета
    private real d   // закрыт, доступен только изнутри этого класса

    public function someMethod( real d ) // работает и с методами
        this.d = d

function someFunc()
    MyClass x = new MyClass
    x.a = 3.14  // Допустимо, атрибут a по умолчанию открыт
    x.b = 57.29 // Допустимо, атрибут b был объявлен открытым явно
    x.c = 2.71  // Допустимо, если функция someFunc определена в том же пакете,
                // что и класс MyClass
    x.d = 0.69  // Ошибка! Атрибут d объявлен закрытым, обращение извне класса невозможно
    x.someMethod(1.44) // Допустимо, по тем же правилам

Наследование

Класс может расширять другие классы. Расширяющий класс (далее подкласс или наследник) наследует все атрибуты и методы расширяемого класса (далее суперкласс или родительский класс), которые объявлены без private модификатора и может быть использован где угодно, где мог бы быть использован родительский класс.
Для объявления расширяющего класса используется ключевое слово extends.
Ключевое слово extends следует сразу за именем подкласса, а после него указывается имя расширяемого класса.
Конструктор подкласса обязательно должен вызывать конструктор суперкласса, за счет ключевого слова super. В конструкторе подкласса не может быть каких-либо инструкций прежде вызова конструктора суперкласса.
Если программистом не было предусмотрено вызова super, Wurst попытается вставить его самостоятельно, без параметров.
Методы, наследуемые подклассом, могут быть в нем переопределены с помощью ключевого слова override
// Тип-снаряд
class Missile
    construct(string fx, real x, real y)
        ...
    // Какие-то действия, происходящие при столкновении снаряда с юнитом
    function onCollide(unit u)
        // ...


// Fireball, это особый тип снаряда
class Fireball extends Missile
    // Создаем экземпляр, используя определенную модель для снаряда Fireball
    construct(real x, real y)
        // Вызов конструктора родительского класса
        super("Abilities\\Weapons\\RedDragonBreath\\RedDragonMissile.mdl", x, y)

    // Fireball совершает какие-то специфические действия при столкновении с юнитом,
    // соответственно, мы переопределяем метод столкновения
    override function onCollide(unit u)
        ...

Подробнее о super

Когда вы переопределяете метод родительского класса, вы по прежнему можете вызвать оригинальный метод изнутри метода наследника. Для этого используется ключевое слово super, подобно тому, как если бы это был экземпляр родительского класса.
Подобный механизм может быть чрезвычайно полезен, если метод только-лишь расширяет возможности переопределенного, но не заменяет его полностью. Этот механизм можно продемонстрировать на предыдущем примере
// Тип-снаряд
class Missile
    construct(string fx, real x, real y)
        ...

    // Наносит урон юниту при столкновении
    function onCollide(unit u)
        UnitDamageTarget(...)


// Fireball, это особый тип снаряда
class Fireball extends Missile

    construct(real x, real y)
        // Вызов конструктора родительского класса
        super("Abilities\\Weapons\\RedDragonBreath\\RedDragonMissile.mdl", x, y)

    // Fireball так же наносит урон при столкновении, 
    // но дополняет действие добавлением спец-эффекта
    override function onCollide(unit u)
        super.onCollide(u) // Вместо того чтобы полностью переписать родительский метод
                           // набором таких же действий, мы его просто вызовем
        AddSpecialEffectTarget(...) // А теперь действия, свойственные именно
                                    // классу Fireball
Таким же образом может быть реализовано обращение к атрибутам родительского класса.

Приведение типов

Для приведения типов используется оператор castTo. Приведение типов полезно в некоторых случаях.
Одним из них может оказаться необходимость сохранения экземпляра класса в качестве целочисленного значения привязанного к таймеру, как это реализовано в пакете TimerUtils. Этот процесс так же может быть обратным (от целочисленного к классу)
class Point
    ...

init
    Point p = new Point(...)
    int i = p castTo int
Приведение типов может оказаться полезным в работе с подтипами. Если вы имеете некоторый объект a и вам известно, что он является динамическим объектом типа B, вы можете произвести приведение типов
class A
    ...

class B extends A
    function someMethod()
        ...

init
    A a = new B()
    // Нам известно, что объект a в действительности является экземпляром классом B,
    // посему, мы можем безопасно привести объект a к типу B:
    B b = a castTo B
    // Теперь мы можем вызвать метод класса B
    b.someMethod()
Примечание: Вам следует всячески избегать использования оператора castTo. Приведение типов не проверяется во время выполнения кода и потому может оказаться чрезвычайно неверным.

Динамическая диспетчеризация

Что это может значить? Это значит, что если вы имеете некоторый объект типа B, приведенный к типу A, то при попытке вызвать переопределенный в подклассе B метод суперкласса A, будет вызван переопределенный метод класса B. Понять эту муть будет проще на примере.
Первый пример
class A
    function printOut()
        print("I'm A")

class B extends A
    override function printOut()
        print("I'm B")

init
    A a = new B()
    a.printOut() // На экран будет выведено сообщение "I'm B", несмотря на то,
                 // что это переменная типа A
Второй пример
class A
    string name

    construct(string name)
        this.name = name

    function printName()
        print("Instance of A named: " + name )


class B extends A

    construct(string name)
        super(name)

    override function printName()
        print("Instance of B named: " + name )


init
    A a = new B("first") // Это работает, так как B расширяет A
    a.printName() // Выведет сообщение "Instance of B named: first", 
                  // так как a это экземпляр B.

instanceof

Wurst позволяет проверять тип экземпляра класса посредством ключевого слова instanceof
class A

class B extends A

init
    A a 
    a = new A
    if a instanceof A // True, объект a есть экземпляр класса A
        ...
    if a instanceof B // False, объект a не является экземпляром класса A
        ...
    a = new B
    if a instanceof B // True, объект a есть динамический экземпляр класса B
        ...
Проверку типов возможно применять только к экземплярам пользовательских классов, невозможно провести сравнение пользовательского типа со встроенным типом, например, следующее выражение недопустимо
a instanceof int

Абстрактные Классы

Wurst позволяет объявлять абстрактные классы.
Абстрактный класс — класс, неспособный иметь собственных экземпляров, но способный иметь абстрактные методы.
Абстрактный метод — метод абстрактного класса, не имеющий тела (реализации/определения) и обязанный быть реализованным в классе-наследнике.
Абстрактные класс/метод объявляется посредством ключевого слова abstract, перед объявлением класса/метода.
Абстрактные классы близки к интерфейсам (см. далее), однако они могут иметь свои атрибуты и реализованные методы.
В качестве примера обратимся к ранее приводимому классу снаряда
// Тип-снаряд
abstract class Missile
    ...
    abstract function onCollide(unit u)


// Fireball, это особый тип снаряда
class Fireball extends Missile
    ...
    override function onCollide(unit u)
        ...
В данном примере, родительский класс Missile является абстрактным и обязывает наследников реализовать метод onCollide, вызываемый при столкновении с юнитом. Таким образом, гарантировано общее поведение производных классов.
Одни абстрактные классы могут наследовать другие абстрактные классы, без необходимости реализации абстрактных методов, однако, в таком случае, наследник тоже обязан быть абстрактным классом.

Интерфейсы

Интерфейсы - группы объявленных, но нереализованных методов. Интерфейсы напоминают абстрактные классы, но они не могут иметь какие-либо атрибуты или реализацию методов.
Объявление интерфейса начинается с ключевого слова interface.
Класс, который обязуется реализовать некоторый интерфейс, должен использовать ключевое слово implements, за которым следует имя интерфейса
// Интерфейс снаряда
interface Missile
    function onCollide(unit u)


// Fireball реализует интерфейс Missile
class Fireball implements Missile
    ...
    override function onCollide(unit u)
        ...
Один класс может реализовать несколько интерфейсов, в таком случае, их имена перечисляются через запятую
class MyClass implements A, B, C, ...

Просмотров: 152

Комментарии пока отсутcтвуют