Перейти к содержанию

Наследование, полиморфизм, области видимости, статические методы

Три парадигмы ООП

ZScript является объектно-ориентированным языком, и из ООП на него распространяются и парадигмы:

  • Наследование: использование одного класса в качестве "родителя" другого, с сохранением в дочернем классе свойств (переменных, методов) родительского, но и с добавлением своих. С помощью данной парадигмы выстраиваются целые иерархии и деревья классов — довольно удобно.
  • Полиморфизм: возможность изменять некоторые свойства родителей в дочерних классах. Даёт бóльшую гибкость иерархии.
  • Инкапсуляция: обеспечение принципа "вещи-в-себе" — цельности объекта по данным и коду; также предоставляет полуавтоматическую защиту от несанкционированного доступа ("кто и как может работать со мной").

Наследование — возможность создавать классы с сохранением свойств какого-то другого класса. Обозначается двоеточием и названием родительского класса перед открывающей операторной скобкой класса:

class A {
    int a, b;
    String internalstr;

    void PrintStr( void ) {
        console.printf( internalstr );
    }
}

class B1: A {
    // Здесь есть и поля "a", "b", "internalstr", и метод "PrintStr".
    // Добавляем ещё что-нибудь:
    double gravityValue;
}

class C1: B1 {
    // Наледованы поля "a", "b", "internalstr", "gravityValue" и метод "PrintStr".
    int sum4( int c ) {
        return a + b + c;
    }

    double AdjustGravity( void ) {
        if ( gravityValue < 0.0 )
            gravityValue = 0.0;

        return gravityValue;
    }
}

class B2: A {
    // Тут нет полей и методов из B1 и C1, так как ни тот, ни другой 
    //не являются родительскими классами для этого. Но из A — есть.
    double health;

    void SetStr( String newstr ) {
        internalstr = newstr;
    }

    void ResetAll( void ) {
        a = b = 0;
        health = 0.0;
        internalstr = "";
    }
}

Для примеров использования полиморфизма и инкапсуляции см. "1.13. Виртуальные методы, абстрактные классы/методы".

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

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

Укороченный материал по теме на Википедии.

Предупреждение о совместимости: на старых версиях движка (примерно до GZDoom 3.4.0) полям данных нельзя было назначать область видимости, они всегда были public. Методов это ограничение не касалось.

Public

Применяется по умолчанию ко всему, если не указано "protected" или "private": доступность всем и вся. Где можно от него избавиться — лучше избавиться.

Специального ключевого слова в ZScript не имеет.

Protected

Метод или переменная будет доступна только для этого класса и его наследников (вне зависимости от количества наследований).

Ключевое слово — "protected".

Private

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

Ключевое слово — "private".

Примеры

Из-за использования слова "protected" для одного строкового поля в классе "A" пример не будет работать на старых версиях движка. Однако так пример выглядит нагляднее.

class A {
    int a, b;
    protected String internalstr;

    private void SetStr( String newstr ) {
        internalstr = newstr;
    }

    void PrintStr( void ) {
        console.printf( internalstr );

        SetStr( "Printed '" .. internalstr .. "'." ); // Отсюда метод без труда вызывается, класс тот же самый.
    }

    protected int sum3( int c ) {
        SetStr( "Called sum3( " .. c .. " )." );

        return a + b + c;
    }
}

class B: A {
    int SomeExtraSum( int d ) {
        if ( d > 0 )
            return sum3( d );
        else
            return -d;
    }

    void Init( void ) {
        SetStr( "Hello world!" ); // Компилятор выдаст ошибку: метод "SetStr()" недоступен даже из-под наследника.

        internalstr = "Hello world!"; // А так можно, ибо поле обявлено как "доступное для себя и наследнииков".
    }
}

class SomeOtherClass {
    A obja;

    int TryToGetSum3( void ) {
        // Ошибка: "obja.sum3()" является protected-методом, для чужих оно недоступно.
        return ( ( obja != NULL )? obja.sum3( 3 ) : -1 );
    }

    int TryToGetInt( void ) {
        // Поле "int a" публично, ошибки не будет.
        return ( ( obja != NULL )? obja.a : -1 );
    }
}

Статические методы

Иногда требуется написать код не к какому-то конкретному экземпляру класса, а к самому классу. Например, когда упомянутого экземпляра пока нет и/или когда суть метода относится в целом к классу. Упоминавшийся ранее "Actor.Spawn()" (см. 1.11) тому пример — у нас может и не быть ни одного актора на уровне, но создавать-то их как-то необходимо.

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

В объявлении такого метода должно присутствовать ключевое слово static.

class Math {
    static double pow( double value, double power ) {
        if ( power == 0.0 || value == 1.0 )
            return 1.0;
        if ( power == 1.0 )
            return value;

        return exp( power * log( value ) );
    }

    static double haversin( double fita ) {
        return ( 1.0 - cos( fita ) ) * 0.5;
    }
}

// <...>
console.printf( "2^5: " .. Math.pow( 2.0, 5.0 ) );

console.printf( "3.17^6.3: " .. Math.pow( 3.17, 6.3 ) );

console.printf( "haversine( 5 ): " .. Math.haversin( 5 ) );