Наследование классов: Наследование классов в C++

Содержание

Классы, наследование— Ruby Rush

В этом уроке мы рассмотрим непростую тему наследования классов. Мы научимся писать классы-детей, наследующие поведение от классов-родителей. А также, спроектируем программу «Блокнот», в которой помимо обычных заметок будут также задачки и ссылки.

План урока

  1. Наследование классов
  2. Готовим классы для нового блокнота

Концепция наследования

Чтобы понять, что такое наследование, давайте рассмотрим в качестве примера понятие Средство транспортировки:

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

У каждого средства транспортировки, вне зависимости от его типа, есть как минимум вот такие свойства и методы

СредствоТранспортировки

Свойства:
@пропускная_способность
@год_запуска_в_эксплуатацию

Методы:
начать_транспортировку
остановить_транспортировку

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

Корабль:
Все методы и свойства «СредстваТранспортировки» плюс:

Свойства:
@скорость_хода
@водоизмещение

Методы:
начать_движение
остановиться

Наконец, добираемся до чего-то совсем конкретного (конкретнее только указать кадастровый номер какого-нибудь судна): рассмотрим моторную лодку:

МоторнаяЛодка:
Все методы и свойства «Корабля» плюс:

Свойства:
@текущая_температура_двигателя
@расход_топлива

Методы:
завести_мотор
заглушить_мотор

Получается, что эти понятия как бы вложены друг в друга: МоторнаяЛодка < Корабль < СредствоТранспортировки.

Для описания таких структур в объектно-ориентированных языках программирования придумали понятие наследования.

Что такое наследование и зачем оно нужно?

Класс — это чертеж, по которому создается объект какой-то определенной группы.

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

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

При этом родительский класс, как правило, ещё абстрактнее, чем его дети: «транспорт» куда абстрактнее «моторной лодки».

Этот шаблон широко используется в современных фреймворках (например, в популярном фреймворке для создания веб-приложений Ruby on Rails). Именно поэтому так важно понять эту концепцию.

Проектируем новый дневник

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

Воспользуемся программой из 17-го урока (вы можете также взять её из материалов к этому уроку). Мы будем её развивать на протяжении этого и следующего урока.

Итак, у нас будет три типа записи: заметка

, ссылка и задача.

Давайте для начала разберёмся со свойствами.

Для заметки нам понадобятся такие поля

@text # содержание заметки
@created_at # дата создания заметки

Для ссылки нам понадобятся такие поля

@url # адрес ссылки
@description # описание ссылки
@created_at # дата создания ссылки

А для тудушки — такие:

@text # тело задачи
@due_date # к какому числу задачу нужно сделать
@created_at # дата создания задачки

Как вы видите, у всех записей есть общее поле @created_at, также если присмотреться, у всех классов есть поле с текстом @text, только у ссылки оно называется @description. Большой беды не будет, если мы его переименуем.

Поля @created_at и @text мы вынесем в родительский класс

Post. Дочерний класс Memo не будет иметь дополнительных полей, дочерний класс Task будет иметь дополнительное поле @due_date, а дочерний класс Link будет иметь дополнительное поле @url.

Родительский класс Post

Итак, выделим общие поля в родительский класс Post. Для этого в папке урока rubytut2/lesson6 создайте папку notepad и в ней создайте файл post.rb с описанием класса Post. Мы опишем в этом классе общую логику работы с записями:

# Базовый класс "Запись"
# Задает основные методы и свойства, присущие всем разновидностям Записи
class Post

  # Конструктор
  def initialize
    @created_at = Time.now # дата создания записи
    @text = nil # массив строк записи — пока пустой
  end

  # Этот метод вызывается в программе, когда нужно
  # считать ввод пользователя и записать его в нужные поля объекта
  def read_from_console
    # должен быть реализован классами-детьми,
    # которые знают как именно считывать свои данные из консоли
  end

  # Этот метод возвращает состояние объекта в виде массива строк, готовых к записи в файл
  def to_strings
    # должен быть реализован классами-детьми,
    # которые знают как именно хранить себя в файле
  end

  # Этот метод записывает текущее состояние объекта в файл
  def save
    # он будет только у родителя, его мы напишем позже
  end
end

# PS: Весь набор методов, объявленных в родительском классе называется интерфейсом класса
# Дети могут по–разному реализовывать методы, но они должны подчиняться общей идее
# и набору функций, которые заявлены в базовом (родительском классе)

Дочерний класс Memo

Напишем класс Memo, который унаследует основной функционал от класса Post.

# Класс "Заметка", разновидность базового класса "Запись"
class Memo < Post

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

  # Этот метод пока пустой, он будет спрашивать ввод содержимого Заметки
  # наподобие программы Дневник из "базового блока" курса
  def read_from_console
  end
end

Дочерний класс Link

Класс ссылки тоже будет наследоваться от класса Post:

# Класс Ссылка, разновидность базового класса "Запись"
class Link < Post

  def initialize
    super # вызываем конструктор родителя

    # потом инициализируем специфичное для ссылки поле
    @url = ''
  end

  # Этот метод пока пустой, он будет спрашивать 2 строки — адрес ссылки и описание
  def read_from_console
  end

  # Массив из трех строк: адрес ссылки, описание и дата создания
  # Будет реализован в след. уроке
  def to_strings
  end
end

Дочерний класс Task

Наконец, опишем в файле task. rb класс

Task, который наследует себя от класса Post:

# Подключим встроенный в руби класс Date для работы с датами
require 'date'

# Класс Задача, разновидность базового класса "Запись"
class Task < Post
  def initialize
    super # вызываем конструктор родителя

    # потом инициализируем специфичное для Задачи поле - дедлайн
    @due_date = Time.now
  end

  # Этот метод пока пустой, он будет спрашивать 2 строки - описание задачи и дату дедлайна
  def read_from_console
  end

  # Массив из трех строк: дедлайн задачи, описание и дата создания
  # Будет реализован в след. уроке
  def to_strings
  end
end

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

Наследование классов в Python.

Классы в языке Python поддерживают наследование классов, что позволяет создавать новые классы с расширенным и/или измененным функционалом базового класса.

Новый класс, созданный на основе базового класса — называется производный класс (derived class) или просто подкласс.

Подкласс наследует атрибуты и методы из родительского класса. Он так же может переопределять (override) методы родительского класса. Если подкласс не определяет свой конструктор __init__, то он наследует конструктор родительского класса по умолчанию.

Синтаксис определения производного (дочернего) класса выглядит следующим образом:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    <statement-N>

Имя BaseClassName должно быть определено в области, содержащей определение производного класса. Вместо имени базового класса допускаются и другие произвольные выражения. Это может быть полезно, например, когда базовый класс определен в другом модуле:

class DerivedClassName(modname.BaseClassName):
    <statement-1>
    .
    <statement-N>

Выполнение определения производного класса DerivedClassName происходит так же, как и для базового класса BaseClassName. Когда объект класса создан, базовый класс BaseClassName запоминается. Это используется для разрешения ссылок на атрибуты. Если запрошенный атрибут не найден в классе DerivedClassName, поиск переходит к поиску в базовом классе BaseClassName. Это правило применяется рекурсивно, если сам базовый класс является производным от какого-либо другого класса.

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

Производные классы DerivedClassName могут переопределять методы своих базовых классов BaseClassName. Поскольку методы не имеют особых привилегий при вызове других методов того же объекта, метод базового класса, который вызывает другой метод, определенный в том же базовом классе, может в конечном итоге вызвать метод производного класса, который переопределяет его. Для программистов C++ — все методы в Python фактически являются виртуальными.

Переопределяющий метод в производном классе может фактически расширить, а не просто заменить метод базового класса с тем же именем. Существует простой способ вызвать метод базового класса напрямую: просто вызовите BaseClassName.methodname(self, arguments). Это иногда полезно и для «клиентов». Обратите внимание, что это работает только в том случае, если базовый класс доступен как имя базового класса BaseClassName в глобальной области видимости.

Python имеет две встроенные функции, которые работают с наследованием:

  • Используйте isinstance() для проверки типа экземпляра класса: isinstance(obj, int) будет истинным True только в том случае, если obj.__class__ равен int или класс является производным от класса int.

  • Используйте issubclass() для проверки наследования классов: issubclass(bool, int) является истинным, так как bool является подклассом int(). Однако issubclass(float, int) является ложным False, так как float не является подклассом int.

Примеры использования переопределения методов.
class One:
    def __init__(self, name):
        self.name = name

    def talk(self):
        return f'Меня зовут {self.name}'

    def say(self):
        return f'Привет {self.name}'


class Two(One):
    # декорируем метод
    def say(self):
        x = One.say(self)
        return f'{x} !!!'


class Three(One):
    # переопределяем метод
    def say(self, word):
        return f'{word} {self.name}...'

one = One('Андрей')
two = Two('Юра')
three = Three('Аня')

Результат:

print(f'class {One.__name__}:')
print(one.talk())
print(one.say())
# class One:
# Меня зовут Андрей
# Привет Андрей

print(f'class {Two.__name__}:')
print(two.talk())
print(two.say())
print('Two is subclass One:', issubclass(Two, One))
# class Two:
# Меня зовут Юра
# Привет Юра !!!
# Two is subclass One: True

print(f'class {Three. __name__}:')
print(three.talk())
print(three.say('Пока'))
print('Three is subclass One:', issubclass(Three, One))
# class Three:
# Меня зовут Аня
# Пока Аня...
# Three is subclass One: True

print('Three is subclass Two:', issubclass(Three, Two))
# Three is subclass Two: False

Наследование. Урок 4 курса «Объектно-ориентированное программирование на Python»

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

Суть наследования здесь схожа с наследованием объектами от классов. Дочерние классы наследуют атрибуты родительских, а также могут переопределять атрибуты и добавлять свои.

Простое наследование методов родительского класса

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

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

class Table:
    def __init__(self, l, w, h):
        self.length = l
        self.width = w
        self.height = h
 
class KitchenTable(Table):
    def setPlaces(self, p):
        self.places = p
 
class DeskTable(Table):
    def square(self):
        return self.width * self.length

В данном случае классы KitchenTable и DeskTable не имеют своих собственных конструкторов, поэтому наследуют его от родительского класса. При создании экземпляров этих столов, передавать аргументы для __init__() обязательно, иначе возникнет ошибка:

>>> from test import *
>>> t1 = KitchenTable()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 3 required 
positional arguments: 'l', 'w', and 'h'
>>> t1 = KitchenTable(2, 2, 0. 7)
>>> t2 = DeskTable(1.5, 0.8, 0.75)
>>> t3 = KitchenTable(1, 1.2, 0.8)

Несомненно можно создавать столы и от родительского класса Table. Однако он не будет, согласно неким родственным связям, иметь доступ к методам setPlaces() и square(). Точно также как объект класса KitchenTable не имеет доступа к единоличным атрибутам сестринского класса DeskTable.

>>> t4 = Table(1, 1, 0.5)
>>> t2.square()
1.2000000000000002
>>> t4.square()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Table' object 
has no attribute 'square'
>>> t3.square()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'KitchenTable' 
object has no attribute 'square'

В этом смысле терминология «родительский и дочерний класс» не совсем верна. Наследование в ООП – это скорее аналог систематизации и классификации наподобие той, что есть в живой природе. Все млекопитающие имеют четырехкамерное сердце, но только носороги – рог.

Полное переопределение метода надкласса

Что если в подклассе нам не подходит код метода его надкласса. Допустим, мы вводим еще один класс столов, который является дочерним по отношению к DeskTable. Пусть это будут компьютерные столы, при вычислении рабочей поверхности которых надо отнимать заданную величину. Имеет смысл внести в этот новый подкласс его собственный метод square():

class ComputerTable(DeskTable):
    def square(self, e):
        return self.width * self.length - e

При создании объекта типа ComputerTable по-прежнему требуется указывать параметры, так как интерпретатор в поисках конструктора пойдет по дереву наследования сначала в родителя, а потом в прародителя и найдет там метод __init__().

Однако когда будет вызываться метод square(), то поскольку он будет обнаружен в самом ComputerTable, то метод square() из DeskTable останется невидимым, т. е. для объектов класса ComputerTable он окажется переопределенным.

>>> from test import ComputerTable
>>> ct = ComputerTable(2, 1, 1)
>>> ct.square(0.3)
1.7

Дополнение, оно же расширение, метода

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

class ComputerTable(DeskTable):
    def square(self, e):
        return DeskTable.square(self) - e 

Здесь вызывается метод другого класса, а потом дополняется своими выражениями. В данном случае вычитанием.

Рассмотрим другой пример. Допустим, в классе KitchenTable нам не нужен метод, поле places должно устанавливаться при создании объекта в конструкторе. В классе можно создать собственный конструктор с чистого листа, чем переопределить родительский:

class KitchenTable(Table):
    def __init__(self, l, w, h, p):
        self.length = l
        self.width = w
        self.height = h
        self.places = p

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

class KitchenTable(Table):
    def __init__(self, l, w, h, p):
        Table.__init__(self, l, w, h)
        self.places = p

Теперь при создании объекта типа KitchenTable надо указывать в конструкторе четыре аргумента. Три из них будут переданы выше по лестнице наследования, а четвертый оприходован на месте.

>>> tk = KitchenTable(2, 1.5, 0.7, 10)
>>> tk.places
10
>>> tk.width 
1.5

Параметры со значениями по умолчанию у родительского класса

Рассмотрим случай, когда родительский класс имеет параметры со значениями по умолчанию, а дочерний – нет:

class Table:
    def __init__(self, l=1, w=1, h=1):
        self.length = l
        self.width = w
        self.height = h
 
 
class KitchenTable(Table):
    def __init__(self, p, l, w, h):
        Table.__init__(self, l, w, h)
        self.places = p

При таком определении классов можно создать экземпляр от Table без передачи аргументов для конструктора:

Можем ли мы создать экземпляр от KitchenTable, передав значение только для параметра p? Например, вот так:

Возможно ли, что p будет присвоено число 10, а l, w и h получат по единице от родительского класса? Невозможно, будет выброшено исключение по причине несоответствия количества переданных аргументов количеству требуемых конструктором:

. ..
    k = KitchenTable(10)
TypeError: __init__() missing 3 required
 positional arguments: 'l', 'w', and 'h'

Когда создается объект от дочернего класса, сначала вызывается его конструктор, если он есть. Интерпретатор еще не знает, что в теле этого конструктора будет вызван конструктор родительского класса. Ведь это не обязательно. Значит, если все параметры дочернего конструктора не имеют значений по умолчанию, при построении объекта все значения должны передаваться.

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

class Table:
    def __init__(self, l=1, w=1, h=1):
        self.length = l
        self.width = w
        self.height = h
 
 
class KitchenTable(Table):
    def __init__(self, l=1, w=1, h=0.7, p=4):
        Table.__init__(self, l, w, h)
        self.places = p

Параметр p, которого нет у родительского класса, мы делаем последним не просто так. Бывает, объекты разных родственных классов создаются или обрабатываются в одном цикле, то есть по одному алгоритму. При этом у них должны быть одинаковые «интерфейсы», то есть одинаковое количество передаваемых в конструктор аргументов.

Поэтому лучше, когда методы родственных классов принимают одинаковое число параметров. А если разное, то у «лишних» должны быть значения по-умолчанию, чтобы при вызове конструктора их можно было бы не передавать. Если такие параметры находятся еще и в конце, передачу аргументов для предстоящих параметров можно выполнять без ключей.

Другой вариант – отказаться от конструктора в дочернем классе, а значение для поля places устанавливать отдельным вызовом метода:

class Table:
    def __init__(self, l=1, w=1, h=1):
        self.length = l
        self.width = w
        self.height = h
 
 
class KitchenTable(Table):
    places = 4
 
    def set_places(self, p):
        self.places = p

Здесь у всех кухонных столов по-умолчанию будет 4 места. Если мы хотим изменить значение поля places, можем вызвать метод set_places(). Хотя в случае Python можем сделать это напрямую, присвоив полю. При этом у экземпляра появится собственное поле places.

k = KitchenTable()
k.places = 6

Поэтому метод set_places() в общем-то не нужен.

В любом случае произвольное количество мест будет устанавливаться не в конструкторе, а отдельно. Если все же требуется указывать места при создании объекта, это можно сделать и в конструкторе родителя:

class Table:
    def __init__(self, l=1, w=1, h=1):
        self.length = l
        self.width = w
        self.height = h
        if isinstance(self, KitchenTable):
            p = int(input("Сколько мест: "))
            self.places = p

С помощью функции isinstance() проверяется, что создаваемый объект имеет тип KitchenTable. Если это так, то у него появляется поле places.

Мы не используем параметр p со значением по умолчанию в заголовке конструктора потому, что, если объектам других родственных классов он не нужен, не происходило путаницы и сложностей с документированием кода.

Практическая работа

Разработайте программу по следующему описанию.

В некой игре-стратегии есть солдаты и герои. У всех есть свойство, содержащее уникальный номер объекта, и свойство, в котором хранится принадлежность команде. У солдат есть метод «иду за героем», который в качестве аргумента принимает объект типа «герой». У героев есть метод увеличения собственного уровня.

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

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

Отправьте одного из солдат первого героя следовать за ним. Выведите на экран идентификационные номера этих двух юнитов.

Курс с примерами решений практических работ и всеми уроками:
android-приложение, pdf-версия

Каскад и наследование — Изучение веб-разработки

Цель этого урока — углубить ваше понимание некоторых основополагающих концепций CSS — каскадов, спецификаций и наследования, — которые контролируют то, как CSS применяется к HTML и как разрешаются конфликты.

Хотя изучение этого урока может показаться менее актуальным и немного более академичным, чем некоторые другие части курса, понимание этих вещей спасёт вас от головной боли в дальнейшем! Мы рекомендуем вам внимательно изучить этот раздел и убедиться, что вы понимаете концепции, перед тем, как двигаться дальше.

CSS (Cascading Style Sheets) означает Каскадные Таблицы Стилей и первое слово «каскадные» является невероятно важным для понимания: то, как ведёт себя каскад — ключевой момент в понимании CSS.

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

Также значимой является концепция наследования, которая заключается в том, что некоторые свойства CSS наследуют по умолчанию значения, установленные для родительского элемента текущего элемента, а некоторые не наследуют. Это также может стать причиной поведения, которое вы, возможно, не ожидаете.

Давайте начнём с краткого обзора ключевых моментов, которых мы касаемся, далее рассмотрим каждый из них по очереди и посмотрим, как они взаимодействуют друг с другом и с вашим CSS. Это может показаться набором сложных для понимания понятий. Однако, когда вы получите больше опыта в написании CSS, для вас станет более очевидным то, как это работает.

Каскад

Каскад таблицы стилей, если говорить упрощённо, означает, что порядок следования правил в CSS имеет значение; когда применимы два правила, имеющие одинаковую специфичность, используется то, которое идёт в CSS последним.

В приведённом ниже примере у нас есть два правила, которые могут применяться к h2. В результате h2 окрасится синим цветом — эти правила имеют идентичный селектор и, следовательно, одинаковую специфичность, поэтому побеждает последний в порядке следования.

 

Специфичность

Специфичность определяет, как браузер решает, какое именно правило применяется в случае, когда несколько правил имеют разные селекторы, но, тем не менее, могут быть применены к одному и тому же элементу. Различные типы селекторов ( селекторы элементов h2{...}, селекторы классов, селекторы идентификаторов и т.д ) имеют разной степени влияние на элементы страницы. Чем более общее влияние оказывает селектор на элементы страницы тем меньше его специфичность, конкретность. По существу, это мера того, насколько специфическим будет отбор по селектору:

  • Селектор элементов ( например h2 ) менее специфичен — он выберет все элементы этого типа на странице — поэтому получит меньше баллов.
  • Селектор класса более специфичен — он выберет только те элементы на странице, которые имеют конкретное значение атрибута class — поэтому получит больше баллов, селектор класса применится после селектора элемента и поэтому перекроет его стили.

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

 

Позже мы объясним, как сделать оценку специфичности, и прочие детали.

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

Наследование также надо понимать в этом контексте — некоторые значения свойства CSS, установленные для родительских элементов наследуются их дочерними элементами, а некоторые нет.

Например, если вы установили значение color и font-family для элемента, то каждый элемент внутри него также будет иметь этот цвет и шрифт, если только вы не применили к ним напрямую стиль с другим цветом и шрифтом.

 

Некоторые свойства не наследуются — например, если вы установили для элемента width равным 50%, все его дочерние элементы не получат ширину в 50% от ширины своего родительского элемента. Если бы это было так, CSS было бы чрезвычайно трудно использовать!

Примечание: На страницах справочника CSS свойств вы можете найти окно технической информации, обычно в конце раздела спецификации, в котором перечислены некоторые технические данные об этом свойстве, в том числе наследуется оно или нет. Например, здесь: color property Specifications section.

Эти три концепции вместе определяют, какая CSS применяется и к какому элементу; в следующих разделах мы увидим, как они взаимодействуют. Это может показаться сложным, но вы начнёте лучше понимать их по мере приобретения опыта работы с CSS, и вы всегда можете обратиться к справочной информации, если что-то забыли. Даже опытные разработчики не помнят всех деталей!

Видео ниже показывает, как вы можете использовать Firefox DevTools для проверки каскада стилей, спецификации, и т.д. на странице:

Итак, наследование. В примере ниже мы имеем <ul> с двумя уровнями неупорядоченных списков, вложенных в него. Мы установили для внешнего <ul> стиль границы, внутренние отступы и цвет шрифта.

Цвет шрифта применён к прямому потомку, но также и к непрямому потомку — к прямому потомку <li> и к элементам внутри первого вложенного списка. Далее мы добавили класс special ко второму вложенному списку и применили к нему другой цвет шрифта. Теперь это свойство наследуется всеми его потомками.

 

Такие свойства, как ширина (как в примере выше), внутренние и внешние отступы и стиль границы не наследуются. Если бы потомки нашего списка наследовали бы границу, то каждый отдельный список и каждая позиция в списке получили бы такую же границу — вряд ли мы хотели бы получить такой эффект!

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

Контроль наследования

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

inherit
Устанавливает значение свойства, применённого к элементу, таким же, как у его родительского элемента. Фактически, это «включает наследование».
initial
Устанавливает значение свойства, применённого к выбранному элементу, равным initial value этого свойства (в соответствии с настройками браузера по умолчанию. Если в таблице стилей браузера отсутствует значение этого свойства, оно наследуется естественным образом.)
unset (en-US)
Возвращает свойству его естественное значение, что означает, что если свойство наследуется естественным образом, оно действует как inherit, иначе оно действует как initial.

Примечание: Существует также более новое значение revert, которое имеет ограниченную поддержку браузерами.

Можно посмотреть список ссылок и изучить, как работают универсальные значения. Пример, следующий ниже, позволяет вам поиграть с CSS и увидеть, что происходит, когда вы вносите изменения. Подобные эксперименты с кодом — лучший способ освоить HTML и CSS.

Например:

  1. Второй элемент списка имеет класс my-class-1. Таким образом, цвет для следующего вложенного элемента a устанавливается по наследству. Как изменится цвет, если это правило будет удалено?
  2. Понятно ли, почему третий и четвёртый элементы a имеют именно такой цвет? Если нет, перечитайте описание значений, представленное выше.
  3. Какая из ссылок изменит цвет, если вы зададите новый цвет для элемента <a> — например: a { color: red; }?

Возврат всех исходных значений свойств

Стенографическое свойство CSS all можно использовать для того, чтобы присвоить одно из значений наследования к (почти) всем свойствам одновременно. Это одно из четырёх значений (inherit, initial, unset, или revert). Это удобный способ для отмены изменений, внесённых в стили, для того, чтобы вы могли вернуться к стартовой точке перед внесением новых изменений.

В примере ниже имеются два блока <blockquote>. Первый имеет стиль, который применён к самому элементу blockquote, второй имеет класс fix-this, который устанавливает значение all в unset.

 

Попробуйте установить для all ещё одно из доступных значений и исследуйте, в чём заключается разница.

Теперь мы понимаем, почему параграф, следующий по глубине в структуре HTML документа, имеет тот же цвет, что CSS применяет к body, а вводные уроки дали понимание того, как изменить применение CSS к чему-либо в любой точке документа — или назначить CSS элементу, или создать класс. Теперь рассмотрим подробнее то, как каскад определяет выбор CSS правил, применяемых в случае влияния на стиль элемента нескольких объектов.

Вот три фактора, перечисленные в порядке возрастания важности. Следующий отменяет предыдущий.

  1. Порядок следования
  2. Специфичность
  3. Важность

Мы внимательно изучим их, чтобы увидеть, как именно браузеры определяют, какой CSS следует применить.

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

Мы уже видели, какое значение для каскада имеет порядок следования. Если у вас несколько правил, которые имеют одинаковую важность, то побеждает правило, которое идёт последним в CSS. Другими словами, правила, более близкие к самому элементу, переписывают более ранние, пока последнее не победит, оно и стилизует элемент. 

Специфичность

Понимая, что порядок следования правил имеет значение, в какой-то момент вы окажетесь в ситуации, когда вы знаете, что правило появляется позже в таблице стилей, но применяется более раннее, конфликтующее правило. Это связано с тем, что более раннее правило имеет более высокую специфичность — оно более специфично и поэтому выбирается браузером как правило, которое должно стилизовать элемент.

Как мы видели ранее в этом уроке, селектор класса имеет больший вес, чем селектор элемента, поэтому свойства, определённые в классе, будут переопределять свойства, применённые непосредственно к элементу.

Здесь следует отметить, что, хотя мы думаем о селекторах и правилах, применяемых к объекту, который они выбирают, переписывается не всё правило, а только свойства, которые являются одинаковыми.

Такое поведение помогает избежать повторения в вашем CSS. Обычной практикой является определение общих стилей для базовых элементов, а затем создание классов для тех, которые отличаются. Например, в таблице стилей ниже мы определяем общие стили для заголовков второго уровня, а затем создаём несколько классов, которые изменяют только некоторые свойства и значения. Определённые вначале значения применяются ко всем заголовкам, затем к заголовкам с классами применяются более конкретные значения.

 

Давайте теперь посмотрим, как браузер будет вычислять специфичность. Мы уже знаем, что селектор элемента имеет низкую специфичность и может быть перезаписан классом. По существу, значение в баллах присуждается различным типам селекторов, и их сложение даёт вам вес этого конкретного селектора, который затем может быть оценён в сравнении с другими потенциальными соперниками.

Степень специфичности, которой обладает селектор, измеряется с использованием четырёх различных значений (или компонентов), которые можно представить как тысячи, сотни, десятки и единицы — четыре однозначные цифры в четырёх столбцах:

  1. Тысячи: поставьте единицу в эту колонку, если объявление стиля находится внутри атрибута style (встроенные стили). Такие объявления не имеют селекторов, поэтому их специфичность всегда просто 1000.
  2. Сотни: поставьте единицу в эту колонку за каждый селектор ID, содержащийся в общем селекторе.
  3. Десятки: поставьте единицу в эту колонку за каждый селектор класса, селектор атрибута или псевдокласс, содержащийся в общем селекторе.
  4. Единицы: поставьте общее число единиц в эту колонку за каждый селектор элемента или псевдоэлемент, содержащийся в общем селекторе.

Примечание: Универсальный селектор (*), комбинаторы (+, >,  ~, ») и псевдокласс отрицания (:not) не влияют на специфичность.

Следующая таблица показывает несколько несвязанных примеров, которые помогут вам разобраться. Посмотрите их все и убедитесь, что вы понимаете, почему они обладают той специфичностью, которую мы им дали. Мы ещё не рассмотрели селекторы детально, но вы можете найти подробную информацию о каждом селекторе в справочнике селекторов MDN.

СелекторТысячиСотниДесяткиЕдиницыСпецифичность
h200010001
h2 + p::first-letter00030003
li > a[href*="en-US"] > .inline-warning00220022
#identifier01000100
Без селектора, с правилом внутри атрибута style элемента.10001000

Прежде чем мы продолжим, давайте посмотрим на пример в действии.

 

Так что здесь происходит? Прежде всего, нас интересуют только первые семь правил этого примера, и, как вы заметите, мы включили их значения специфичности в комментарий перед каждым правилом.

  • Первые два правила конкурируют за стилизацию цвета фона ссылки — второе выигрывает и делает фоновый цвет синим, потому что у него есть дополнительный селектор ID в цепочке: его специфичность 201 против 101.
  • Третье и четвёртое правило конкурируют за стилизацию цвета текста ссылки — второе выигрывает и делает текст белым, потому что, хотя у него на один селектор элемента меньше, отсутствующий селектор заменяется на селектор класса, который оценивается в десять вместо единицы. Таким образом, приоритетная специфичность составляет 113 против 104.
  • Правила 5–7 соревнуются за определение стиля границы ссылки при наведении курсора. Шестой селектор со специфичностью 23 явно проигрывает пятому со специфичностью 24 — у него в цепочке на один селектор элемента меньше. Седьмой селектор, однако, превосходит как пятый, так и шестой — он имеет то же количество подселекторов в цепочке, что и пятый, но один элемент заменён селектором класса. Таким образом, приоритетная специфичность 33 против 23 и 24.

Примечание: Это был условный пример для более простого усвоения. В действительности, каждый тип селектора имеет собственный уровень специфичности, который не может быть замещён селекторами с более низким уровнем специфичности. Например, миллион соединённых селекторов класса не способны переписать правила одного селектора id.

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

!important

Существует специальный элемент CSS, который вы можете использовать для отмены всех вышеперечисленных вычислений, однако вы должны быть очень осторожны с его использованием — !important. Он используется, чтобы сделать конкретное свойство и значение самыми специфичными, таким образом переопределяя нормальные правила каскада.

Взгляните на этот пример, где у нас есть два абзаца, один из которых имеет ID.

 

Давайте пройдёмся по этому примеру, чтобы увидеть, что происходит — попробуйте удалить некоторые свойства, чтобы увидеть, что получится, если вам трудно понять:

  1. Вы увидите, что применены значения color (en-US) и padding третьего правила, но background-color — нет. Почему? Действительно, все три безусловно должны применяться, потому что правила, более поздние в порядке следования, обычно переопределяют более ранние правила.
  2. Однако вышеприведённые правила выигрывают, потому что селекторы классов имеют более высокую специфичность, чем селекторы элементов.
  3. Оба элемента имеют class с названием better, но у второго также есть id с названием winning. Поскольку ID имеют ещё более высокую специфичность, чем классы (у вас может быть только один элемент с каждым уникальным ID на странице, но много элементов с одним и тем же классом — селекторы ID очень специфичны, на что они и нацелены), красный цвет фона и однопиксельная чёрная граница должны быть применены ко 2-му элементу, причём первый элемент получает серый фоновый цвет и отсутствие границы, как определено классом.
  4. 2-й элемент получил красный цвет фона и отсутствие границы. Почему? Из-за объявления !important во втором правиле — размещение которого после border: none означает, что это объявление перевесит значение границы в предыдущем правиле, даже если ID имеет более высокую специфичность.

Примечание: Единственный способ переопределить объявление !important –  это включить другое объявление !important в правило с такой же специфичностью позже или в правило с более высокой специфичностью.

Полезно знать о существовании !important, чтобы вы понимали, что это такое, когда встретите в чужом коде. Тем не менее, мы настоятельно рекомендуем вам никогда не использовать его, если в этом нет острой необходимости. !important меняет обычный порядок работы каскада, поэтому он может серьёзно затруднить отладку проблем CSS, особенно в большой таблице стилей.

Одна из ситуаций, в которой вам, возможно, придётся это использовать, — это когда вы работаете с CMS, где вы не можете редактировать модули CSS ядра, и вы действительно хотите переопределить стиль, который нельзя переопределить другим способом. Но, вообще говоря, не стоит использовать этот элемент, если можно этого избежать.

Наконец, также полезно отметить, что важность объявления CSS зависит от того, в какой таблице стилей оно указано — у пользователя есть возможность установить индивидуальные таблицы стилей для переопределения стилей разработчика, например, пользователь может иметь проблемы со зрением и захочет установить размер шрифта на всех посещаемых им веб-страницах в два раза больше нормального размера, чтобы облегчить чтение.

Конфликтующие объявления будут применяться в следующем порядке, с учётом замены более ранних более поздними:

  1. Объявления в таблицах стилей клиентского приложения (например, стили браузера по умолчанию, используемые, когда не заданы другие стили).
  2. Обычные объявления в пользовательских таблицах стилей (индивидуальные стили устанавливаются пользователем).
  3. Обычные объявления в авторских таблицах стилей (это стили, установленные нами, веб-разработчиками).
  4. Важные объявления в авторских таблицах стилей.
  5. Важные объявления в пользовательских таблицах стилей.

Для таблиц стилей веб-разработчиков имеет смысл переопределить пользовательские таблицы стилей так, чтобы можно было сохранить запланированный дизайн, но иногда у пользователей есть веские причины для переопределения стилей веб-разработчика, как упомянуто выше — это может быть достигнуто с помощью использования !important в их правилах.

Мы охватили много тем в этой статье. А вы смогли запомнить наиболее важную информацию? Можете пройти несколько дополнительных тестов для того чтобы убедиться в том, что вы усвоили эту информацию, прежде чем пойдёте дальше — смотрите Test your skills: the Cascade.

Если вы поняли большую часть этой статьи, отлично — вы начали знакомиться с фундаментальными механизмами CSS. Далее мы рассмотрим селекторы подробно.

Если вы не до конца поняли каскад, специфичность и наследование, не волнуйтесь! Это, безусловно, самая сложная вещь из тех, что мы до сих пор  изучали в курсе, и даже профессиональные веб-разработчики иногда считают её коварной. Мы советуем вам вернуться к этой статье несколько раз в ходе изучения курса и продолжать обдумывать эту тему.

Обратитесь сюда, если вы столкнётесь со странными проблемами, когда стили применяются не так, как вы ожидаете. Это может быть проблемой специфичности.

Классы и наследование — Kotlin

Классы

Классы в Kotlin объявляются с помощью использования ключевого слова class:

class Invoice {
}

Объявление класса состоит из имени класса, заголовка (указания типов его параметров, основного конструктора и т.п) и тела класса, заключённого в фигурные скобки. И заголовок, и тело класса являются необязательными составляющими: если у класса нет тела, фигурные скобки могут быть опущены.

class Empty

Конструкторы

Класс в Kotlin может иметь основной конструктор (primary constructor) и один или более дополнительных конструкторов (secondary constructors). Основной конструктор является частью заголовка класса, его объявление идёт сразу после имени класса (и необязательных параметров):

class Person constructor(firstName: String)

Если у конструктора нет аннотаций и модификаторов видимости, ключевое слово constructor может быть опущено:

class Person(firstName: String)

Основной конструктор не может содержать в себе исполняемого кода. Инициализирующий код может быть помещён в соответствующий блок (initializers blocks), который помечается словом init.

При создании экземпляра класса блоки инициализации выполняются в том порядке, в котором они идут в теле класса, чередуясь с инициализацией свойств:

class InitOrderDemo(name: String) {
    val firstProperty = "Первое свойство: $name".also(::println)
    
    init {
        println("Первый блок инициализации: ${name}")
    }
    
    val secondProperty = "Второе свойство: ${name.length}".also(::println)
    
    init {
        println("Второй блок инициализации: ${name.length}")
    }
}

fun main() {
    InitOrderDemo("Привет")
}

Обратите внимание, что параметры основного конструктора могут быть использованы в инициализирующем блоке. Они также могут быть использованы при инициализации свойств в теле класса:

class Customer(name: String) {
    val customerKey = name.toUpperCase()
}

В действительности, для объявления и инициализации свойств основного конструктора в Kotlin есть лаконичное синтаксическое решение:

class Person(val firstName: String, val lastName: String, var age: Int) {
  // ...
}

Свойства, объявленные в основном конструкторе, могут быть изменяемые (var) и неизменяемые (val).

Если у конструктора есть аннотации или модификаторы видимости, ключевое слово constructor обязательно, и модификаторы используются перед ним:

class Customer public @Inject constructor(name: String) { ... }

Для более подробной информации по данному вопросу см. «Модификаторы доступа».

Дополнительные конструкторы

В классах также могут быть объявлены дополнительные конструкторы (secondary constructors), перед которыми используется ключевое слово constructor:

class Person {
    var children: MutableList<Person> = mutableListOf<>()
    constructor(parent: Person) {
        parent.children.add(this)
    }
}

Если у класса есть основной конструктор, каждый дополнительный конструктор должен прямо или косвенно ссылаться (через другой(ие) конструктор(ы)) на основной. Осуществляется это при помощи ключевого слова this:

class Person(val name: String) {
    var children: MutableList<Person> = mutableListOf<>()
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

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

class Constructors {
    init {
        println("Блок инициализации")
    }

    constructor(i: Int) {
        println("Конструктор")
    }
}

fun main() {
    Constructors(1)
}

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

class DontCreateMe private constructor () {
}

>Примечание: В виртуальной машине JVM компилятор генерирует дополнительный конструктор без параметров в случае, если все параметры основного конструктора имеют значения по умолчанию. Это делает использование таких библиотек, как Jackson и JPA, более простым в языке Kotlin, так как они используют пустые конструкторы при создании экземпляров классов.

>`kotlin >class Customer(val customerName: String = «») >`

Создание экземпляров классов

Для создания экземпляра класса конструктор вызывается так, как если бы он был обычной функцией:

val invoice = Invoice()

val customer = Customer("Joe Smith")

Обращаем ваше внимание на то, что в Kotlin не используется ключевое слово new.

Создание экземпляров вложенных, внутренних и анонимных внутренних классов описано в разделе Вложенные классы.

Члены класса

Классы могут содержать в себе:

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

Для всех классов в языке Kotlin родительским суперклассом является класс Any. Он также является родительским классом для любого класса, в котором не указан какой-либо другой родительский класс:

class Example // Неявно наследуется от Any

У Any есть три метода: equals(), hashCode() и toString(). Эти методы определены для всех классов в Kotlin.

По умолчанию все классы в Kotlin имеют статус final, который блокирует возможность наследования.
Чтобы сделать класс наследуемым, его нужно пометить ключевым словом open.

open class Base // Класс открыт для наследования

Для явного объявления суперкласса мы помещаем его имя за знаком двоеточия в оглавлении класса:

open class Base(p: Int)

class Derived(p: Int) : Base(p)

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

Если у класса нет основного конструктора, тогда каждый последующий дополнительный конструктор должен включать в себя инициализацию базового типа с помощью ключевого слова super или давать отсылку на другой конструктор, который это делает. Примечательно, что любые дополнительные конструкторы могут ссылаться на разные конструкторы базового типа:

class MyView : View {
    constructor(ctx: Context) : super(ctx) {
    }

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
    }
}

Переопределение методов класса

Как упоминалось ранее, мы придерживаемся идеи определённости и ясности в языке Kotlin. Поэтому Kotlin требует явно указывать модификаторы и для членов, которые могут быть переопределены, и для самого переопределения:

open class Shape {
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}

class Circle() : Shape() {
    override fun draw() { /*...*/ }
}

Для Circle.draw() необходима аннотация override. В случае её отсутствия компилятор выдаст ошибку. Если у функции типа Shape.fill() нет аннотации open, объявление метода с такой же сигнатурой в производном классе невозможно, с override или без. Модификатор open не действует при добавлении к членам final класса (т.е. класса без модификатора open).

Член класса, помеченный override, является сам по себе open, т.е. он может быть переопределён в производных классах. Если вы хотите запретить возможность переопределения такого члена, используйте final:

open class Rectangle() : Shape() {
    final override fun draw() { /*...*/ }
}

Переопределение свойств класса

Переопределение свойств работает также, как и переопределение методов; все свойства, унаследованные от суперкласса, должны быть помечены ключевым словом override, а также должны иметь совместимый тип. Каждое объявленное свойство может быть переопределено свойством с инициализацией или свойством с get-методом.

open class Shape {
    open val vertexCount: Int = 0
}

class Rectangle : Shape() {
    override val vertexCount = 4
}

Вы также можете переопределить свойство val свойством var, но не наоборот. Это разрешено, поскольку свойство val объявляет get-метод, а при переопределении его как var дополнительно объявляется set-метод в производном классе.

Обратите внимание, что ключевое слово override может быть использовано в основном конструкторе класса как часть объявления свойства.

interface Shape {
    val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape // Всегда имеет 4 вершины

class Polygon : Shape {
    override var vertexCount: Int = 0  // Может быть установлено любое количество
}

Порядок инициализации производного класса

При создании нового экземпляра класса в первую очередь выполняется инициализация базового класса (этому шагу предшествует только оценка аргументов, передаваемых в конструктор базового класса) и, таким образом, происходит до запуска логики инициализации производного класса.

open class Base(val name: String) {

    init { println("Инициализация класса Base") }

    open val size: Int = 
        name.length.also { println("Инициализация свойства size в класса Base: $it") }
}

class Derived(
    name: String,
    val lastName: String
) : Base(name.capitalize().also { println("Аргументы, переданные в конструктор класса Base: $it") }) {

    init { println("Инициализация класса Derived") }

    override val size: Int =
        (super.size + lastName.length).also { println("Инициализация свойства size в классе Derived: $it") }
}

fun main() {
    println("Построение класса Derived(\"hello\", \"world\")")
    val d = Derived("hello", "world")
}

Это означает, что свойства, объявленные или переопределенные в производном классе, не инициализированы к моменту вызова конструктора базового класса. Если какое-либо из этих свойств используется в логике инициализации базового класса (прямо или косвенно через другую переопределенную реализацию члена класса), это может привести к некорректному поведению или сбою во время выполнения. Поэтому при разработке базового класса следует избегать использования членов с ключевым словом open в конструкторах, инициализации свойств и блоках инициализации (init).

Вызов функций и свойств суперкласса

Производный класс может вызывать реализацию функций и свойств своего суперкласса, используя ключевой слово super:

open class Rectangle {
    open fun draw() { println("Рисование прямоугольника") }
    val borderColor: String get() = "black"
}

class FilledRectangle : Rectangle() {
    override fun draw() {
        super.draw()
        println("Заполнение прямоугольника")
    }

    val fillColor: String get() = super.borderColor
}

Во внутреннем классе доступ к суперклассу внешнего класса осуществляется при помощи ключевого слова super, за которым следует имя внешнего класса: super@Outer:

class FilledRectangle: Rectangle() {
    fun draw() { /* ... */ }
    val borderColor: String get() = "black"
    
    inner class Filler {
        fun fill() { /* ... */ }
        fun drawAndFill() {
            [email protected]() // Вызывает реализацию функции draw() класса Rectangle
            fill()
            println("Нарисованный прямоугольник заполнен ${[email protected]} цветом.") // Используется реализация get()-метода свойства borderColor в классе Rectangle
        }
    }
}

Правила переопределения

В Kotlin правила наследования реализации определены следующим образом: если класс наследует многочисленные реализации одного и того члена от ближайших родительских классов, он должен переопределить этот член и обеспечить свою собственную реализацию (возможно, используя одну из унаследованных). Для того, чтобы отметить конкретный супертип (родительский класс), от которого мы наследуем данную реализацию, мы используем ключевое слово super. Для задания имени родительского супертипа используются треугольные скобки, например super<Base>:

open class Rectangle {
    open fun draw() { /* ... */ }
}

interface Polygon {
    fun draw() { /* ... */ } // члены интерфейса открыты ('open') по умолчанию
}

class Square() : Rectangle(), Polygon {
    // Компилятор требует, чтобы функция draw() была переопределена:
    override fun draw() {
        super<Rectangle>.draw() // вызов Rectangle.draw()
        super<Polygon>.draw() // вызов Polygon.draw()
    }
}

Это нормально, наследоваться одновременно от Rectangle и Polygon, но так как у каждого из них есть своя реализация функции draw(), мы должны переопределить draw() в Square и обеспечить нашу собственную реализацию этого метода для устранения получившейся неоднозначности.

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

Класс и некоторые его члены могут быть объявлены как abstract. Абстрактный член не имеет реализации в своём классе. Обратите внимание, что нам не надо аннотировать абстрактный класс или функцию словом open — это подразумевается и так.

Можно переопределить неабстрактный open член абстрактным

open class Polygon {
    open fun draw() {}
}

abstract class Rectangle : Polygon() {
    abstract override fun draw()
}

Объекты-помощники

Если вам нужно написать функцию, которая может быть использована без создания экземпляра класса, которая имела бы доступ к данным внутри этого класса (к примеру, фабричный метод), вы можете написать её как член объявления объекта внутри этого класса.

В частности, если вы объявляете объект-помощник в своём классе, у вас появляется возможность обращаться к членам класса, используя название класса в качестве классификатора.

Прочие классы

Также обратите внимание на:

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

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

При создании новый класс является наследников членов и методов ранее определенного базового класса. Создаваемый путем наследования класс является производным (derived class), который в свою очередь может выступать в качестве базового класса (based class) для создаваемых классов. Если имена методов производного и базового классов совпадают, то методы производного класса перегружают методы базового класса.

При использовании наследования члены и методы кроме свойств public и private могут иметь свойство protected. Для одиночного класса описатели protected и private равносильны. Разница между protected и private проявляется при наследовании. Закрытые члены и методы, объявленные в базовом классе как protected, в производном могут использоваться как открытые (public). Защищенные (protected) члены и методы являются промежуточным вариантом между public и private.

При создании производного класса используется следующий синтаксис:

1
2
3
4
5
6
7
8
9
10
11
12

class name_derived_class:
type_inheritance base_class
{
//закрытые методы и члены класса

public:
//открытые методы и члены класса

protected:
//защищенные методы и члены класса

};

Здесь name_derived_class — имя создаваемого производного класса, type_inheritance — способ наследования, возможны следующие способы наследования: public, private и protected; base_class — имя базового типа. Следует различать тип доступа к элементам в базовом классе и тип наследования.

Типы наследования и типы доступа

Способ доступаСпецификатор в базовом классеДоступ в производном классе
privateprivate
protected
public
нет
private
private
protectedprivate
protected
public
нет
protected
protected
publicprivate
protected
public
нет
protected
public

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

Если в описании класса есть ссылка на описываемый позже класс, то его нужно просто объявить с помощью оператора.

class new_class;

Это описание аналогично описанию прототипов функции.

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148

#include «stdafx.h»
#include <iostream>
#include <math.h>
#define PI 3.14159;
using namespace std;
//базовый класс figure
class figure
{
public:
//n-количество сторон фигуры для окружности n=1
int n;
//p-массив длин сторон фигуры, для окружности в p хранится радиус
float *p;
//конструктор
figure();
//метод вычисления периметра фигуры
float perimetr();
//метод вычисления площади фигуры
virtual float square();
//метод вывода информации о фигуре:
//ее название, периметр, площадь и т.д.
virtual void show_parametri();
};
//конструктор класса фигуры
figure::figure()
{
cout<<» Это абстрактный конструктор «<<endl;
}
//метод вычисления периметра, он будет
//перегружаться только в классе _circle
float figure::perimetr()
{
int i;
float psum;
for (psum=0, i=0; i<n; psum+=p[i], i++)
return psum;
}
//метод вычисления площади, пока он абстрактный, в
//каждом классе будет перегружаться реальным методом
float figure::square()
{
cout<<» Квадратная фигура не абстрактна «<<endl;
return 0;
}
//метод вывода информации о фигуре будет
//перегружаться в каждом производном классе
void figure::show_parametri()
{
cout<<» Абстрактная фигура «;
}
//производный класс _circle (окружность) основанный
//на классе figure
class _circle:public figure
{
public:
//конструктор
_circle();
//перегружаемые методы perimetr(), square(), show_parametri()
float perimetr();
virtual float square();
virtual void show_parametri();
};
//производный класс RecTangle (прямоугольник),
//основанный на классе figure
class RecTangle:public figure
{
public:
//конструктор
RecTangle();
//перегружаемые методы square(), show_parametri()
virtual float square();
virtual void show_parametri();
};
//главная функция
void main()
{
setlocale(LC_ALL,«Rus»);
_circle RR;
RR.show_parametri();
RecTangle PP;
PP.show_parametri();
system(«pause»);
}
//конструктор класса _circle
_circle::_circle()
{
cout<<» Параметры окружности «<<endl;
//в качестве сторон окружности выступает
//единственный параметр радиус
n=1;
p=new float[n];
cout<<» Введите радиус «<<endl;
cin>>p[0];
}
//метод вычисления периметра окружности
float _circle::perimetr()
{
return 2*PI*p[0];
}
//метод вычисления площади окружности
float _circle::square()
{
return PI*p[0]*p[0];
}
//метод вывода параметров окружности
void _circle::show_parametri()
{
//вывод сообщения о том, что это окружность
cout<<» Это окружность «<<endl;
//вывод радиуса окружности
cout<<» Радиус = «<<p[0]<<endl;
//вывод периметра окружности
cout<<» Периметр = «<<perimetr()<<endl;
//вывод площади окружности
cout<<» Площадь = «<<square()<<endl;
}
//конструктор класса RecTangle
RecTangle::RecTangle()
{
cout<<» Параметры прямоугольника «<<endl;
//количество сторон = 4
n=4;
p=new float[n];
//ввод длин сторон прямоугольника
cout<<» Введите длину сторон «;
cin>>p[0]>>p[1];
p[2]=p[0];
p[3]=p[1];
}
//метод вычисления площади прямоугольника
float RecTangle::square()
{
return p[0]*p[1];
}
//метод вывода параметров прямоугольника
void RecTangle::show_parametri()
{
//вывод сообщения о том, что это прямоугольник
cout<<» Это прямоугольник «<<endl;
//вывод длины сторон прямоугольника
cout<<» a= «<<p[0]<<» b= «<<p[1]<<endl;
//вывод периметра прямоугольника. Обратите внимание,
//что в классе RecTangle вызывается метод perimetr()
//базового класса (figure)
cout<<» Периметр = «<<perimetr()<<endl;
//вывод площади прямоугольника
cout<<» Площадь = «<<square()<<endl;
}


Похожие записи:

Наследование (программирование) — это… Что такое Наследование (программирование)?

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

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

Типы наследования

Простое наследование

Класс, от которого произошло наследование, называется базовым или родительским (англ. base class). Классы, которые произошли от базового, называются потомками, наследниками или производными классами (англ. derived class).

В некоторых языках используются абстрактные классы. Абстрактный класс — это класс, содержащий хотя бы один абстрактный метод, он описан в программе, имеет поля, методы и не может использоваться для непосредственного создания объекта. То есть от абстрактного класса можно только наследовать. Объекты создаются только на основе производных классов, наследованных от абстрактного. Например, абстрактным классом может быть базовый класс «сотрудник вуза», от которого наследуются классы «аспирант», «профессор» и т. д. Так как производные классы имеют общие поля и функции (например, поле «год рождения»), то эти члены класса могут быть описаны в базовом классе. В программе создаются объекты на основе классов «аспирант», «профессор», но нет смысла создавать объект на основе класса «сотрудник вуза».

Множественное наследование

При множественном наследовании у класса может быть более одного предка. В этом случае класс наследует методы всех предков. Достоинства такого подхода в большей гибкости. Множественное наследование реализовано в C++. Из других языков, предоставляющих эту возможность, можно отметить Python и Эйфель. Множественное наследование поддерживается в языке UML.

Множественное наследование — потенциальный источник ошибок, которые могут возникнуть из-за наличия одинаковых имен методов в предках. В языках, которые позиционируются как наследники C++ (Java, C# и др.), от множественного наследования было решено отказаться в пользу интерфейсов. Практически всегда можно обойтись без использования данного механизма. Однако, если такая необходимость все-таки возникла, то, для разрешения конфликтов использования наследованных методов с одинаковыми именами, возможно, например, применить операцию расширения видимости — «::» — для вызова конкретного метода конкретного родителя.

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

Большинство современных объектно-ориентированных языков программирования (C#, Java, Delphi и др.) поддерживают возможность одновременно наследоваться от класса-предка и реализовать методы нескольких интерфейсов одним и тем же классом. Этот механизм позволяет во многом заменить множественное наследование — методы интерфейсов необходимо переопределять явно, что исключает ошибки при наследовании функциональности одинаковых методов различных классов-предков.

Единый базовый класс

В ряде языков программирования все классы явно или неявно наследуются от некого базового класса. Smalltalk был одним из первых языков, в которых использовалась эта концепция. К таким языкам относятся Objective-C (NSObject), Perl (UNIVERSAL), Eiffel (ANY), Java (java.lang.Object), C# (System.Object), Delphi (TObject).

Наследование в языках программирования

Visual Basic

Наследование в Visual Basic:

Class A 'базовый класс
End Class
 
Class B : Inherits A    'наследование от A
End Class
 
Noninheritable Class C     'Класс, который нельзя наследовать (final в Java)
End Class
 
MustInherit Class Z 'Класс, который обязательно наследовать (абстрактный класс)
End Class

C++

Наследование в C++:

class A{    //базовый класс
};
 
class B : public A{    //public наследование
};
 
class C : protected A{    //protected наследование
};
 
class Z : private A{    //private наследование
};

В C++ существует три типа наследования: public, protected, private. Спецификаторы доступа членов базового класса меняются в потомках следующим образом:

ANSI ISO IEC 14882 2003

Если класс объявлен как базовый для другого класса со спецификатором доступа public, тогда public члены базового класса доступны как public члены производного класса, protected члены базового класса доступны как protected члены производного класса.

Если класс объявлен как базовый для другого класса со спецификатором доступа protected, тогда public и protected члены базового класса доступны как protected члены производного класса.

Если класс объявлен как базовый для другого класса со спецификатором доступа private, тогда public и protected члены базового класса доступны как private члены производного класса.

\ANSI ISO IEC 14882 2003

Одним из основных преимуществ public-наследования является то, что указатель на классы-наследники может быть неявно преобразован в указатель на базовый класс, то есть для примера выше можно написать:

Эта интересная особенность открывает возможность динамической идентификации типа (RTTI).

Delphi (Object Pascal)

Для использования механизма наследования в Delphi необходимо в объявлении класса справа от слова class указать класс предок:

Предок:

TAncestor = class
private
protected
public
  // Виртуальная процедура
  procedure VirtualProcedure; virtual; abstract; 
  procedure StaticProcedure;
end;

Наследник:

TDescendant = class(TAncestor)
private
protected
public
  // Перекрытие виртуальной процедуры
  procedure VirtualProcedure; override;
  procedure StaticProcedure;
end;

Абсолютно все классы в Delphi являются потомками класса TObject. Если класс-предок не указан, то подразумевается, что новый класс является прямым потомком класса TObject.

Множественное наследование в Delphi частично поддерживается за счёт использования классов-помощников (Сlass Helpers).

Python

Python поддерживает как одиночное, так и множественное наследование. При доступе к атрибуту порядок просмотра производных классов называется порядком разрешения метода (англ. method resolution order)[1].

class Ancestor1(object):   # Предок 1
    def m1(self): pass
class Ancestor2(object):   # Предок 2
    def m1(self): pass
class Descendant(Ancestor1, Ancestor2):   # Наследник
    def m2(self): pass
 
d = Descendant()           # инстанциация
print d.__class__.__mro__  # порядок разрешения метода:
(<class '__main__.Descendant'>, <class '__main__.Ancestor1'>, <class '__main__.Ancestor2'>, <type 'object'>)

С версии Python 2.2 в языке сосуществуют «классические» классы и «новые» классы. Последние являются наследниками object. «Классические» классы будут поддерживаться вплоть до версии 2.6, но удалены из языка в Python версии 3.0.

Множественное наследование применяется в Python, в частности, для введения в основной класс классов-примесей (англ. mix-in).

PHP

Для использования механизма наследования в PHP необходимо в объявлении класса после имени объявляемого класса-наследника указать слово extends и имя класса-предка:

class Descendant extends Ancestor {
}

В случае перекрытия классом-наследником свойств и методов предка, доступ к свойствам и методам предка можно получить с использованием ключевого слова parent:

class A {
  function example() {
    echo "Вызван метод A::example().<br />\n";
  }
}
 
class B extends A {
  function example() {
    echo "Вызван метод B::example().<br />\n";
    parent::example();
  }
}

Objective-C

@interface MyNumber : NSObject { 
   int num;
}
- (int) num;
- (void) setNum: (int) theNum;
@end
 
@implementation
- (id) init { 
   self = [super init];
   return self;
}
 
- (int) num {
   return num;
}
 
- (void) setNum: (int) theNum {
   num = theNum;
}
@end

Переопределенные методы не нужно объявлять в интерфейсе.

Java

Пример наследования от одного класса и двух интерфейсов:

        public class A { }
        public interface I1 { }
        public interface I2 { }
        public class B extends A implements I1, I2 { }

Директива final в объявлении класса делает наследование от него невозможным.

C#

Пример наследования от одного класса и двух интерфейсов:

        public class A { }
        public interface I1 { }
        public interface I2 { }
        public class B : A, I1, I2 { }

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

        public class A<T>
        { }
        public class B : A<int>
        { }
        public class B2<T> : A<T>
        { }

Допустимо также наследование вложенных классов от классов, их содержащих:

    class A
    {
        public class B : A { }
    }

Директива sealed в объявлении класса делает наследование от него невозможным.[2]

Ruby

class Parent
 
  def public_method
    "Public method"
  end
 
  private
 
    def private_method
      "Private method"
    end
 
end
 
class Children < Parent
 
  def public_method
    "Redefined public method"
  end
 
  def call_private_method
    "Ancestor's private method: " + private_method
  end
 
end

Класс Parent является предком для класса Children, у которого переопределен метод public_method.

children = Children.new
children.public_method #=> "Redefined public method"
children.call_private_method #=> "Ancestor's private method: Private method"

Приватные методы предка можно вызывать из наследников.

JavaScript

var Parent = function( data ) {
    this.data = data || false;
    this.public_method = function() { return 'Public Method'; }
}
 
var Child = function() {
    this.public_method = function() { return 'Redefined public method'; }
    this.getData = function() { return 'Data: ' + this.data; }
}
 
Child.prototype = new Parent('test');
var Test = new Child();
 
Test.getData(); // => "Data: test"
Test.public_method(); // => "Redefined public method"
Test.data; // => "test"

Класс Parent является предком для класса Children, у которого переопределен метод public_method. В JavaScript используется прототипное наследование.

Конструкторы и деструкторы

В С++ конструкторы при наследовании вызываются последовательно от самого раннего предка до самого позднего потомка, а деструкторы наоборот — от самого позднего потомка до самого раннего предка.

class First
{
public:
    First()  { cout << ">>First constructor" << endl; }
    ~First() { cout << ">>First destructor" << endl; }
};
 
class Second: public First
{
public:
    Second()  { cout << ">Second constructor" << endl; }
    ~Second() { cout << ">Second destructor" << endl; }
};
 
class Third: public Second
{
public:
    Third()  { cout << "Third constructor" << endl; }
    ~Third() { cout << "Third destructor" << endl; }
};
 
// выполнение кода
Third *th = new Third();
delete th;
 
// результат вывода
/*
>>First constructor
>Second constructor
Third constructor
 
Third destructor
>Second destructor
>>First destructor
*/

См. также

Примечания

Ссылки

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


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

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

Родительский класс — это наследуемый класс, также называемый базовый класс.

Дочерний класс — это класс, наследуемый от другого класса, также называется производным классом.


Создать родительский класс

Любой класс может быть родительским, поэтому синтаксис такой же, как при создании любого другой класс:

Пример

Создайте класс с именем Person с имя и фамилия свойства, и метод printname :

класс Person:
def __init __ (self, fname, lname):
себя.firstname = fname
self.lastname = lname

def printname (self):
print (self.firstname, self.lastname)

# Используйте класс Person для создания объекта, а затем выполнить метод printname:

x = Person («John», «Doe»)
x.printname ()

Попробуй сам »

Создание дочернего класса

Чтобы создать класс, который наследует функциональность от другого класса, отправьте родительский класс в качестве параметра при создании дочернего класс:

Пример

Создайте класс с именем Student , который унаследует свойства и методы из Класс Person :

класс Студент (человек):
балл

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

Теперь класс Student имеет те же свойства и методы, что и класс Person. класс.

Пример

Используйте класс Student для создания объекта, а затем выполните метод printname :

x = Студент («Майк», «Олсен»)
x.printname ()

Попробуй сам »

Добавить функцию __init __ ()

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

Мы хотим добавить к дочернему классу функцию __init __ () (вместо ключевого слова pass ).

Примечание: Функция __init __ () вызывается автоматически каждый раз, когда класс используется для создания нового объекта.

Пример

Добавьте функцию __init __ () в Студент класс:

class Student (Человек):
def __init __ (self, fname, lname):
# добавить свойства и т. д.

Когда вы добавляете функцию __init __ () , дочерний класс больше не будет наследовать родительская функция __init __ () .

Примечание: Детский __init __ () функция отменяет наследование родительского __init __ () функция.

Чтобы сохранить наследование родительского __init __ () функцию, добавьте вызов в родительская __init __ () функция:

Пример

class Student (Человек):
def __init __ (self, fname, lname):
Человек.__init __ (сам, имя, имя)

Попробуй сам »

Теперь мы успешно добавили функцию __init __ () и сохранили наследование родительского класса, и мы готовы добавить функциональность в __init __ () функция.


Используйте функцию super ()

Python также имеет функцию super () , которая заставит дочерний класс унаследовать все методы и свойства от своего родитель:

Пример

class Student (Человек):
def __init __ (self, fname, lname):
супер().__init __ (имя, имя)

Попробуй сам »

Используя функцию super () , вы не должны использовать имя родительского элемента, он автоматически унаследует методы и свойства от своего родителя.


Добавить недвижимость

Пример

Добавьте объект под названием Graduationyear в список Студент класс:

class Student (Человек):
def __init __ (self, fname, lname):
супер().__init __ (fname, lname)
self.graduationyear = 2019

Попробуй сам »

В приведенном ниже примере год 2019 должен быть переменной и передан в Студент класс при создании студенческих объектов. Для этого добавьте еще один параметр в функцию __init __ ():

Пример

Добавьте параметр года и передайте правильный год при создании объектов:

class Студент (человек):
def __init __ (self, fname, lname, year):
супер().__init __ (fname, lname)
self.graduationyear =

год x = Студент («Майк», «Олсен», 2019)

Попробуй сам »

Добавить методы

Пример

Добавьте метод под названием welcome в Студент класс:

class Студент (человек):
def __init __ (self, fname, lname, year):
super () .__ init __ (имя-файла, имя-файла)
self.graduationyear = год

def welcome (self):
print («Добро пожаловать», себя.firstname, self.lastname, «to the class of», self.graduationyear)

Попробуй сам »

Если вы добавляете метод в дочерний класс с тем же именем, что и функция в родительский класс, наследование родительского метода будет отменено.




Итераторы Python


Итераторы Python

Итератор — это объект, содержащий счетное количество значений.

Итератор — это объект, который можно повторять, что означает, что вы можете пройти через все значения.

Технически в Python итератор — это объект, реализующий протокол итератора, состоящий из методов __iter __ () и __next __ () .


Итератор против Iterable

Списки, кортежи, словари и наборы — все это повторяемые объекты. Они повторяются контейнеров , из которых можно получить итератор.

Все эти объекты имеют метод iter () , который используется для получения итератора:

Пример

Вернуть итератор из кортежа и вывести каждое значение:

mytuple = («яблоко», «банан», «вишня»)
myit = iter (mytuple)

print (next (myit))
print (next (myit))
print (next (myit))

Попробуй сам »

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

Пример

Строки также являются повторяемыми объектами, содержащими последовательность символов:

mystr = «банан»
myit = iter (mystr)

print (next (myit))
print (next (myit))
print (next (myit))
print (next (myit))
print (next (myit))
print (next (myit))

Попробуй сам »

Зацикливание через итератор

Мы также можем использовать цикл для для итерации по итерируемому объекту:

Пример

Итерировать значения кортежа:

mytuple = («яблоко», «банан», «вишня»)

для x в mytuple:
печать (x)

Попробуй сам »

Пример

Итерировать символы строки:

mystr = «banana»

для x в mystr:
печать (x)

Попробуй сам »

Цикл для фактически создает объект итератора и выполняет next () метод для каждого цикла.



Создать итератор

Чтобы создать объект / класс в качестве итератора, вы должны реализовать методы __iter __ () и __next __ () к вашему объекту.

Как вы узнали из Python В главе «Классы / объекты» все классы имеют функцию, называемую __init __ () , что позволяет инициализация при создании объекта.

Метод __iter __ () действует аналогично, вы можете выполнять операции (инициализация и т. д.), но всегда должен возвращать объект-итератор сам.

Метод __next __ () также позволяет выполнять операций и должен возвращать следующий элемент в последовательности.

Пример

Создайте итератор, который возвращает числа, начиная с 1, и каждую последовательность. увеличится на единицу (возврат 1,2,3,4,5 и т. д.):

class MyNumbers:
def __iter __ (self):
self.a = 1
return self

def __next __ (self):
х = себя.a
self.a + = 1
return x

myclass = MyNumbers ()
myiter = iter (myclass)

print (next (myiter))
print (next (myiter))
print (next (myiter))
print (next (myiter))
print (следующий (myiter))

Попробуй сам »

StopIteration

Приведенный выше пример будет продолжаться вечно, если у вас будет достаточно операторов next () или если он будет использован в для петли .

Чтобы итерация не продолжалась бесконечно, мы можем использовать StopIteration Заявление .

В методе __next __ () мы можем добавить условие завершения, чтобы вызвать ошибку, если итерация выполняется указанное количество раз:

Пример

Остановить после 20 итераций:

class MyNumbers:
def __iter __ (self):
self.a = 1
return self

def __next __ (self):
если self.a <= 20:
x = self.a
self.a + = 1
возврат x
else:
поднять StopIteration

myclass = MyNumbers ()
myiter = iter (myclass)

для x в myiter:
печать (x)

Попробуй сам »


Наследование Python (с примерами)

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

Наследование — мощная функция объектно-ориентированного программирования.

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


Синтаксис наследования Python

класс BaseClass:
  Кузов базового класса
класс DerivedClass (BaseClass):
  Тело производного класса 

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


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

Чтобы продемонстрировать использование наследования, давайте рассмотрим пример.

Многоугольник — это замкнутая фигура с 3 или более сторонами. Скажем, у нас есть класс Polygon , который определяется следующим образом.

  класс Polygon:
    def __init __ (self, no_of_sides):
        self.n = no_of_sides
        self.sides = [0 для i в диапазоне (no_of_sides)]

    def inputSides (self):
        себя.Стороны = [float (input ("Enter side" + str (i + 1) + ":")) для i в диапазоне (self.n)]

    def dispSides (сам):
        для i в диапазоне (self.n):
            print ("Сторона", i + 1, "is", self.sides [i])  

Этот класс имеет атрибуты данных для хранения количества сторон n и величины каждой стороны в виде списка, называемого сторонами .

Метод inputSides () принимает величину каждой стороны, а dispSides () отображает эти длины сторон.

Треугольник — это многоугольник с 3 сторонами.Итак, мы можем создать класс с именем Triangle , который наследуется от Polygon . Это делает все атрибуты класса Polygon доступными для класса Triangle .

Нам не нужно определять их снова (возможность повторного использования кода). Треугольник можно определить следующим образом.

  класс Треугольник (Многоугольник):
    def __init __ (сам):
        Многоугольник .__ init __ (self, 3)

    def findArea (сам):
        a, b, c = self.sides
        # вычисляем полупериметр
        s = (a + b + c) / 2
        площадь = (s * (s-a) * (s-b) * (s-c)) ** 0.5
        print ('Площадь треугольника% 0.2f'% area)  

Однако класс Triangle имеет новый метод findArea () для поиска и печати площади треугольника. Вот пример выполнения.

  >>> t = Треугольник ()

>>> t.inputSides ()
Введите сторону 1: 3
Введите сторону 2: 5
Введите сторону 3: 4

>>> t.dispSides ()
Сторона 1 - 3,0
Сторона 2 - 5,0
Сторона 3 - 4,0

>>> t.findArea ()
Площадь треугольника 6,00  

Мы видим, что, хотя мы не определили методы вроде inputSides () или dispSides () для класса Triangle отдельно, мы смогли их использовать.

Если атрибут не найден в самом классе, поиск продолжается до базового класса. Это повторяется рекурсивно, если базовый класс сам является производным от других классов.


Переопределение методов в Python

Обратите внимание, что в приведенном выше примере метод __init __ () был определен в обоих классах, Triangle и Polygon . Когда это происходит, метод в производном классе переопределяет метод в базовом классе. То есть __init __ () в Triangle получает предпочтение перед __init__ в Polygon .

Обычно при переопределении базового метода мы склонны расширять определение, а не просто заменять его. То же самое делается путем вызова метода в базовом классе из метода в производном классе (вызов Polygon .__ init __ () из __init __ () в Triangle ).

Лучше использовать встроенную функцию super () . Итак, super () .__ init __ (3) эквивалентно Polygon .__ init __ (self, 3) и является предпочтительным.Чтобы узнать больше о функции super () в Python, посетите функцию Python super ().

Две встроенные функции isinstance () и issubclass () используются для проверки наследования.

Функция isinstance () возвращает True , если объект является экземпляром класса или других классов, производных от него. Каждый класс в Python наследуется от объекта базового класса .

  >>> isinstance (t, Треугольник)
Правда

>>> isinstance (t, Многоугольник)
Правда

>>> isinstance (t, int)
Ложь

>>> isinstance (t, объект)
Правда  

Аналогично, issubclass () используется для проверки наследования классов.

  >>> issubclass (Многоугольник, Треугольник)
Ложь

>>> issubclass (Треугольник, Многоугольник)
Правда

>>> issubclass (булево, интервал)
Правда  
Учебник по программированию на Ruby

- наследование классов и модули

В предыдущей главе мы кратко говорили о наследовании. Наследование - это когда класс наследует поведение от другого класса. Класс, наследующий поведение, называется подклассом, а класс, от которого он наследуется, называется суперклассом.

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

Здесь мы извлекаем метод speak в суперкласс Animal и используем наследование, чтобы сделать это поведение доступным для классов GoodDog и Cat .

  класс Животное
  деф говорить
    "Привет!"
  конец
конец

класс GoodDog  Здравствуйте!
ставит paws.speak # => Здравствуйте!
  

Мы используем символ <, чтобы обозначить, что класс GoodDog наследуется от класса Animal . Это означает, что все методы класса Animal доступны для использования классу GoodDog . Мы также создали новый класс под названием Cat , который также наследуется от Animal . Мы удалили метод speak из класса GoodDog , чтобы использовать метод speak из Animal .

Когда мы запускаем этот код, мы видим правильный результат. Оба класса теперь используют метод языка суперкласса Animal .

Но что, если мы хотим использовать исходный метод speak только из класса GoodDog . Давайте добавим его обратно и посмотрим, что произойдет.

  класс Животное
  деф говорить
    "Привет!"
  конец
конец

класс GoodDog  Спарки говорит arf!
ставит paws.speak # => Здравствуйте!
  

В классе GoodDog мы переопределяем метод speak в классе Animal , потому что Ruby сначала проверяет класс объекта на наличие метода, прежде чем он будет искать в суперклассе. Это означает, что когда мы написали код sparky.speak , он сначала посмотрел на класс sparky , то есть GoodDog .Он нашел там метод talk и использовал его. Когда мы написали код paws.speak , Ruby сначала посмотрел на класс paws , то есть Cat . Он не нашел там метода говорить , поэтому продолжил поиск в суперклассе Cat , Animal . Он нашел метод говорящего в Animal и использовал его. Мы поговорим об этом пути поиска метода более подробно чуть позже.

Наследование может быть отличным способом устранения дублирования в вашей кодовой базе.В сообществе Ruby вы часто встретите аббревиатуру «DRY». Это расшифровывается как «Не повторяйся». Это означает, что если вы обнаружите, что повторяете одну и ту же логику снова и снова в своих программах, есть способы извлечь эту логику в одно место для повторного использования.

Ruby предоставляет нам ключевое слово super для вызова методов ранее в пути поиска методов. Когда вы вызываете super из метода, он ищет в пути поиска метода метод с тем же именем, а затем вызывает его.Давайте посмотрим на быстрый пример того, как это работает:

  класс Животное
  деф говорить
    "Привет!"
  конец
конец

класс GoodDog  "Привет! из класса GoodDog"
  

В приведенном выше примере мы создали простой класс Animal с методом экземпляра speak . Затем мы создали GoodDog , который является подклассом Animal , также с методом экземпляра speak , чтобы переопределить унаследованную версию.Однако в методе подкласса speak мы используем super для вызова метода speak из суперкласса Animal , а затем расширяем функциональность, добавляя текст к возвращаемому значению.

Другой более распространенный способ использования super - это инициализировать . Давайте посмотрим на иллюстрацию этого:

  класс Животное
  attr_accessor: имя

  def инициализировать (имя)
    @name = имя
  конец
конец

класс GoodDog  # 
  

Интересная концепция, которую мы хотим объяснить, - это использование super в классе GoodDog . В этом примере мы используем super без аргументов. Однако метод инициализации , где используется super , принимает аргумент и добавляет новый поворот к способу вызова super . Здесь, помимо поведения по умолчанию, super автоматически пересылает аргументы, которые были переданы методу, из которого вызывается super ( инициализирует метод в классе GoodDog ).На этом этапе super передаст аргумент color в initialize , определенном в подклассе, для суперкласса Animal и вызовет его. Это объясняет наличие @ name = "brown" при создании экземпляра bruno . Наконец, подкласс initialize продолжает устанавливать переменную экземпляра @color .

При вызове с конкретными аргументами, например. super (a, b) , указанные аргументы будут отправлены вверх по цепочке поиска метода.Давайте посмотрим на быстрый пример:

  класс BadDog <Животное
  def initialize (возраст, имя)
    супер (имя)
    @age = возраст
  конец
конец

BadDog.new (2, "медведь") # => # 
  

Это похоже на наш предыдущий пример, с той разницей, что super принимает аргумент, следовательно, переданный аргумент отправляется суперклассу. Следовательно, в этом примере, когда создается класс BadDog , переданный аргумент name («медведь») передается суперклассу и устанавливается в переменную экземпляра @name .

Остался последний поворот. Если вы вызываете super () точно так, как показано - с круглыми скобками - он вызывает метод в суперклассе вообще без аргументов. Если в вашем суперклассе есть метод, который не принимает аргументов, это самый безопасный, а иногда и единственный способ вызвать его:

  класс Животное
  def инициализировать
  конец
конец

класс Медведь <Животное
  def инициализировать (цвет)
    супер()
    @color = цвет
  конец
конец

bear = Bear.new ("black") # => # 
  

Если вы забудете использовать здесь круглые скобки, Ruby вызовет исключение ArgumentError , поскольку количество аргументов неверно.

Еще один способ ОСУШИТЬ код в Ruby - использовать модулей . Мы уже немного видели, как использовать модули, но здесь мы приведем еще несколько примеров.

Извлечение общих методов в суперкласс, как мы делали в предыдущем разделе, - отличный способ моделирования понятий, которые являются естественно иерархическими. Мы привели в пример животных. У нас есть общий суперкласс под названием Animal , который может сохранять все базовое поведение всех животных. Затем мы можем немного расширить модель и, возможно, получить подкласс Mammal от Animal .Мы можем представить всю иерархию классов, как на рисунке ниже.

На приведенной выше диаграмме показано, как выглядит наследование на основе чистого класса. Помните, что цель этого - поместить правильное поведение (то есть методы) в правильный класс, чтобы нам не нужно было повторять код в нескольких классах. Мы можем представить, что все объекты Fish связаны с животными, которые живут в воде, поэтому, возможно, метод swim должен быть в классе Fish .Мы также можем представить, что у всех объектов Mammal будет теплая кровь, поэтому мы можем создать метод под названием warm_blooded? в классе Mammal и вернуть true . Следовательно, объекты Cat и Dog будут иметь доступ к warm_blooded? , который автоматически наследуется от Mammal классами Cat и Dog , но у них не будет доступа к методам в классе Fish .

Этот тип иерархического моделирования в некоторой степени работает, но всегда есть исключения. Например, мы поместили метод swim в класс Fish , но некоторые млекопитающие также могут плавать. Мы не хотим перемещать метод swim в Animal , потому что не все животные плавают, и мы не хотим создавать еще один метод swim в Dog , потому что это нарушает принцип DRY. Для таких проблем мы хотели бы сгруппировать их в модуль, а затем смешать с этим модулем с классами, которым требуется такое поведение.Вот пример:

  модуль для плавания
  def плавать
    "Я плыву!"
  конец
конец

класс Animal; конец

class Fish <Животное
  включить Swimmable # смешивание в Swimmable модуль
конец

class Mammal <Животное
конец

class Cat <Млекопитающее
конец

класс Собака <Млекопитающее
  включить Swimmable # смешивание в Swimmable модуль
конец
  

А теперь объекты Fish и Dog могут плавать, но объекты других классов не смогут:

  Спарки = Собака.новый
neemo = Fish.new
paws = Cat.new

sparky.swim # => Я плаваю!
neemo.swim # => Я плаваю!
paws.swim # => NoMethodError: неопределенный метод `swim 'для # 
  

Использование модулей для группировки общих поведений позволяет нам создавать более мощный, гибкий и СУХОЙ дизайн.

Примечание. Обычным соглашением об именах для Ruby является использование суффикса «способный» для любого глагола, описывающего поведение, моделируемое модулем.Вы можете увидеть это соглашение с нашим модулем Swimmable . Точно так же мы могли бы назвать модуль, который описывает «ходьбу», как Walkable . Не все модули названы таким образом, однако это довольно распространенное явление.

Теперь вы знаете два основных способа, которыми Ruby реализует наследование. Наследование классов - это традиционный способ думать о наследовании: один тип наследует поведение другого типа. Результатом является новый тип, который специализируется на типе суперкласса.Другая форма иногда называется наследование интерфейса : здесь в игру вступают модули миксинов. Класс не наследуется от другого типа, а вместо этого наследует интерфейс, предоставляемый модулем mixin. В этом случае тип результата не является специализированным типом по отношению к модулю.

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

  • Вы можете создать подкласс (наследование класса) только от одного класса.Вы можете смешивать столько модулей (наследование интерфейса), сколько захотите.
  • Если существует связь «is-a», обычно правильным выбором является наследование класса. Если существует связь «есть», обычно лучше использовать наследование интерфейса. Например, собака - это «животное, и она« умеет плавать ».
  • Вы не можете создать экземпляры модулей (т.е. из модуля нельзя создать объект). Модули используются только для размещения имен и группирования общих методов вместе.

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

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

  модуль пешеходный
  деф прогулка
    "Я иду."
  конец
конец

модуль Swimmable
  def плавать
    "Я плыву."
  конец
конец

модуль Альпинистский
  определенно подняться
    «Я лазаю».
  конец
конец

класс Animal
  включить пешеходный

  деф говорить
    "Я животное, и я говорю!"
  конец
конец
  

У нас есть три модуля и один класс.Мы смешали один модуль с классом Animal . Путь поиска метода - это путь, по которому Ruby ищет метод. Мы можем увидеть этот путь с помощью метода класса ancestors .

  помещает "--- Поиск метода животных ---"
помещает Animal.ancestors
  

Результат выглядит так:

  --- Поиск по методу животных ---
Животное
Пешеходный
Объект
Ядро
BasicObject
  

Это означает, что когда мы вызываем метод любого объекта Animal , сначала Ruby просматривает класс Animal , затем модуль Walkable , затем класс Object , затем модуль Kernel и, наконец, BasicObject класс.

  fido = Animal.new
fido.speak # => Я животное, и я говорю!
  

Руби нашла метод speak в классе Animal и больше не стала искать.

  fido.walk # => Я иду.
  

Ruby сначала искал метод экземпляра walk в Animal и, не найдя его там, продолжил поиск в следующем месте в соответствии с нашим списком, которым является модуль Walkable .Он увидел там метод walk , выполнил его и перестал смотреть дальше.

  fido.swim
  # => NoMethodError: неопределенный метод `swim 'для # 
  

Ruby просмотрел все классы и модули в списке и не нашел метод swim , поэтому он выдал ошибку.

Давайте добавим еще один класс в приведенный выше код. Этот класс наследуется от класса Animal и смешивается с модулями Swimmable и Climbable .

  класс GoodDog <Животное
  включить Swimmable
  включать альпинистский
конец

помещает "--- поиск метода GoodDog ---"
ставит GoodDog.ancestors
  

И вот результат:

  --- Поиск метода GoodDog ---
Хороший пес
Альпинистский
Плавать
Животное
Пешеходный
Объект
Ядро
BasicObject
  

В этом выводе есть несколько интересных моментов. Во-первых, это говорит нам о важности порядка, в котором мы включаем модули. На самом деле Ruby просматривает последний модуль, который мы включили , первый .Это означает, что в редких случаях, когда модули, которые мы смешиваем, содержат метод с тем же именем, первым будет обращаться к последнему включенному модулю. Вторая интересная вещь заключается в том, что модуль, включенный в суперкласс, попал в путь поиска метода. Это означает, что все объекты GoodDog будут иметь доступ не только к методам Animal , но и к методам, определенным в модуле Walkable , а также ко всем другим модулям, смешанным с любым из его суперклассов.

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

Мы уже видели, как модули могут использоваться для смешивания общего поведения в классы. Теперь мы увидим еще два использования модулей.

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

  модуль Млекопитающее
  класс Dog
    def говорить (звук)
      p "# {звук}"
    конец
  конец

  класс Cat
    def say_name (имя)
      p "# {имя}"
    конец
  конец
конец
  

Мы вызываем классы в модуле, добавляя имя класса к имени модуля двумя двоеточиями ( :: )

  buddy = Млекопитающее :: Собака.новый
kitty = Млекопитающее :: Cat.new
buddy.speak ('Arf!') # => "Арф!"
kitty.say_name ('котенок') # => "котенок"
  

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

  модуль Млекопитающее
  ...

  def self.some_out_of_place_method (число)
    число ** 2
  конец
конец
  

Такое определение методов в модуле означает, что мы можем вызывать их прямо из модуля:

  значение = Mammal.some_out_of_place_method (4)
  

Мы также можем вызвать такие методы, выполнив:

  значение = Млекопитающее :: some_out_of_place_method (4)
  

, хотя первый вариант является предпочтительным.

Последнее, о чем мы хотим рассказать, это то, что на самом деле довольно просто, но необходимо; Контроль доступа к методам.Контроль доступа - это концепция, которая существует во многих языках программирования, включая Ruby. Обычно это реализуется с помощью модификаторов доступа . Назначение модификаторов доступа - разрешить или ограничить доступ к определенному объекту. В Ruby мы озабочены ограничением доступа к методам, определенным в классе. Таким образом, в контексте Ruby вы обычно увидите эту концепцию, называемую Method Access Control .

Способ реализации управления доступом к методам в Ruby заключается в использовании модификаторов доступа public , private и protected .Сейчас все методы в нашем классе GoodDog являются общедоступными. Открытый метод - это метод, доступный любому, кто знает имя класса или имя объекта. Эти методы легко доступны для использования остальной частью программы и составляют интерфейс класса (именно так другие классы и объекты будут взаимодействовать с этим классом и его объектами).

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

В нашем классе GoodDog у нас есть одна выполняемая операция, которую мы можем переместить в частный метод. Когда мы инициализируем объект, мы вычисляем возраст собаки в собачьих годах. Давайте реорганизуем эту логику в метод и сделаем ее закрытой, чтобы ничто вне класса не могло ее использовать.

  класс GoodDog
  DOG_YEARS = 7

  attr_accessor: name,: age

  def инициализировать (n, a)
    self.name = n
    self.age = a
  конец

  частный

  def human_years
    возраст * DOG_YEARS
  конец
конец

sparky = GoodDog.new ("Спарки", 4)
Sparky.human_years
  

Получаем сообщение об ошибке:

  NoMethodError: закрытый метод `human_years 'вызван
  # 
  

Мы сделали метод human_years закрытым, поместив его в метод private .Тогда для чего это нужно, если мы не можем это назвать? частные методы доступны только из других методов в классе. Например, с учетом приведенного выше кода разрешено следующее:

  # предполагаем, что определение метода ниже находится над "частным" методом

def public_disclosure
  "# {self.name} по человеческому роду - # {human_years}"
конец
  

Обратите внимание, что в этом случае мы можем , а не , использовать self.human_years , потому что метод human_years является частным.Помните, что self.human_years эквивалентно sparky.human_years , что недопустимо для частных методов. Следовательно, нам нужно просто использовать human_years . Таким образом, частные методы вообще недоступны вне определения класса и доступны только изнутри класса при вызове без self .

Начиная с Ruby 2.7, теперь разрешено вызывать частные методы с буквальным self в качестве вызывающего.

Общедоступные и частные методы являются наиболее распространенными, но в некоторых менее распространенных ситуациях нам понадобится промежуточный подход.Мы можем использовать метод protected для создания защищенных методов . Самый простой способ понять защищенные методы - следовать этим двум правилам:

  • изнутри класса, защищенных методов доступны так же, как общедоступных методов .
  • извне класса, защищенных методов действуют так же, как частных методов .

Давайте посмотрим на несколько примеров:

  класс Животное
  def a_public_method
    «Это сработает?» + Я.a_protected_method
  конец

  защищенный

  def a_protected_method
    "Да, я защищен!"
  конец
конец
  

Изучите приведенный выше код, так как он немного сложен. Мы создадим объект Animal и протестируем его.

  fido = Animal.new
fido.a_public_method # => Это сработает? Да, я защищен!
  

Вышеприведенная строка кода показывает нам, что мы можем вызвать метод , защищенный изнутри класса, даже с добавленным self .А что насчет вне класса?

  fido.a_protected_method
  # => NoMethodError: вызываемый защищенный метод `a_protected_method '
    # <Животное: 0x007fb174157110>
  

Это демонстрирует второе правило, что мы не можем вызывать защищенные методы извне класса. Два правила для защищенных методов также применяются в контексте наследования.

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

Важно помнить, что каждый создаваемый вами класс по своей сути является подклассом класса Object. Класс Object встроен в Ruby и имеет множество важных методов.

  класс Родительский
  def say_hi
    p "Привет от родителей."
  конец
конец

Родитель.суперкласс # => Объект
  

Это означает, что методы, определенные в классе Object , доступны в всех классах .

Кроме того, напомним, что с помощью магии наследования подкласс может переопределить метод суперкласса.

  class Child <Родитель
  def say_hi
    p "Привет от ребенка."
  конец
конец

child = Child.new
child.say_hi # => «Привет от ребенка».
  

Это означает, что если вы случайно переопределите метод, изначально определенный в классе Object , это может иметь далеко идущие последствия для вашего кода.Например, send - это метод экземпляра, который все классы наследуют от объекта Object . Если вы определили новый метод экземпляра send в своем классе, все объекты вашего класса будут вызывать ваш собственный метод send вместо метода в классе Object , который, вероятно, они хотят вызвать. Объект send служит способом вызова метода, передавая ему символ или строку, представляющую метод, который вы хотите вызвать. Следующая пара аргументов будет представлять аргументы метода, если таковые имеются.Давайте посмотрим, как обычно работает send, используя наш класс Child :

  сын = Child.new
son.send: say_hi # => «Привет от ребенка».
  

Давайте посмотрим, что произойдет, когда мы определим метод send в нашем классе Child , а затем попытаемся вызвать метод send объекта Object :

  класс Детский
  def say_hi
    p "Привет от ребенка."
  конец

  def отправить
    p "отправить от ребенка ..."
  конец
конец

lad = Ребенок.новый
парень. отправить: say_hi
  

Обычно мы ожидаем, что на выходе этого вызова будет «Привет от ребенка». , но при запуске кода мы получаем совершенно другой результат:

  ArgumentError: неправильное количество аргументов (1 вместо 0)
from (pry): 12: в `отправить '
  

В нашем примере мы передаем send с одним аргументом, хотя наш переопределенный метод send не принимает никаких аргументов. Давайте посмотрим на другой пример, исследуя объект instance_of? метод.Этот удобный метод возвращает true, , если объект является экземпляром данного класса, и false, в противном случае. Посмотрим на это в действии:

  c = Child.new
c.instance_of? Ребенок # => истина
c.instance_of? Родитель # => ложь
  

Теперь давайте переопределим instance_of? внутри Детский :

  класс Детский
  # другие методы пропущены

  def instance_of?
    p "Я подделка"
  конец
конец

наследник = Ребенок.новый
heir.instance_of? Ребенок
  

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

  ArgumentError: неправильное количество аргументов (1 вместо 0)
from (pry): 22: в `instance_of? '
  

Тем не менее, один метод экземпляра Object , который легко переопределить без каких-либо серьезных побочных эффектов, - это метод to_s . Обычно вы захотите сделать это, когда вам нужно другое строковое представление объекта.В целом, важно ознакомиться с некоторыми из распространенных методов Object и убедиться, что вы случайно не переопределите их, поскольку это может иметь разрушительные последствия для вашего приложения.

Мы уже довольно много прошли. Вы должны хорошо чувствовать общий синтаксис и структуру языка Ruby. У вас есть еще один набор упражнений, которые помогут найти хорошее применение этой информации, и тогда вы будете готовы сделать следующий шаг в своем путешествии в качестве разработчика Ruby.

Все эти комплексные знания об ООП призваны помочь нам создавать лучше спроектированные приложения. Хотя определенно существует неправильных способов разработки приложения, часто нет правильного выбора, когда дело доходит до объектно-ориентированного дизайна, только разные компромиссы. По мере того, как вы приобретете больше опыта в объектно-ориентированном дизайне, вы начнете понимать, как организовывать и формировать классы. На данный момент все это может показаться немного пугающим, но как только вы научитесь думать объектно-ориентированным способом, трудно будет думать таким образом , а не .

Наконец, уделите время выполнению упражнений. ООП - сложная концепция, если вы впервые сталкиваетесь с ней. Даже если вы раньше программировали на другом объектно-ориентированном языке, реализация Ruby может немного отличаться. Недостаточно просто прочитать и понять; вы должны учиться на практике. Приступим к упражнениям!

17.2 - Базовое наследование в C ++

Теперь, когда мы поговорили о том, что такое наследование в абстрактном смысле, давайте поговорим о том, как оно используется в C ++.

Наследование в C ++ происходит между классами. В отношении наследования (is-a) класс, от которого унаследован, называется родительским классом , базовым классом или суперклассом , а класс, выполняющий наследование, называется дочерним классом , производным классом . , или подкласс .

На приведенной выше диаграмме Fruit является родительским элементом, а Apple и Banana - дочерними элементами.

На этой диаграмме Triangle является дочерним элементом (для Shape) и родительским (для Right Triangle).

Дочерний класс наследует поведение (функции-члены) и свойства (переменные-члены) от родительского (с учетом некоторых ограничений доступа, которые мы рассмотрим в следующем уроке).
Эти переменные и функции становятся членами производного класса.

Поскольку дочерние классы являются полноценными классами, они могут (конечно) иметь свои собственные члены, специфичные для этого класса. Мы скоро увидим пример этого.

A Персональный класс

Вот простой класс для представления обычного человека:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

14

18

#include

class Person

{

// В этом примере мы делаем наших участников общедоступными для простоты

public:

std :: string m_name {};

int m_age {};

Человек (const std :: string & name = "", int age = 0)

: m_name {name}, m_age {age}

{

}

const std :: string & getName ( ) const {return m_name; }

int getAge () const {return m_age; }

};

Поскольку этот класс Person разработан для представления общего человека, мы определили только члены, которые будут общими для любого типа человека.У каждого человека (независимо от пола, профессии и т. Д.) Есть имя и возраст, поэтому они здесь представлены.

Обратите внимание, что в этом примере мы сделали все наши переменные и функции общедоступными. Это сделано исключительно для того, чтобы сейчас эти примеры были простыми. Обычно мы делаем переменные приватными. Мы поговорим об элементах управления доступом и о том, как они взаимодействуют с наследованием, позже в этой главе.

Класс игрока в бейсбол

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

Вот наш неполный класс бейсболиста:

class BaseballPlayer

{

// В этом примере мы делаем наших участников общедоступными для простоты

общедоступными:

double m_battingAverage {};

int m_homeRuns {};

BaseballPlayer (среднее значение двойного удара = 0.0, int homeRuns = 0)

: m_battingAverage {battingAverage}, m_homeRuns {homeRuns}

{

}

};

Теперь мы также хотим отслеживать имя и возраст бейсболиста, и у нас уже есть эта информация как часть нашего класса Person.

У нас есть три варианта добавления имени и возраста в BaseballPlayer:
1) Добавить имя и возраст в класс BaseballPlayer непосредственно в качестве членов. Вероятно, это худший вариант, поскольку мы дублируем код, который уже существует в нашем классе Person.Любые обновления для Person также должны быть сделаны в BaseballPlayer.
2) Добавить человека в качестве члена BaseballPlayer с помощью композиции. Но мы должны спросить себя: «Есть ли у BaseballPlayer личность»? Нет, это не так. Так что это неправильная парадигма.
3) Попросите BaseballPlayer унаследовать эти атрибуты от Person. Помните, что наследование представляет собой отношения. Бейсболист - это человек? Да, это. Так что наследование здесь - хороший выбор.

Создание производного класса BaseballPlayer

Чтобы BaseballPlayer унаследовал от нашего класса Person, синтаксис довольно прост.После объявления класса BaseballPlayer мы используем двоеточие, слово «public» и имя класса, который мы хотим унаследовать. Это называется публичное наследство . Мы поговорим подробнее о том, что означает публичное наследование, на следующем уроке.

// BaseballPlayer публично наследует Person

class BaseballPlayer: public Person

{

public:

double m_battingAverage {};

int m_homeRuns {};

BaseballPlayer (среднее значение двойного удара = 0.0, int homeRuns = 0)

: m_battingAverage {battingAverage}, m_homeRuns {homeRuns}

{

}

};

Используя диаграмму деривации, наше наследование выглядит так:

Когда BaseballPlayer наследуется от Person, BaseballPlayer получает функции-члены и переменные от Person. Кроме того, BaseballPlayer определяет два собственных члена: m_battingAverage и m_homeRuns.Это имеет смысл, поскольку эти свойства специфичны для BaseballPlayer, а не для любого человека.

Таким образом, объекты BaseballPlayer будут иметь 4 переменных-члена: m_battingAverage и m_homeRuns из BaseballPlayer, а также m_name и m_age из Person.

Это легко доказать:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

14

18

19

20

21

22

23

24

25

26

27

28

29

30

34

35

36

37

38

39

40

41

42

43

#include

#include

class Person

{

public:

std :: string m_name {};

int m_age {};

Человек (const std :: string & name = "", int age = 0)

: m_name {name}, m_age {age}

{

}

const std :: string & getName ( ) const {return m_name; }

int getAge () const {return m_age; }

};

// BaseballPlayer публично наследует Person

class BaseballPlayer: public Person

{

public:

double m_battingAverage {};

int m_homeRuns {};

BaseballPlayer (среднее значение двойного удара = 0.0, int homeRuns = 0)

: m_battingAverage {battingAverage}, m_homeRuns {homeRuns}

{

}

};

int main ()

{

// Создайте новый объект BaseballPlayer

BaseballPlayer joe {};

// Присвойте ему имя (мы можем сделать это напрямую, потому что m_name публично)

joe.m_name = "Joe";

// Распечатайте имя

std :: cout << joe.getName () << '\ n'; // используем функцию getName (), полученную из базового класса Person

return 0;

}

Что печатает значение:

 Джо
 

Это компилируется и запускается, потому что joe является BaseballPlayer, а все объекты BaseballPlayer имеют переменную-член m_name и функцию-член getName (), унаследованную от класса Person.

Производный класс Employee

Теперь напишем еще один класс, который также наследуется от Person.На этот раз мы напишем класс Employee. Сотрудник «является» человеком, поэтому использование наследования уместно:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

14

// Сотрудник публично наследует от Person

class Employee: public Person

{

public:

double m_hourlySalary {};

long m_employeeID {};

Сотрудник (двойная почасовая зарплата = 0.0, long employeeID = 0)

: m_hourlySalary {hourlySalary}, m_employeeID {employeeID}

{

}

void printNameAndSalary () const

{

9000 <<5 m_std_name "<< m_hourlySalary << '\ n';

}

};

Employee наследует m_name и m_age от Person (а также две функции доступа) и добавляет еще две переменные-члены и собственную функцию-член.Обратите внимание, что printNameAndSalary () использует переменные как из класса, которому он принадлежит (Employee :: m_hourlySalary), так и из родительского класса (Person :: m_name).

Это дает нам диаграмму вывода, которая выглядит так:

Обратите внимание, что Employee и BaseballPlayer не имеют прямых отношений, хотя оба они наследуют от Person.

Вот полный пример использования Employee:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

14

18

19

20

21

22

23

24

25

26

27

28

29

30

34

35

36

37

38

39

40

41

42

43

44

45

#include

#include

class Person

{

public:

std :: string m_name {};

int m_age {};

const std :: string & getName () const {return m_name; }

int getAge () const {return m_age; }

Человек (const std :: string & name = "", int age = 0)

: m_name {name}, m_age {age}

{

}

};

// Сотрудник публично наследует от Person

class Employee: public Person

{

public:

double m_hourlySalary {};

long m_employeeID {};

Сотрудник (двойная почасовая зарплата = 0.0, long employeeID = 0)

: m_hourlySalary {hourlySalary}, m_employeeID {employeeID}

{

}

void printNameAndSalary () const

{

9000 <<5 m_std_name "<< m_hourlySalary << '\ n';

}

};

int main ()

{

Франк сотрудника {20.25, 12345};

frank.m_name = "Фрэнк"; // мы можем это сделать, потому что m_name публично

frank.printNameAndSalary ();

возврат 0;

}

Это отпечатки:

 Франк: 20,25
 

Цепи наследования

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

Например, давайте напишем класс Supervisor. Супервайзер - это сотрудник, то есть человек.Мы уже написали класс Employee, поэтому давайте используем его в качестве базового класса для наследования Supervisor:

.

class Supervisor: public Employee

{

public:

// Этот Supervisor может контролировать максимум 5 сотрудников

long m_overseesIDs [5] {};

};

Теперь наша диаграмма вывода выглядит так:

Все объекты Supervisor наследуют функции и переменные как Employee, так и Person, и добавляют свои собственные переменные-члены m_overseesIDs.

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

Чем полезен такой вид наследования?

Наследование от базового класса означает, что нам не нужно переопределять информацию из базового класса в наших производных классах. Мы автоматически получаем функции-члены и переменные-члены базового класса через наследование, а затем просто добавляем дополнительные функции или переменные-члены, которые нам нужны.Это не только экономит работу, но также означает, что если мы когда-либо обновим или изменим базовый класс (например, добавим новые функции или исправим ошибку), все наши производные классы автоматически унаследуют изменения!

Например, если мы когда-нибудь добавим новую функцию в Person, и Employee, и Supervisor автоматически получат к ней доступ. Если мы добавим новую переменную в Employee, Supervisor также получит к ней доступ. Это позволяет нам создавать новые классы простым, интуитивно понятным и не требующим обслуживания способом!

Заключение

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


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

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

Наследование проявляется, когда новый класс имеет отношение «IS A» с существующим классом.

Собака - это животное. Кошка тоже ЯВЛЯЕТСЯ животным. Следовательно, animal - это базовый класс, а dog и cat - унаследованные классы.

У четырехугольника четыре стороны. Прямоугольник ЯВЛЯЕТСЯ четырехугольником, а значит, и квадратом. Четырехугольник - это базовый класс (также называемый родительским классом), а прямоугольник и квадрат - это унаследованные классы, также называемые дочерними классами.

Дочерний класс наследует определения данных и методы родительского класса. Это облегчает повторное использование уже доступных функций. Дочерний класс может добавить еще несколько определений или переопределить метод базового класса.

Эта функция чрезвычайно полезна при построении иерархии классов для объектов в системе. Также возможно разработать новый класс на основе нескольких существующих классов.Эта функция называется множественным наследованием.

Общий механизм установления наследования проиллюстрирован ниже:

 родитель класса:
    заявления
                    
дочерний класс (родитель):
    заявления
 

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

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

Класс четырехугольника, имеющий четыре стороны в качестве переменных экземпляра и метод perimeter (), определяется ниже:

  класс quadriLateral:
    def __init __ (self, a, b, c, d):
        себя.сторона1 = а
        self.side2 = b
        self.side3 = c
        self.side4 = d

    периметр определения (себя):
        p = self.side1 + self.side2 + self.side3 + self.side4
        print ("периметр =", p)
  

Конструктор (метод __init __ () ) получает четыре параметра и назначает их четырем переменным экземпляра. Чтобы протестировать вышеуказанный класс, объявите его объект и вызовите метод perimeter () .

>>> q1 = quadriLateral (7,5,6,4)
>>> q1.perimeter ()
периметр = 22

Теперь мы проектируем класс прямоугольника на основе класса quadriLateral (прямоугольник ЯВЛЯЕТСЯ четырехугольником!). Переменные экземпляра и метод perimeter () из базового класса должны быть автоматически доступны ему без его переопределения.

Поскольку противоположные стороны прямоугольника одинаковы, нам нужны только две смежные стороны, чтобы построить его объект. Следовательно, два других параметра метода __init __ () не установлены. Метод __init __ () передает параметры конструктору своего базового (четырехугольного) класса с помощью функции super () . Объект инициализируется с установкой side3 и side4 в значение none.Противоположные стороны уравниваются конструктором класса прямоугольник. Помните, что он автоматически унаследовал метод perimeter () , поэтому нет необходимости его переопределять.

  прямоугольник класса (quadriLateral):
    def __init __ (self, a, b):
        super () .__ init __ (а, б, а, б)
  

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

>>> r1 = прямоугольник (10, 20)
>>> r1.perimeter ()
периметр = 60

Переопределение в Python

В приведенном выше примере мы видим, как повторно используются ресурсы базового класса при создании унаследованного класса. Однако у унаследованного класса могут быть свои собственные атрибуты и методы экземпляра.

Методы родительского класса доступны для использования в унаследованном классе. Однако при необходимости мы можем изменить функциональность любого метода базового класса. Для этого унаследованный класс содержит новое определение метода (с тем же именем и подписью, уже присутствующими в базовом классе). Естественно, объект нового класса будет иметь доступ к обоим методам, но объект из его собственного класса будет иметь приоритет при вызове.Это называется переопределением метода.

Сначала мы определим новый метод с именем area () в классе прямоугольника и будем использовать его в качестве основы для класса square . Площадь прямоугольника равна произведению его смежных сторон.

  прямоугольник класса (QuadriLateral):
    def __init __ (self, a, b):
        super () .__ init __ (а, б, а, б)

    область определения (self):
        а = себя.сторона1 * self.side2
        print ("площадь прямоугольника =", а)
  

Давайте определим квадратный класс, который наследует класс прямоугольника. Метод area () переопределяется для реализации формулы для площади квадрата как квадрата его сторон.

  квадрат класса (прямоугольник):
    def __init __ (self, a):
        super () .__ init __ (а, а)
    область определения (self):
        a = pow (self.сторона1, 2)
        print ('Площадь квадрата:', a)
 
 
>>> s = Квадрат (10)
>>> s.area ()
Площадь Квадрата: 100

Наследование - Кристалл

Каждый класс, кроме Объект , корень иерархии, наследуется от другого класса (его суперкласса). Если вы не укажете его, по умолчанию используется Ссылка для классов и Struct для структур.

Класс наследует все переменные экземпляра и все методы экземпляра и класса суперкласса, включая его конструкторы (, новый и инициализируют ).

  класс Человек
  def инициализировать (@ имя: строка)
  конец

  def приветствовать
    помещает "Привет, я # {@ name}"
  конец
конец

класс Сотрудник <Человек
конец

employee = Employee.new "Джон"
employee.greet # "Привет, я Джон"
  

Если класс определяет новый или инициализирует , тогда его конструкторы суперкласса не наследуются:

  класс Человек
  def инициализировать (@ имя: строка)
  конец
конец

класс Сотрудник <Человек
  def initialize (@name: String, @company_name: String)
  конец
конец

Работник.новый "Джон", "Акме" # ОК
Employee.new "Peter" # Ошибка: неверное количество аргументов для 'Employee: Class # new' (1 вместо 2)
  

Вы можете переопределить методы в производном классе:

  класс Человек
  def приветствовать (сообщение)
    помещает "Привет, # {msg}"
  конец
конец

класс Сотрудник <Человек
  def приветствовать (сообщение)
    помещает "Привет, # {msg}"
  конец
конец

p = Person.new
p.greet "all" # "Всем привет"

e = Employee.new
egreet "all" # "Привет всем"
  

Вместо переопределения вы можете определить специализированные методы, используя ограничения типа:

  класс Человек
  def приветствовать (сообщение)
    помещает "Привет, # {msg}"
  конец
конец

класс Сотрудник <Человек
  def greet (сообщение: Int32)
    помещает "Привет, это число: # {msg}"
  конец
конец

e = Сотрудник.новый
egreet "all" # "Всем привет"

egreet 1 # "Привет, это число: 1"
  

супер

Вы можете вызвать метод суперкласса, используя super :

  класс Человек
  def приветствовать (сообщение)
    помещает "Привет, # {msg}"
  конец
конец

класс Сотрудник <Человек
  def приветствовать (сообщение)
    super # То же, что: super (msg)
    super ("другое сообщение")
  конец
конец
  

Без аргументов или круглых скобок super получает все параметры метода в качестве аргументов.В противном случае он получает аргументы, которые вы ему передаете.

Ковариация и контравариантность

Наследование одного места может быть немного сложнее с массивами. Мы должны быть осторожны при объявлении массива объектов, в котором используется наследование. Например, рассмотрим следующие

  класс Foo
конец

класс Bar  [# ]: массив (Foo)
bar_arr = [Bar.new] # => [# ]: массив (столбец)
bar_arr2 = [Фу.новое] ошибки компилятора Bar #
  

Массив Foo может содержать как Foo, так и Bar, но массив Bar может содержать только Bar и его подклассы.

Одно из мест, где это может вас сбить с толку, - это когда в игру вступает автоматический кастинг. Например, не сработает:

  класс Foo
конец

класс Bar  

мы объявили @arr как тип Array (Foo) , поэтому у нас может возникнуть соблазн подумать, что мы можем начать помещать туда Bar .Не совсем. В инициализации тип выражения [Bar.new] - Массив (столбец) , точка. И Array (Bar) - это , а не , который можно назначить для экземпляра Array (Foo) var.

Как правильно это сделать? Измените выражение так, чтобы оно было правильного типа: Array (Foo) (см. Пример выше).

  класс Foo
конец

класс Bar  

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

.
Оставить комментарий

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *