Назовите класс net от которого наследуются все классы
Перейти к содержимому

Назовите класс net от которого наследуются все классы

  • автор:

Вопросы на собеседовании по C#

Вопросы на собеседовании по C#

Аспектно-ориентированное Аспектно-ориентированное програмирование базируется на изучении того сколько и каких компонент или частей требуется для взаимодействия с системой. Основным для АОП является взаимодействие этих элементов. Интерфейсно-ориентированное Интерфейсно-ориентированное программирование это подход основанный на программировании по контракту. Ни одна часть интерфейса не зависит от того как реализована другая, вся коммуникация между ними описана в контракте. Веб-сервисы на основе WSDL наиболее известный пример этого подхода. Объектно-ориентированное Объектно-ориентированное программирование основывается на понятиях абстракции, инкапсуляции, полиморфизма и наследования. Классы реализуют эти концепции для построение объектов, контролирующих или реализующих систему. Абстракция позволяет уменьшить связывание между компонентами пердоставляя дополнительный слой между ними, для того чтоб один объект не зависил от того, как другой реализуюет свои бизнес-правила. (Интерфейсы, слои) Отличная штука, если вы хотите изолировать части системы так, чтоб их изменение или замена не стали проблеммой для работы системы в целом. Инкапсуляция позволяет работать абстракции пряча детали реализации класса от вызывающих его. (Публичные и приватные поля) Наследование позволяет базовым(родительским) классам содержать общую функциональность и передавать ее всем своим классам-потомкам. Класс Figure(фигура) может иметь поле цвет, которое будет унаследовано его классами-потомками Square(квадрат) или Circle(круг). Полиморфизм позволяет реализовывать одноименные публичные методы, позволяя различным классам выполнять различные действия при одном и том же вызове. То есть объекты классов Square и Circle могут отображаться(реализовывать метод render) поразному несмотря не то, что они оба подклассы Shape, метод render определен в Shape. (Overriding)

Что такое CLR? Что такое IL? Что такое CLS?

CLR («Common Language Runtime», «общеязыковая исполняющая среда») — это компонент .NET Framework, основной задачей которого является управление интерпретацией и исполнением кода IL. CLR отвечает за изоляцию памяти приложений, проверку типов, безопасность кода, преобразование IL в машинный код. IL (Intermediate Language) — код, содержащий набор инструкций, не зависящих от платформы. Иными словами, после компиляции исходного кода он преобразуется не в код для какой-то определенной платформы, а в промежуточный код на языке IL. CLS («Common Language Specification», общеязыковая спецификация) — это набор правил, следуя которым разработчики достигают бесконфликтной работы во всех языках .NET.

Что такое managed code?

Управляемый код (managed code) — это код, работающий в среде CLR. Содержит метаданные, в которых находится информация для среды выполнения — о типах, членах и ссылках, используемых в коде.

Что такое assembly?

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

Что такое приватные и совместные сборки?

Сборки (assembly) бывают двух типов — приватные (private), которые использует только само приложение, и совместные (shared), использующиеся набором приложений. При приватных сборках приложение изолируется от внешнего воздействия программ и операционной системы, отпадает необходимость заботиться об уникальности имен в глобальном пространстве имен. Чтобы сделать сборку совместной, ее необходимо специальным образом собрать и присвоить ей строгое шифрованное имя.

Что такое assembly manifest?

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

В чем разница между понятиями namespace и assembly?

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

В чем различие между Value Type и Reference Type?

Value Type находятся в стеке, а Reference Type в куче.

Когда объект удаляется сборщиком мусора?

Объект удаляется сборщиком мусора, когда на него не остается ссылок.

Что такое Code Access Security (CAS)?

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

Что такое attribute?

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

Как обеспечить использование именованных параметров в конструкторе атрибута?

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

[DllImport("Kernel32", CharSet=CharSet.Auto, SetLastError=true)]

Здесь передается 1 позиционный параметр — строка «Kernel32» и 2 именованных параметра, которые устанавливают открытые поля CharSet и SetLastError в значения CharSet.Auto и true соответственно.

В чем различие между Finalize и Dispose?

Dispose — обеспечивает явный контроль над ресурсами, используемыми объектом, а Finalize — неявный, используемый сборщиком мусора.

Что такое Boxing и Unboxing?

  1. Выделяется память в управляемой куче.
  2. Совершается копирование полей размерного типа в память, которая была выделена в куче.
  3. Возвращается адрес объекта.
struct Point < public Int32 x, у; >class App < static void Main() < ArrayList a = new ArrayList(); Point p; // Выделяется память для Point (не в куче). for (Int32 i = 0 ; i < 10; i++) < p.x = p. у = i; // Инициализация членов в нашем размерном типе. a.Add(p); // Упаковка размерного типа и добавление ссылки в ArrayList. >Point р1 = (Point) a[0]; // распаковка и копирование полей >

Упаковка и распаковка/копирование снижают производительность приложения как в плане замедления, так и в расходе дополнительной памяти, поэтому нужно стараться минимизировать создание кода, в котором происходят операции упаковки и распаковки/копирования.

Что такое GAC?

GAC — это глобальный кэш сборок. В нем хранятся совместно используемые сборки. Обычно это каталог С:\Windows\Assembly\GAC. Этот каталог имеет определенную структуру, в котором хранятся подкаталоги, имена которых сгенерированы по определенному алгоритму. В GAC можно поместить только сборки со строгими именами. Для того, чтобы поместить сборку в GAC, используют специальный инструмент GACUtil.exe, который знает всю внутреннюю структуру GAC и может генерировать имена подкаталогов надлежащим образом. Регистрировать в GAC сборки необходимо для того, чтобы избежать конфликтов имен сборок. Приведем пример: две компании выпустили сборку и назвали ее одним именем Calculus. Если мы скопируем эту сборку в каталог, в котором уже находится сборка с таким же именем, то мы затрем сборку, которая ранее могла использоваться каким-то приложением. Это приложение с новой сборкой теперь работать не сможет. Решением этой проблемы будет регистрация этих двух сборок в GAC, в котором для каждой будет создан отдельный каталог.

Какие типы можно использовать в предложении foreach?

Массивы, коллекции. Классы в которых реализован интерфейс System.Collections.IEnumerable.

В чем различие между классом и структурой?

Для С# классы System.Object, System.Exception, System.File-Stream и System.Random — это ссылочные типы (память выделяется из упр. кучи). В свою очередь размерные типы в документации называются структурами (structure) и перечислениями (enumeration). Например, структуры System.In132, System.Boolean, System.Decimal, System.TimeSpan и перечисления System.DayOfWeek, System.10.FileAttributes и System.Drawing.FontStyle являются размерными типами (хранятся обычно в стеке потока, но могут быть встроены в ссылочные типы).

Что означает модификатор virtual?

При наследовании класса. Данный метод м.б. переопределен в производных классах с помощью ключевого слова override.

Чем отличается event от delegate?

Delegate – это по сути указатель на функцию.

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

Может ли класс реализовать два интерфейса, у которых объявлены одинаковые методы? Каким образом?

public class GiuseppePizzaria : IWindow, IRestaurant < // Реализация метода GetMenu интерфейса IWindow. Object IWindow.GetMenu() < . >// Реализация метода GetMenu интерфейса IRestaurant. Object IRestaurant.GetMenu() < . >>

Поддерживает ли C# множественное наследование?

С# поддерживает множественное наследование в виде наследования от класса и нескольких интерфейсов, или просто от нескольких интерфейсов.

Но не поддерживает наследование от нескольких классов.

Кому доступны переменные с модификатором protected на уровне класса?

Наследуются ли переменные с модификатором private?

Да, но они не являются доступными.

Опишите модификатор “protected internal”

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

Назовите класс .NET, от которого наследуются все классы?

Что обозначает термин immutable (неизменяемый)?

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

Какая разница между классами System.String и System.Text.StringBuilder?

Данные, хранящиеся в классе System.String и есть неизменяемые (immutable). Класс System.StringBuilder разрабатывался так, чтобы над изменяемой строкой можно было проделать множество операций. То есть при каждой операции над объектом класса System.String происходит перенос данных в новую область памяти, что влияет на производительность программы.

Какое преимущество использования класса System.Text.StringBuilder перед System.String?

Класс StringBuilder более эффективен в случае работы с большим количеством строк. Объекты класса System.String неизменяемы, поэтому при каждом изменении строки создаётся новый объект в памяти.

Можно ли хранить разные типы данных в объекте класса System.Array?

static void Main(string[] args) < object[] arr = new object[] < "string", 0, new Guid() >; foreach (object var in arr) < Console.WriteLine(var.GetType().ToString()); >Console.ReadKey(); double d = 0.0; ValueType[] arr2 = new ValueType[] < d, 0, new Guid() >; foreach (ValueType var in arr2) < Console.WriteLine(var.GetType().ToString()); >Console.ReadKey(); > 

Объясните разницу между System.Array.CopyTo() и System.Array.Clone()?

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

Как отсортировать элементы массива в убывающем порядке?

Нужно вызвать метод Sort(), а затем метод Reverse().

As, is – что это, как применяется?

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

Выражение o is Employee проверяет, является ли переменная o объектом типа Employee.

Операторы приведения типов.

If(o is Employee)

Employee e = (Employee) o;

>

В чем разница между throw ex; и throw; ?

  • throw повторно выбрасывает исключение (re-throw), которое было обнаружено, и сохраняет трассировку стека (путь к источнику исключения).
  • throw ex генерирует одно и то же исключение, но сбрасывает трассировку стека на метод, где делается throw ex.

Как работает return в try-catch-finally?

Алгоритм приблизительно такой:

  1. Выполняется код перед оператором return;
  2. Выражение в return оценивается
  3. Выполняется finally блок
  4. Возвращается результат, посчитанный на шаге 2

Рассмотрим следующий код:

public static async Task Main() < Console.WriteLine(MyMethod().Text); Console.ReadKey(); >public static TestClass MyMethod() < var number = new TestClass < Text = "5" >; try < return number; >catch < number.Text = "haha"; return number; >finally < number.Text = "finally"; >>

Что мы получим на экране?

finally

почему так? Мы не меняли результат который был получен на шаге 2, мы изменили значение свойства ссылочного типа, но не меняли сам объект

Если мы перепишем блок finally следующим образом:

. finally < number = new TestClass ; >

то получим в ответ

Если мы проделаем это с value type, результат будет тем же. Т.е в блоке finally нельзя изменить возвращаемый объект, но можно менять свойства объекта.

Вопросы по классам

Какой синтаксис используется для указания класса родителя в C#?

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

class ChildClass : ParentClass

Можно ли запретить наследование от своего собственного класса?

Да. Для этого служит ключевое слово “sealed”.

Можно ли разрешить наследование класса, но запретить перекрытие метода?

Да. Указываем класс как public, а метод как sealed.

Что такое абстрактный класс?

Это класс, объект которого не может быть создан. Такой класс должен иметь класс-наследник с реализацией абстрактных методов. Абстрактный класс – это фактически чертёж нормального класса без реализации.

В каком случае вы обязаны объявить класс абстрактным?

  1. В том случае, если класс является наследником абстрактного класса, но не все методы базового класса перекрыты и имеют реализацию.
  2. В том случае, если хотя бы один метод класса является абстрактным.

Что такое интерфейс класса?

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

Почему нельзя указать модификатор видимости для методов интерфейса?

Потому что все они должны иметь модификатор public, который и установлен по умолчанию.

Можно ли наследовать от нескольких интерфейсов?

Назовите отличия между интерфейсом и абстрактным классом?

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

Назовите различия между структурами и классами.

Для С# классы System.Object, System.Exception, System.File-Stream и System.Random — это ссылочные типы (память выделяется из упр. кучи). В свою очередь размерные типы в документации называются структурами (structure) и перечислениями (enumeration). Например, структуры System.In132, System.Boolean, System.Decimal, System.TimeSpan и перечисления System.DayOfWeek, System.10.FileAttributes и System.Drawing.FontStyle являются размерными типами (хранятся обычно в стеке потока, но могут быть встроены в ссылочные типы).

В чем разница между абстрактными и виртуальными классами? Между виртуальными и абстрактными методами?

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

Dispose(), Finalize() – что это за методы, как используются в .NET?

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

Для чего в .NET используется конструкция using(…)? Причем тут IDisposable?

Значение Using непосредственно связано с интерфейсом IDisposable. Интерфейс IDisposable представляет нам возможность быстро освободить общие ресурсы, не полагаясь на автоматический сборщик мусора (garbage collector). Конструкция Using позволяет вызывать метод Dispose автоматически, как только нужный объект выйдет за блок Using.

Вопросы по методам и свойствам

Назовите явное имя параметра, передаваемого в метод set свойства класса?

value. Тип этого параметра определяется типом свойства.

Что обозначает ключевое слово “virtual” для метода или свойства?

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

Чем перекрытый метод отличается от перегруженного метода?

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

Можно ли объявить перекрытый метод статическим, если перекрываемый метод не является статическим?

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

Вопросы по сборкам

Что такое «сопутствующая сборка» (satellite assembly)?

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

Какая наименьшая исполнимая единица в .NET?

Что происходит в памяти при упаковке и распаковке значимого типа?

  1. В управляемой куче выделяется память. Ее объем определяется длиной размерного типа и некоторыми накладными расходами, позволяющими этому размерному типу стать настоящим объектом. Этими накладными расходами являются указатель на таблицу методов и индекс SyncBlocklndex.
  2. Поля размерного типа копируются в память, выделенную только что в куче.
  3. Возвращается адрес объекта. Этот адрес является ссылкой на объект; размерный тип превратился в ссылочный.

Объектно-ориентированное программирование

Наследование (inheritance) является одним из ключевых моментов ООП. Благодаря наследованию один класс может унаследовать функциональность другого класса.

Пусть у нас есть следующий класс Person, который описывает отдельного человека:

class Person < private string _name = ""; public string Name < get < return _name; >set < _name = value; >> public void Print() < Console.WriteLine(Name); >>

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

class Employee : Person

После двоеточия мы указываем базовый класс для данного класса. Для класса Employee базовым является Person, и поэтому класс Employee наследует все те же свойства, методы, поля, которые есть в классе Person. Единственное, что не передается при наследовании, это конструкторы базового класса с параметрами.

Таким образом, наследование реализует отношение is-a (является), объект класса Employee также является объектом класса Person:

Person person = new Person < Name = "Tom" >; person.Print(); // Tom person = new Employee < Name = "Sam" >; person.Print(); // Sam

И поскольку объект Employee является также и объектом Person, то мы можем так определить переменную: Person p = new Employee() .

По умолчанию все классы наследуются от базового класса Object , даже если мы явным образом не устанавливаем наследование. Поэтому выше определенные классы Person и Employee кроме своих собственных методов, также будут иметь и методы класса Object: ToString(), Equals(), GetHashCode() и GetType().

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

  • Не поддерживается множественное наследование, класс может наследоваться только от одного класса.
  • При создании производного класса надо учитывать тип доступа к базовому классу — тип доступа к производному классу должен быть таким же, как и у базового класса, или более строгим. То есть, если базовый класс у нас имеет тип доступа internal , то производный класс может иметь тип доступа internal или private , но не public . Однако следует также учитывать, что если базовый и производный класс находятся в разных сборках (проектах), то в этом случае производый класс может наследовать только от класса, который имеет модификатор public.
  • Если класс объявлен с модификатором sealed , то от этого класса нельзя наследовать и создавать производные классы. Например, следующий класс не допускает создание наследников:

sealed class Admin

Доступ к членам базового класса из класса-наследника

Вернемся к нашим классам Person и Employee. Хотя Employee наследует весь функционал от класса Person, посмотрим, что будет в следующем случае:

class Employee : Person < public void PrintName() < Console.WriteLine(_name); >>

Этот код не сработает и выдаст ошибку, так как переменная _name объявлена с модификатором private и поэтому к ней доступ имеет только класс Person . Но зато в классе Person определено общедоступное свойство Name, которое мы можем использовать, поэтому следующий код у нас будет работать нормально:

class Employee : Person < public void PrintName() < Console.WriteLine(Name); >>

Таким образом, производный класс может иметь доступ только к тем членам базового класса, которые определены с модификаторами private protected (если базовый и производный класс находятся в одной сборке), public , internal (если базовый и производный класс находятся в одной сборке), protected и protected internal .

Ключевое слово base

Теперь добавим в наши классы конструкторы:

class Person < public string Name < get; set;>public Person(string name) < Name = name; >public void Print() < Console.WriteLine(Name); >> class Employee : Person < public string Company < get; set; >public Employee(string name, string company) : base(name) < Company = company; >>

Класс Person имеет конструктор, который устанавливает свойство Name. Поскольку класс Employee наследует и устанавливает то же свойство Name, то логично было бы не писать по сто раз код установки, а как-то вызвать соответствующий код класса Person. К тому же свойств, которые надо установить в конструкторе базового класса, и параметров может быть гораздо больше.

С помощью ключевого слова base мы можем обратиться к базовому классу. В нашем случае в конструкторе класса Employee нам надо установить имя и компанию. Но имя мы передаем на установку в конструктор базового класса, то есть в конструктор класса Person, с помощью выражения base(name) .

Person person = new Person("Bob"); person.Print(); // Bob Employee employee = new Employee("Tom", "Microsoft"); employee.Print(); // Tom

Конструкторы в производных классах

Конструкторы не передаются производному классу при наследовании. И если в базовом классе не определен конструктор по умолчанию без параметров, а только конструкторы с параметрами (как в случае с базовым классом Person), то в производном классе мы обязательно должны вызвать один из этих конструкторов через ключевое слово base. Например, из класса Employee уберем определение конструктора:

class Employee : Person < public string Company < get; set; >= ""; >

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

class Employee : Person < public string Company < get; set; >= ""; public Employee(string name, string company) // ! Ошибка < Name = name; Company = company; >>

То есть в классе Employee через ключевое слово base надо явным образом вызвать конструктор класса Person:

class Employee : Person < public string Company < get; set; >= ""; public Employee(string name, string company) : base(name) < Company = company; >>

Либо в качестве альтернативы мы могли бы определить в базовом классе конструктор без параметров:

class Person < public string Name < get; set; >// конструктор без параметров public Person() < Name = "Tom"; Console.WriteLine("Вызов конструктора без параметров"); >public Person(string name) < Name = name; >public void Print() < Console.WriteLine(Name); >>

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

public Employee(string company)

Фактически был бы эквивалентен следующему конструктору:

public Employee(string company) :base()

Порядок вызова конструкторов

При вызове конструктора класса сначала отрабатывают конструкторы базовых классов и только затем конструкторы производных. Например, возьмем следующие классы:

class Person < string name; int age; public Person(string name) < this.name = name; Console.WriteLine("Person(string name)"); >public Person(string name, int age) : this(name) < this.age = age; Console.WriteLine("Person(string name, int age)"); >> class Employee : Person < string company; public Employee(string name, int age, string company) : base(name, age) < this.company = company; Console.WriteLine("Employee(string name, int age, string company)"); >>

При создании объекта Employee:

Employee tom = new Employee("Tom", 22, "Microsoft");

Мы получим следующий консольный вывод:

Person(string name) Person(string name, int age) Employee(string name, int age, string company)

В итоге мы получаем следующую цепь выполнений.

  1. Вначале вызывается конструктор Employee(string name, int age, string company) . Он делегирует выполнение конструктору Person(string name, int age)
  2. Вызывается конструктор Person(string name, int age) , который сам пока не выполняется и передает выполнение конструктору Person(string name)
  3. Вызывается конструктор Person(string name) , который передает выполнение конструктору класса System.Object, так как это базовый по умолчанию класс для Person.
  4. Выполняется конструктор System.Object.Object() , затем выполнение возвращается конструктору Person(string name)
  5. Выполняется тело конструктора Person(string name) , затем выполнение возвращается конструктору Person(string name, int age)
  6. Выполняется тело конструктора Person(string name, int age) , затем выполнение возвращается конструктору Employee(string name, int age, string company)
  7. Выполняется тело конструктора Employee(string name, int age, string company) . В итоге создается объект Employee

Вопросы и ответы для собеседования по Kotlin. Часть 1

  • Кратко о Nothing
  • Назовите подтип всех типов в Kotlin
  • Сколько существует instance Nothing (0)?
  • Есть ли аналог Nothing в Java (нет)?

Преимущества языка Kotlin перед Java

Код на Kotlin компактнее на 30-40%

Меньше кода = меньше ошибок, выше скорость разработки.

Безопасная работа с обнуляемыми переменными (Null Safety)

В отличие от Java, в Kotlin по умолчанию все типы являются non-nullable, то есть не могут принимать значение null. Присвоение или возврат null приведет к ошибке компиляции. Чтобы присвоить переменной значение null, в Kotlin необходимо явно пометить эту переменную как nullable (добавив после типа знак вопроса). В Java же при использовании ссылки на объект с указанным значением null, появляется исключение в виде «NullPointerException!».

Функции-расширения (Extensions)

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

Классы данных (data classes)

Разработчику на Java приходится писать много стандартного, но часто встречающегося кода (т.н. шаблонный код или boilerplate). В Kotlin же есть возможность создания специальных классов для определения полей для хранения данных, конструктора, функций сеттеров и геттеров для каждого поля, и функций Hashcode(), toString() и equals(). Для этого достаточно добавить data в определение класса, затем компилятор сделает все сам.

Синглтоны на уровне языка (Object)

В Java все должно объявляться внутри класса. Но в Kotlin все иначе. Компоненты могут объявляться за пределами класса, и это автоматически делает их статическими. Поэтому нам не требуется ключевое слово static. В Java статические члены обрабатываются не так, как члены-объекты. Это означает, что для статических членов нам недоступны такие вещи, как реализация интерфейса, помещение экземпляра в ассоциативный список (map) или передача его в качестве параметра методу, который принимает объект. В Kotlin static не является ключевым словом и вместо статических членов используются объекты-компаньоны, позволяющие преодолеть вышеуказанные ограничения. В этом и заключается преимущество. Даже если члены объектов-компаньонов выглядят как статические члены в других языках, во время выполнения они все равно остаются членами экземпляров реальных объектов и могут, например, реализовывать интерфейсы.

Kotlin предоставляет возможность создавать дополнительные потоки, однако в нем также существуют т.н. корутины (сопрограммы), которые позволяют использовать меньше памяти в сравнении с обычным потоком, т.к. реализованы они без стека. Корутины же в свою очередь способны выполнять интенсивные и длительные задачи методом приостановления выполнения без блокировки потока и его последующего восстановления. Что в дальнейшем позволяет сгенерировать асинхронный код без блокирования, который при его выполнении не отличить от синхронного. К тому же, они генерируют эффектные доп. стили например async или await.

Дополнительно:

Разница между Exception в Java и Kotlin

Одним из ключевых отличий между Java и Kotlin является подход к исключениям. В Java есть два типа исключений: checked и unchecked.

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

С точки зрения исключений компилятор Kotlin отличается тем, что не различает checked и unchecked исключения. Все исключения — только unchecked, поэтому нет необходимости отлавливать или объявлять какие-либо исключения (вы самостоятельно принимаете решение, стоит ли их отлавливать и обрабатывать).

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

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

Как перенести статичные методы из Java в Kotlin?

В Kotlin нет статических методов, для этих целей обычно служит companion object .
Для того чтобы метод из Java был представлен как статический используется аннотация @JvmStatic . Эта аннотация говорит компилятору Kotlin создать статический метод в байт-коде, что позволяет использовать методы так же, как в Java.

Например, если у нас есть статический метод в Java:

public class MyClass < public static int sum(int a, int b) < return a + b; >>

Мы можем использовать этот метод в Kotlin, добавив аннотацию @JvmStatic :

object MyClass < @JvmStatic fun sum(a: Int, b: Int): Int < return a + b >>

В какой модификатор преобразуется internal в Java?

В Java нет эквивалента модификатору доступа internal из Kotlin. При компиляции Kotlin-кода в Java-байткод, модификатор доступа internal преобразуется в модификатор public в Java.

Таким образом, все члены класса, отмеченные как internal , будут видны из любого места в том же пакете, а также из любого другого модуля, которому был разрешен доступ к этому модулю. Члены internal классов проходят через искажение имен, чтобы усложнить случайное использование их из Java и позволить перегрузку для членов с одинаковыми сигнатурами, которые не видят друг друга в соответствии с правилами Kotlin.

Отличия в проверке на равенство == и equals()

1. Проверка на равенство в Java

Структурное равенство (значение) — метод equals().
Ссылочное равенство — оператор == :
— примитивные типы данных: сравнивает значения переменных
— ссылочные типы данных (объекты, массивы): сравнивает ссылки

2. Проверка на равенство в Kotlin

Структурное равенство (значение) — оператор == (проверка через equals() )
Ссылочное равенство — оператор === :
— примитивные типы данных: сравнивает значения переменных
— ссылочные типы данных (объекты, массивы): сравнивает ссылки

3. Разница == с Java

Структурное равенство (значение) — оператор == в Kotlin это equals() в Java, т.е. в Kotlin строки можно всегда сравнивать через == .
Ссылочное равенство — оператор === в Kotlin это == в Java.

Кратко про анонимные классы и объекты, object и companion object

Анонимный класс — это класс, которые явно не объявлен с помощью class , наследуется от заданного класса или реализует заданный интерфейс.

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

val obj = object : SuperClassOrInterface() < // implementation here >

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

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

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

Ключевое слово object позволяет одновременно объявить класс и создать его экземпляр (т.е. объект). При этом применять его можно по-разному:

  • object Name — это объявление объекта (оbject declaration), реализация паттерна Singleton;
  • companion object — это объект-компаньон внутри класса (также Singleton);
  • object — это объект-выражение (анонимный объект/object expression), не Singleton.

Объявление объекта (object declaration), object как Singleton

Объявляется объект при помощи ключевого слова object , после которого следует имя объекта .

Файл, содержащий только object представляет из себя Singleton, т.е. будет создан только один экземпляр этого класса. Пример:

object One < val cats = arrayListOf() fun callCat() < for (cat in cats) < . >> >

Можно обращаться к методам и свойствам класса через имя объекта:

One.cats.add(Cat(. )) One.callCat()

Инициализация объявления объекта потокобезопасна и выполняется при первом доступе (лениво).

Сompanion object (также Singleton)

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

Синглтон-свойство companion object достигается за счет того, что он создается внутри класса в качестве статического поля. Он будет инициализирован при первом обращении к нему или при создании первого экземпляра класса, в котором он объявлен.

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

class MyClass < init < // Выполняется всегда после инициализации companion object >companion object < init < // Выполняется всегда перед блоком init содержащего класса >> > val myClass = MyClass()

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

class SomeClass < companion object < fun create() >> val someClass = SomeClass.create()

Компилируется в public static final class на Java. Работает подобно ключевому слову static в Java.

Объект-выражение (анонимный объект/object expression)

Объект-выражение — это выражение, которое «на ходу» создает анонимный объект.

Для объекта-выражения не указывается имя!
Если же объекту всё-таки требуется имя, то его можно сохранить в переменной:

val tom = object < val name = "Tom" var age = 37 fun sayHello() < println("Hi, my name is $name") >> println("Name: $ Age: $") tom.sayHello()

Анонимные объекты не являются синглтонами!
Каждый раз при выполнении объекта-выражения создаётся новый объект.

Анонимный объект является заменой анонимным внутренним классам в Java.

Разница между анонимным и декларируемым (объявляемым) объектом

  • анонимный объект ( object ) инициализируется непосредственно при использовании;
  • декларированный (объявляемый) объект ( object Name ) инициализируется лениво, в момент первого к нему доступа;
  • вспомогательный объект ( companion object ) инициализируется в момент, когда класс, к которому он относится, загружен и семантически совпадает со статическим инициализатором Java.

Аннотация @JvmStatic

С помощью аннотации @JvmStatic есть возможность объявить методы по настоящему статическими, ее можно добавить как к методам object , так и к методам companion object .

object ObjectWithStatic < @JvmStatic fun staticFun(): Int < return 5 >>

В этом случае метод staticFun будет действительно объявлен статическим:

public final class ObjectWithStatic < public static final ObjectWithStatic INSTANCE; @JvmStatic public static final int staticFun() < return 5; >private ObjectWithStatic() < INSTANCE = (ObjectWithStatic)this; >static < new ObjectWithStatic(); >> 

Что такое Null safety, nullable и non-nullable типы?

Null safety — это концепция безопасности, которая предотвращает некоторые из наиболее распространенных ошибок в программировании, связанных с использованием null-значений. В Kotlin эта концепция реализуется за счет строгой типизации и системы Nullable/Non-nullable типов данных.

Nullable типы — это типы, которые могут содержать значение null . Non-nullable типы — это типы, которые не могут содержать значение null и всегда должны иметь некоторое значение.

В Kotlin переменные по умолчанию являются non-nullable — это означает, что они не могут принимать значение null . Если переменная может принимать значение null , то ее нужно объявить с использованием знака вопроса (?). При использовании Nullable переменной в коде Kotlin не допустит обращение к ней без предварительной проверки на null-значение.

Также Kotlin предоставляет множество функций для безопасной работы с nullable-значениями, таких как операторы elvis ?: , безопасный вызов ?. и другие.

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

Способы проверки значения на null (if-else, операторы «?.», «. «, «?:»)

Kotlin разграничивает типы с поддержкой и без поддержки null-значений. Это означает, что при объявлении переменной, которая может хранить null, нужно явно объявить ее как nullable при помощи символа ? .

val languageName: String? = null

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

Рассмотрим все доступные способы проверки значения на null.

1. Проверка с помощью if-else

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

if(languageName != null) < print("Name is : $languageName") >else

Использование оператора безопасного вызова будет предпочтительнее, так как он позволяет решить проблему меньшим количеством кода. Однако, если со значением переменной производятся какие-то сложные вычисления и перед началом вычислений нужно проверить равно ли оно null, то if-else вполне подойдёт.

2. Оператор безопасного вызова «?.»

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

languageName?.length

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

Если вы хотите вызвать функцию или каким-то другим способом обработать значение, отличное от null, то совместно с оператором безопасного вызова используйте функцию let. Всё, что будет указано в функции let выполнится только в том случае, если значение переменной отлично от null.

languageName?.let
3. Оператор «!!»

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

Данный оператор понравится любителям NullPointerException. Он как бы говорит компилятору, что если значение переменной — null, то ТРЕБУЮ выбросить NullPointerException.

val languageName: String? = null val size = languageName. length

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

4. Элвис оператор или оператор объединения по null «?:»

Оператор элвис подобен проверке на null в варианте if-else. Элвис используется для замены null каким-либо значением, принадлежащим обычно зауженному типу. В результате выражение с элвисом позволяет не увеличивать в программе количество nullable-переменных.

Оператор указывается между двумя значениями. Если значение слева от оператора равно null, то применяется значение справа.

val size: Int = languageName.length ?: 0

Если значение languageName не равно null, его длина будет присвоена переменной size .

Если languageName равно null, тогда будет присвоено значение 0.

Но в любом случае переменной size будет присвоено значение типа Int, а не Int?, то есть non-null тип.

Использование данного оператора с функцией let может полностью заменить проверку с помощью оператора if-else.

// с использованием if-else if(languageName != null) < print("Name is : $languageName") >else < print("Please enter a valid name") >// Элвис оператор и функция let languageName?.let < print("Name is : $languageName") >?: print("Please enter a valid name")

От какого класса унаследованы все остальные классы в Kotlin?

Класс Any находится на вершине иерархии — все классы в Kotlin являются наследниками Any . Это стандартный родительский класс для всех классов, которые явно не унаследованы от другого класса. Именно в нем определены equals , hashCode и toString . Класс Any по назначению похож на Object в Java.

public open class Any

Чем Any в Kotlin отличается от Object в Java?

Any не является полным аналогом java.lang.Object .

В Object 11 методов в классе, в Any только 3 метода: equals() , hashCode() и toString() . При импорте типов Java в Kotlin все ссылки типа java.lang.Object преобразуются в Any . Поскольку Any не зависит от платформы, он объявляет только toString() , hashCode() и equals() в качестве своих членов, поэтому, чтобы сделать другие члены java.lang.Object доступными, Kotlin использует функции расширения.

Несмотря на то, что классы Object и Any имеют сходства (корневые классы иерархии классов), они также имеют и отличия, связанные с языковыми особенностями Kotlin и Java:

  1. Класс Any в Kotlin является не только базовым классом для пользовательских классов, но также и супертипом для всех не-nullable типов данных, включая примитивные. В то время как в Java, класс Object является базовым классом только для пользовательских классов.
  2. Класс Any в Kotlin также имеет nullable версию Any? , которая является супертипом для всех nullable типов данных в Kotlin. В то время как в Java, класс Object не имеет nullable версии.

Какой тип находится на вершине иерархии типов в Kotlin?

Аналогично Object в Java, к чему можно привести любой тип в Kotlin?
Правильным ответом будет Any? .

Сам по себе класс Any это почти аналог Object , однако, благодаря поддержке nullable и не-nullable типов в Kotlin мы получили Any? . Фактически, Any? соответствует любому типу и null , а Any только любому типу.

Если по порядку:

  1. Any является корнем иерархии не-nullable типов.
  2. Any? является корнем иерархии nullable типов.
  3. Так как Any? является супертипом Any , то Any? находится в самом верху иерархии типов в Kotlin.

Картинка для понимания:

Кратко о Unit

Тип Unit в Kotlin выполняет ту же функцию, что и void в Java.

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

fun knockKnock() < println("Who’s there?") >// то же самое, но с указанным типом Unit fun knockKnock(): Unit = println("Who’s there?") 

Сколько существует instance Unit (1)?

В стандартной библиотеке Kotlin Unit определён как объект, наследуемый от Any и содержащий единственный метод, переопределяющий toString() :

public object Unit

Unit является синглтоном (ключевое слово object ). Unit ничего не возвращает, а метод toString всегда будет возвращать “kotlin.Unit” . При компиляции в java-код Unit всегда будет превращаться в void .

Кратко о Nothing

Nothing является типом, который полезен при объявлении функции, которая ничего не возвращает и не завершается.

  • функция, которая выбрасывает exception или в которой запущен бесконечный цикл;
  • функция TODO() — public inline fun TODO(): Nothing = throw NotImplementedError() ;
  • в тестах есть функция с именем fail , которая выдает исключение с определенным сообщением:
fun fail(message: String): Nothing

Назовите подтип всех типов в Kotlin

Nothing в Kotlin — это т.н. bottom type, то есть он является подтипом любого другого типа. Наличие Nothing в системе типов позволяет типизировано выражать то, что без него принципиально невозможно.

Bottom type — это тип, который не имеет значений и предназначен для обозначения невыполнимых ситуаций в программе.

Сколько существует instance Nothing (0)?

Nothing — класс, который является наследником любого класса в Kotlin, даже класса с модификатором final . При этом Nothing нельзя создать — у него приватный конструктор. В коде он объявлен так:

public class Nothing private constructor() 

Есть ли аналог Nothing в Java (нет)?

Тип Nothing является особенным, поскольку в Java ему нет аналогов.

Действительно, каждый ссылочный тип Java, включая java.lang.Void , принимает в качестве значения null , а Nothing не принимает даже этого. Таким образом, этот тип не может быть точно представлен в мире Java. Вот почему Kotlin генерирует необработанный тип, в котором используется аргумент типа Nothing :

fun emptyList(): List = listOf() // is translated to // List emptyList()

Наследование в C++: beginner, intermediate, advanced

В этой статье наследование описано на трех уровнях: beginner, intermediate и advanced. Expert нет. И ни слова про SOLID. Честно.

Beginner

Что такое наследование?

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

Класс, который наследует данные, называется подклассом (subclass), производным классом (derived class) или дочерним классом (child). Класс, от которого наследуются данные или методы, называется суперклассом (super class), базовым классом (base class) или родительским классом (parent). Термины “родительский” и “дочерний” чрезвычайно полезны для понимания наследования. Как ребенок получает характеристики своих родителей, производный класс получает методы и переменные базового класса.

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

В этом примере, метод turn_on() и переменная serial_number не были объявлены или определены в подклассе Computer . Однако их можно использовать, поскольку они унаследованы от базового класса.

Важное примечание: приватные переменные и методы не могут быть унаследованы.

#include using namespace std; class Device < public: int serial_number = 12345678; void turn_on() < cout private: int pincode = 87654321; >; class Computer: public Device <>; int main() < Computer Computer_instance; Computer_instance.turn_on(); cout 

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

В C ++ есть несколько типов наследования:

  • публичный ( public )- публичные ( public ) и защищенные ( protected ) данные наследуются без изменения уровня доступа к ним;
  • защищенный ( protected ) — все унаследованные данные становятся защищенными;
  • приватный ( private ) — все унаследованные данные становятся приватными.

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

#include using namespace std; class Device < public: int serial_number = 12345678; void turn_on() < cout >; class Computer: private Device < public: void say_hello() < turn_on(); cout >; int main() < Device Device_instance; Computer Computer_instance; cout 

Класс Computer теперь использует метод turn_on() как и любой приватный метод: turn_on() может быть вызван изнутри класса, но попытка вызвать его напрямую из main приведет к ошибке во время компиляции. Для базового класса Device , метод turn_on() остался публичным, и может быть вызван из main .

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

В C ++ конструкторы и деструкторы не наследуются. Однако они вызываются, когда дочерний класс инициализирует свой объект. Конструкторы вызываются один за другим иерархически, начиная с базового класса и заканчивая последним производным классом. Деструкторы вызываются в обратном порядке.

Важное примечание: в этой статье не освещены виртуальные десктрукторы. Дополнительный материал на эту тему можно найти к примеру в этой статье на хабре.

#include using namespace std; class Device < public: // constructor Device() < cout // destructor ~Device() < cout >; class Computer: public Device < public: Computer() < cout ~Computer() < cout >; class Laptop: public Computer < public: Laptop() < cout ~Laptop() < cout >; int main()

Конструкторы: Device -> Computer -> Laptop .
Деструкторы: Laptop -> Computer -> Device .

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

Множественное наследование происходит, когда подкласс имеет два или более суперкласса. В этом примере, класс Laptop наследует и Monitor и Computer одновременно.

#include using namespace std; class Computer < public: void turn_on() < cout >; class Monitor < public: void show_image() < cout >; class Laptop: public Computer, public Monitor <>; int main()

Проблематика множественного наследования

Множественное наследование требует тщательного проектирования, так как может привести к непредвиденным последствиям. Большинство таких последствий вызваны неоднозначностью в наследовании. В данном примере Laptop наследует метод turn_on() от обоих родителей и неясно какой метод должен быть вызван.

#include using namespace std; class Computer < private: void turn_on() < cout >; class Monitor < public: void turn_on() < cout >; class Laptop: public Computer, public Monitor <>; int main() < Laptop Laptop_instance; // Laptop_instance.turn_on(); // will cause compile time error return 0; >

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

Intermediate

Проблема ромба

Проблема ромба (Diamond problem)- классическая проблема в языках, которые поддерживают возможность множественного наследования. Эта проблема возникает когда классы B и C наследуют A , а класс D наследует B и C .

К примеру, классы A , B и C определяют метод print_letter() . Если print_letter() будет вызываться классом D , неясно какой метод должен быть вызван — метод класса A , B или C . Разные языки по-разному подходят к решению ромбовидной проблем. В C ++ решение проблемы оставлено на усмотрение программиста.

Ромбовидная проблема — прежде всего проблема дизайна, и она должна быть предусмотрена на этапе проектирования. На этапе разработки ее можно разрешить следующим образом:

  • вызвать метод конкретного суперкласса;
  • обратиться к объекту подкласса как к объекту определенного суперкласса;
  • переопределить проблематичный метод в последнем дочернем классе (в коде — turn_on() в подклассе Laptop ).
#include using namespace std; class Device < public: void turn_on() < cout >; class Computer: public Device <>; class Monitor: public Device <>; class Laptop: public Computer, public Monitor < /* public: void turn_on() < cout // uncommenting this function will resolve diamond problem */ >; int main() < Laptop Laptop_instance; // Laptop_instance.turn_on(); // will produce compile time error // if Laptop.turn_on function is commented out // calling method of specific superclass Laptop_instance.Monitor::turn_on(); // treating Laptop instance as Monitor instance via static cast static_cast( Laptop_instance ).turn_on(); return 0; >

Если метод turn_on() не был переопределен в Laptop, вызов Laptop_instance.turn_on() , приведет к ошибке при компиляции. Объект Laptop может получить доступ к двум определениям метода turn_on() одновременно: Device:Computer:Laptop.turn_on() и Device:Monitor:Laptop.turn_on() .

Проблема ромба: Конструкторы и деструкторы

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

#include using namespace std; class Device < public: Device() < cout >; class Computer: public Device < public: Computer() < cout >; class Monitor: public Device < public: Monitor() < cout >; class Laptop: public Computer, public Monitor <>; int main()

Виртуальное наследование

Виртуальное наследование (virtual inheritance) предотвращает появление множественных объектов базового класса в иерархии наследования. Таким образом, конструктор базового класса Device будет вызван только единожды, а обращение к методу turn_on() без его переопределения в дочернем классе не будет вызывать ошибку при компиляции.

#include using namespace std; class Device < public: Device() < cout void turn_on() < cout >; class Computer: virtual public Device < public: Computer() < cout >; class Monitor: virtual public Device < public: Monitor() < cout >; class Laptop: public Computer, public Monitor <>; int main()

Примечание: виртуальное наследование в классах Computer и Monitor не разрешит ромбовидное наследование если дочерний класс Laptop будет наследовать класс Device не виртуально ( class Laptop: public Computer, public Monitor, public Device <>; ).

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

В С++, класс в котором существует хотя бы один чистый виртуальный метод (pure virtual) принято считать абстрактным. Если виртуальный метод не переопределен в дочернем классе, код не скомпилируется. Также, в С++ создать объект абстрактного класса невозможно — попытка тоже вызовет ошибку при компиляции.

#include using namespace std; class Device < public: void turn_on() < cout virtual void say_hello() = 0; >; class Laptop: public Device < public: void say_hello() < cout >; int main() < Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.say_hello(); // Device Device_instance; // will cause compile time error return 0; >

Интерфейс

С++, в отличии от некоторых ООП языков, не предоставляет отдельного ключевого слова для обозначения интерфейса (interface). Тем не менее, реализация интерфейса возможна путем создания чистого абстрактного класса (pure abstract class) — класса в котором присутствуют только декларации методов. Такие классы также часто называют абстрактными базовыми классами (Abstract Base Class — ABC).

#include using namespace std; class Device < public: virtual void turn_on() = 0; >; class Laptop: public Device < public: void turn_on() < cout >; int main() < Laptop Laptop_instance; Laptop_instance.turn_on(); // Device Device_instance; // will cause compile time error return 0; >

Advanced

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

Наследование от реализованного или частично реализованного класса

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

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

Интерфейс

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

Интерфейс: Пример использования

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

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

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

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

Заключение

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

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *