Как обратиться к классу с

от admin

Name already in use

csharp_for_beginners / c11.md

  • Go to file T
  • Go to line L
  • Copy path
  • Copy permalink
  • Open with Desktop
  • View raw
  • Copy raw contents Copy raw contents

Copy raw contents

Copy raw contents

ГЛАВА 11. Наследование

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

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

Поддержка наследования в C# состоит в том, что в объяв­ ление одного класса разрешается вводить другой класс. Для этого при объявлении производного класса указывается базовый класс. Рассмотрим для начала простой пример. Ниже приведен класс TwoDShape, содержащий ширину и высоту двухмерного объекта, например квадрата, пря­ моугольника, треугольника и т.д.

Класс TwoDShape может стать базовым, т.е. отправной точкой для создания классов, описывающих конкретные типы двумерных объектов. Например, в приведенной ниже программе класс TwoDShape служит для порождения производного класса Triangle. Обратите особое внимание на объявление класса Triangle.

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

В классе Triangle создается особый тип объекта класса TwoDShape (в данном слу­ чае — треугольник). Кроме того, в класс Triangle входят все члены класса TwoDShape, к которым, в частности, добавляются методы Area() и ShowStyle(). Так, описание типа треугольника сохраняется в переменной Style, метод Area() рассчитывает и воз­ вращает площадь треугольника, а метод ShowStyle() отображает тип треугольника. Обратите внимание на синтаксис, используемый в классе Triangle для наследова­ ния класса TwoDShape.

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

В класс Triangle входят все члены его базового класса TwoDShape, и поэтому в нем переменные Width и Height доступны для метода Area(). Кроме того, объекты t1 и t2 в методе Main() могут обращаться непосредственно к переменным Width и Height, как будто они являются членами класса Triangle. На рис. 11.1 схематически показано, каким образом класс TwoDShape вводится в класс Triangle.

Рис. 11.1. Схематическое представление класса Triangle

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

Разумеется, объект класса TwoDShape никак не связан с любым из классов, произво­ дных от класса TwoDShape, и вообще не имеет к ним доступа.

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

Для любого производного класса можно указать только один базовый класс. В C# не предусмотрено наследование нескольких базовых классов в одном производном клас­ се. (В этом отношении C# отличается от C++, где допускается наследование нескольких базовых классов. Данное обстоятельство следует принимать во внимание при переносе кода C++ в С#.) Тем не менее можно создать иерархию наследования, в которой про­ изводный класс становится базовым для другого производного класса. (Разумеется, ни один из классов не может быть базовым для самого себя как непосредственно, так и косвенно.) Но в любом случае производный класс наследует все члены своего базового класса, в том числе переменные экземпляра, методы, свойства и индексаторы.

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

В класс Rectangle входят все члены класса TwoDShape, к которым добавлен метод IsSquare(), определяющий, является ли прямоугольник квадратом, а также метод Area(), вычисляющий площадь прямоугольника.

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

Как пояснялось в главе 8, члены класса зачастую объявляются закрытыми, чтобы исключить их несанкционированное или незаконное использование. Но наследование класса не отменяет ограничения, накладываемые на доступ к закрытым членам класса. Поэтому если в производный класс и входят все члены его базового класса, в нем все равно оказываются недоступными те члены базового класса, которые являются закры­ тыми. Так, если сделать закрытыми переменные класса TwoDShape, они станут недо­ ступными в классе Triangle, как показано ниже.

// Использовать открытые свойства для установки и // получения значений закрытых членов класса. using System;

// Класс для треугольников, производный от // класса TwoDShape. class Triangle : TwoDShape < public string Style; // тип треугольника

// Продемонстрировать применение модификатора доступа protected. using System;

// Добавить конструктор в класс Triangle. using System;

// Класс для двумерных объектов. class TwoDShape < double pri_width; double pr.i_height;

// Класс для треугольников, производный от класса TwoDShape. class Triangle : TwoDShape < string Style;

> В данном примере конструктор класса Triangle инициализирует наследуемые члены класса TwoDShape вместе с его собственным полем Style.

Когда конструкторы определяются как в базовом, так и в производном классе, про­ цесс построения объекта несколько усложняется, поскольку должны выполняться конструкторы обоих классов. В данном случае приходится обращаться к еще одному ключевому слову языка С#: base, которое находит двоякое применение: во-первых, для вызова конструктора базового класса; и во-вторых, для доступа к члену базового класса, скрывающегося за членом производного класса. Ниже будет рассмотрено первое при­ менение ключевого слова base.

Вызов конструкторов базового класса

С помощью формы расширенного объявления конструктора производного класса и ключевого слова base в производном классе может быть вызван конструктор, опре­ деленный в его базовом классе. Ниже приведена общая форма этого расширенного объявления:

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

Для того чтобы продемонстрировать применение ключевого слова base на кон­ кретном примере, рассмотрим еще один вариант класса TwoDShape в приведенной ниже программе. В данном примере определяется конструктор, инициализирующий свойства Width и Height. Затем этот конструктор вызывается конструктором класса Triangle.

Теперь конструктор класса Triangle объявляется следующим образом.

В данном варианте конструктор Triangle() вызывает метод base с параметрами w и h. Это, в свою очередь, приводит к вызову конструктора TwoDShape(), инициа­ лизирующего свойства Width и Height значениями параметров w и h. Они больше не инициализируются средствами самого класса Triangle, где теперь остается ини­ циализировать только его собственный член Style, определяющий тип треугольника. Благодаря этому класс TwoDShape высвобождается для конструирования своего по­ добъекта любым избранным способом. Более того, в класс TwoDShape можно ввести функции, о которых даже не будут подозревать производные классы, что предотвра­ щает нарушение существующего кода.

С помощью ключевого слова base можно вызвать конструктор любой формы, определяемой в базовом классе, причем выполняться будет лишь тот конструктор, па­ раметры которого соответствуют переданным аргументам. В качестве примера ниже приведены расширенные варианты классов TwoDShape и Triangle, в которые вклю­ чены как используемые по умолчанию конструкторы, так и конструкторы, принимаю­ щие один аргумент.

Вот к какому результату приводит выполнение этого кода.

А теперь рассмотрим вкратце основные принципы действия ключевого слова base. Когда в производном классе указывается ключевое слово base, вызывается конструктор из его непосредственного базового класса. Следовательно, ключевое слово base всегда обращается к базовому классу, стоящему в иерархии непосредственно над вызываю­ щим классом. Это справедливо даже для многоуровневой иерархии классов. Аргумен­ ты передаются базовому конструктору в качестве аргументов метода base(). Если же ключевое слово отсутствует, то автоматически вызывается конструктор, используемый в базовом классе по умолчанию.

Наследование и сокрытие имен

В производном классе можно определить член с таким же именем, как и у чле­ на его базового класса. В этом случае член базового класса скрывается в производном классе. И хотя формально в C# это не считается ошибкой, компилятор все же выдаст сообщение, предупреждающее о том, что имя скрывается. Если член базового клас­ са требуется скрыть намеренно, то перед его именем следует указать ключевое слово new, чтобы избежать появления подобного предупреждающего сообщения. Следует, однако, иметь в виду, что это совершенно отдельное применение ключевого слова new, не похожее на его применение при создании экземпляра объекта.

Ниже приведен пример сокрытия имени.

Прежде всего обратите внимание на использование ключевого слова new в следую­ щей строке кода.

В этой строке компилятору, по существу, сообщается о том, что вновь создавае­ мая переменная i намеренно скрывает переменную i из базового класса А и что авто­ ру программы об этом известно. Если же опустить ключевое слово new в этой строке кода, то компилятор выдаст предупреждающее сообщение.

Вот к какому результату приводит выполнение приведенного выше кода.

В классе В определяется собственная переменная экземпляра i, которая скрывает переменную i из базового класса А. Поэтому при вызове метода Show() для объек­ та типа В выводится значение переменной i, определенной в классе В, а не той, что определена в классе А.

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

Имеется еще одна форма ключевого слова base, которая действует подобно клю­ чевому слову this, за исключением того, что она всегда ссылается на базовый класс в том производном классе, в котором она используется. Ниже эта форма приведена в общем виде:

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

Выполнение этого кода приводит к следующему результату.

Несмотря на то что переменная экземпляра i в производном классе В скрывает переменную i из базового класса А, ключевое слово base разрешает доступ к перемен­ ной i, определенной в базовом классе.

С помощью ключевого слова base могут также вызываться скрытые методы. Например, в приведенном ниже коде класс В наследует класс А и в обоих классах объявляется метод Show(). А затем в методе Show() класса В с помощью ключевого слова base вызывается вариант метода Show(), определенный в классе А.

Выполнение этого кода приводит к следующему результату.

Как видите, в выражении base.Show() вызывается вариант метода Show() из ба­ зового класса.

Обратите также внимание на следующее: ключевое слово new используется в при­ веденном выше коде с целью сообщить компилятору о том, что метод Show(), вновь объявляемый в производном классе В, намеренно скрывает другой метод Show(), определенный в базовом классе А.

Создание многоуровневой иерархии классов

В представленных до сих пор примерах программ использовались простые иерар­ хии классов, состоявшие только из базового и производного классов. Но в C# мож­ но также строить иерархии, состоящие из любого числа уровней наследования. Как упоминалось выше, многоуровневая иерархия идеально подходит для использования одного производного класса в качестве базового для другого производного класса. Так, если имеются при класса, А, В и С, то класс С может наследовать от класса В, а тот, в свою очередь, от класса А. В таком случае каждый производный класс наследует ха­ рактерные особенности всех своих базовых классов. В частности, класс С наследует все члены классов В и А.

Для того чтобы показать, насколько полезной может оказаться многоуровневая иерархия классов, рассмотрим следующий пример программы. В ней производный класс Triangle служит в качестве базового для создания другого производного клас­ са — ColorTriangle. При этом класс ColorTriangle наследует все характерные осо­ бенности, а по существу, члены классов Triangle и TwoDShape, к которым добавляет­ ся поле color, содержащее цвет треугольника.

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

Благодаря наследованию в классе ColorTriangle могут использоваться опреде­ ленные ранее классы Triangle и TwoDShape, к элементам которых добавляется лишь та информация, которая требуется для конкретного применения данного класса. В этом отчасти и состоит ценность наследования, поскольку оно допускает повторное использование кода.

Приведенный выше пример демонстрирует еще одно важное положение: ключе­ вое слово base всегда обозначает ссылку на конструктор ближайшего по иерархии базового класса. Так, ключевое слово base в классе ColorTriangle обозначает вызов конструктора из класса Triangle, а ключевое слово base в классе Triangle — вызов конструктора из класса TwoDShape. Если же в иерархии классов конструктору базо­ вого класса требуются параметры, то все производные классы должны предоставлять эти параметры вверх по иерархии, независимо от того, требуются они самому произ­ водному классу или нет.

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

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

Вот к какому результату приводит выполнение этой программы.

Как видите, конструкторы вызываются по порядку выведения их классов.

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

Ссылки на базовый класс и объекты производных классов

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

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

Вообще говоря, переменная ссылки на объект может ссылаться только на объект своего типа.

Но из этого принципа строгого соблюдения типов в C# имеется одно важное ис­ ключение: переменной ссылки на объект базового класса может быть присвоена ссыл­ ка на объект любого производного от него класса. Такое присваивание считается впол­ не допустимым, поскольку экземпляр объекта производного типа инкапсулирует эк­ земпляр объекта базового типа. Следовательно, по ссылке на объект базового класса можно обращаться к объекту производного класса. Ниже приведен соответствующий пример.

В данном примере класс Y является производным от класса X. Поэтому следующая операция присваивания:

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

Следует особо подчеркнуть, что доступ к конкретным членам класса определяется типом переменной ссылки на объект, а не типом объекта, на который она ссылается. Это означает, что если ссылка на объект производного класса присваивается перемен­ ной ссылки на объект базового класса, то доступ разрешается только к тем частям это­ го объекта, которые определяются базовым классом. Именно поэтому переменной х2 недоступен член b класса Y, когда она ссылается на объект этого класса. И в этом есть своя логика, поскольку базовому классу ничего не известно о тех членах, которые до­ бавлены в производный от него класс. Именно поэтому последняя строка кода в при­ веденном выше примере была закомментирована.

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

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

В представленном выше примере объект t2 конструируется из объекта t1 и поэто­ му подобен ему. Ниже приведен результат выполнения кода из данного примера.

Обратите особое внимание на следующий конструктор класса Triangle:

Он принимает объект типа Triangle в качестве своего параметра и передает его (с помощью ключевого слова base) следующему конструктору класса TwoDShape.

Самое любопытное, что конструктор TwoDShape() предполагает получить объект класса TwoDShape, тогда как конструктор Triangle() передает ему объект класса Triangle. Как пояснялось выше, такое вполне допустимо, поскольку по ссылке на объект базового класса можно обращаться к объекту производного класса. Следова­ тельно, конструктору TwoDShape() можно на совершенно законных основаниях пере­ дать ссылку на объект класса, производного от класса TwoDShape. А поскольку кон­ структор TwoDShape() инициализирует только те части объекта производного класса, которые являются членами класса TwoDShape, то для него не имеет никакого значения, содержит ли этот объект другие члены, добавленные в производном классе.

Виртуальные методы и их переопределение

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

Метод объявляется как виртуальный в базовом классе с помощью ключевого сло­ ва virtual, указываемого перед его именем. Когда же виртуальный метод переопре­ деляется в производном классе, то для этого используется модификатор override. А сам процесс повторного определения виртуального метода в производном классе называется переопределением метода. При переопределении имя, возвращаемый тип и сигнатура переопределяющего метода должны быть точно такими же, как и у того виртуального метода, который переопределяется. Кроме того, виртуальный метод не может быть объявлен как static или abstract (подробнее данный вопрос рассма­ тривается далее в этой главе).

Переопределение метода служит основанием для воплощения одного из самых эффективных в C# принципов: динамической диспетчеризации методов, которая пред­ ставляет собой механизм разрешения вызова во время выполнения, а не компиляции. Значение динамической диспетчеризации методов состоит в том, что именно благо­ даря ей в C# реализуется динамический полиморфизм.

Ниже приведен пример, демонстрирующий виртуальные методы и их переопре­ деление.

Вот к какому результату приводит выполнение этого кода.

В коде из приведенного выше примера создаются базовый класс Base и два произ­ водных от него класса — Derived1 и Derived2. В классе Base объявляется виртуаль­ ный метод Who(), который переопределяется в обоих производных классах. Затем в методе Main() объявляются объекты типа Base, Derived1 и Derived2. Кроме того, объявляется переменная baseRef ссылочного типа Base. Далее ссылка на каждый тип объекта присваивается переменной baseRef и затем используется для вызова мето­ да Who(). Как следует из результата выполнения приведенного выше кода, вариант выполняемого метода Who() определяется по типу объекта, к которому происходит обращение по ссылке во время вызова этого метода, а не по типу класса переменной baseRef.

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

Выполнение этого кода приводит к следующему результату.

В данном примере метод Who() не переопределяется в классе Derived2. Поэтому для объекта класса Derived2 вызывается метод Who() из класса Base.

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

Вот к какому результату приводит выполнение этого кода.

В данном примере класс Derived3 наследует класс Derived2, который наследует класс Derived1, а тот, в свою очередь, — класс Base. Как показывает приведенный выше результат, выполняется метод Who(), переопределяемый в классе Derived1, поскольку это первый вариант виртуального метода, обнаруживаемый при продви­ жении вверх по иерархии от классов Derived3 и Derived2, где метод Who() не пере­ определяется, к классу Derived1.

И еще одно замечание: свойства также подлежат модификации ключевым словом virtual и переопределению ключевым словом override. Это же относится и к ин­ дексаторам.

Что дает переопределение методов

Благодаря переопределению методов в C# поддерживается динамический поли­ морфизм. В объектно-ориентированном программировании полиморфизм играет очень важную роль, потому что он позволяет определить в общем классе методы, которые становятся общими для всех производных от него классов, а в производных классах — определить конкретную реализацию некоторых или же всех этих методов. Переопределение методов — это еще один способ воплотить в C# главный принцип полиморфизма: один интерфейс — множество методов.

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

Применение виртуальных методов

Для того чтобы стали понятнее преимущества виртуальных методов, применим их в классе TwoDShape. В предыдущих примерах в каждом классе, производном от класса TwoDShape, определялся метод Area(). Но, по-видимому, метод Area() лучше было бы сделать виртуальным в классе TwoDShape и тем самым предоставить возможность переопределить его в каждом производном классе с учетом особенностей расчета пло­ щади той двумерной формы, которую инкапсулирует этот класс. Именно это и сдела­ но в приведенном ниже примере программы. Ради удобства демонстрации классов в этой программе введено также свойство name в классе TwoDShape.

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

Рассмотрим данный пример программы более подробно. Прежде всего, метод Area() объявляется как virtual в классе TwoDShape и переопределяется в классах Triangle и Rectangle по объяснявшимся ранее причинам. В классе TwoDShape ме­ тод Area() реализован в виде заполнителя, который сообщает о том, что пользователь данного метода должен переопределить его в производном классе. Каждое переопре­ деление метода Area() предоставляет конкретную его реализацию, соответствующую типу объекта, инкапсулируемого в производном классе. Так, если реализовать класс для эллипсов, то метод Area() должен вычислять площадь эллипса.

У программы из рассматриваемого здесь примера имеется еще одна примечатель­ ная особенность. Обратите внимание на то, что в методе Main() двумерные формы объявляются в виде массива объектов типа TwoDShape, но элементам этого массива присваиваются ссылки на объекты классов Triangle, Rectangle и TwoDShape. И это вполне допустимо, поскольку по ссылке на базовый класс можно обращаться к объ­ екту производного класса. Далее в программе происходит циклическое обращения к элементам данного массива для вывода сведений о каждом объекте. Несмотря на всю свою простоту, данный пример наглядно демонстрирует преимущества наследования и переопределения методов. Тип объекта, хранящийся в переменной ссылки на базо­ вый класс, определяется во время выполнения и соответственно обусловливает даль­ нейшие действия. Так, если объект является производным от класса TwoDShape, то для получения его площади вызывается метод Area(). Но интерфейс для выполнения этой операции остается тем же самым независимо от типа используемой двумерной формы.

Применение абстрактных классов

Иногда требуется создать базовый класс, в котором определяется лишь самая об­ щая форма для всех его производных классов, а наполнение ее деталями предостав­ ляется каждому из этих классов. В таком классе определяется лишь характер методов, которые должны быть конкретно реализованы в производных классах, а не в самом ба­ зовом классе. Подобная ситуация возникает, например, в связи с невозможностью по­ лучить содержательную реализацию метода в базовом классе. Именно такая ситуация была продемонстрирована в варианте класса TwoDShape из предыдущего примера, где метод Area() был просто определен как заполнитель. Такой метод не вычисляет и не выводит площадь двумерного объекта любого типа.

Создавая собственные библиотеки классов, вы можете сами убедиться в том, что у метода зачастую отсутствует содержательное определение в контексте его базового класса. Подобная ситуация разрешается двумя способами. Один из них, как показано в предыдущем примере, состоит в том, чтобы просто выдать предупреждающее со­ общение. Такой способ может пригодиться в определенных ситуациях, например при отладке, но в практике программирования он обычно не применяется. Ведь в базовом классе могут быть объявлены методы, которые должны быть переопределены в произ­ водном классе, чтобы этот класс стал содержательным. Рассмотрим для примера класс Triangle. Он был бы неполным, если бы в нем не был переопределен метод Area(). В подобных случаях требуется какой-то способ, гарантирующий, что в производном классе действительно будут переопределены все необходимые методы. И такой способ в С# имеется. Он состоит в использовании абстрактного метода.

Абстрактный метод создается с помощью указываемого модификатора типа abstract. У абстрактного метода отсутствует тело, и поэтому он не реализуется в ба­ зовом классе. Это означает, что он должен быть переопределен в производном классе, поскольку его вариант из базового класса просто непригоден для использования. Не­ трудно догадаться, что абстрактный метод автоматически становится виртуальным и не требует указания модификатора virtual. В действительности совместное исполь­ зование модификаторов virtual и abstract считается ошибкой.

Для определения абстрактного метода служит приведенная ниже общая форма.

Как видите, у абстрактного метода отсутствует тело. Модификатор abstract мо­ жет применяться только в методах экземпляра, но не в статических методах (static). Абстрактными могут быть также индексаторы и свойства.

Класс, содержащий один или больше абстрактных методов, должен быть также объявлен как абстрактный, и для этого перед его объявлением class указывается мо­ дификатор abstract. А поскольку реализация абстрактного класса не определяется полностью, то у него не может быть объектов. Следовательно, попытка создать объект аб­ страктного класса с помощью оператора new приведет к ошибке во время компиляции.

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

Используя абстрактный класс, мы можем усовершенствовать рассматривавшийся ранее класс TwoDShape. Для неопределенной двухмерной фигуры понятие площади не имеет никакого смысла, поэтому в приведенном ниже варианте класса TwoDShape метод Area() и сам класс TwoDShape объявляются как abstract. Это, конечно, озна­ чает, что во всех классах, производных от класса TwoDShape, должен быть переопреде­ лен метод Area().

Как показывает представленный выше пример программы, во всех производных классах метод Area() должен быть непременно переопределен, а также объявлен аб­ страктным. Убедитесь в этом сами, попробовав создать производный класс, в котором не переопределен метод Area(). В итоге вы получите сообщение об ошибке во время компиляции. Конечно, возможность создавать ссылки на объекты типа TwoDShape по- прежнему существует, и это было сделано в приведенном выше примере программы, но объявлять объекты типа TwoDShape уже нельзя. Именно поэтому массив shapes сокращен в методе Main() до 4 элементов, а объект типа TwoDShape для общей двух­ мерной формы больше не создается.

Обратите также внимание на то, что в класс TwoDShape по-прежнему входит метод ShowDim() и что он не объявляется с модификатором abstract. В абстрактные клас­ сы вполне допускается (и часто практикуется) включать конкретные методы, которые могут быть использованы в своем исходном виде в производном классе. А переопре­ делению в производных классах подлежат только те методы, которые объявлены как abstract.

Предотвращение наследования с помощью ключевого слова sealed

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

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

Ниже приведен пример объявления класса типа sealed.

Как следует из комментариев в приведенном выше фрагменте кода, класс В не мо­ жет наследовать класс А, потому что последний объявлен как sealed.

И еще одно замечание: ключевое слово sealed может быть также использовано в виртуальных методах для предотвращения их дальнейшего переопределения. До­ пустим, что имеется базовый класс В и производный класс D. Метод, объявленный в классе В как virtual, может быть объявлен в классе D как sealed. Благодаря этому в любом классе, наследующем от класса предотвращается переопределение данного метода. Подобная ситуация демонстрируется в приведенном ниже фрагменте кода.

Метод MyMethod() герметизирован в классе D, и поэтому не может быть переопре­ делен в классе X.

В C# предусмотрен специальный класс object, который неявно считается базовым классом для всех остальных классов и типов, включая и типы значений. Иными слова­ ми, все остальные типы являются производными от object. Это, в частности, означает, что переменная ссылочного типа object может ссылаться на объект любого другого типа. Кроме того, переменная типа object может ссылаться на любой массив, по­ скольку в C# массивы реализуются как объекты. Формально имя object считается в C# еще одним обозначением класса System.Object, входящего в библиотеку классов для среды .NET Framework.

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

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

Таблица 11.1. Методы класса object

Метод Назначение
public virtual bool Equals(object ob) Определяет, является ли вызывающий объект таким же, как и объект, доступный по ссылке ob
public static bool Equals(object objA, object objB) Определяет, является ли объект, доступный по ссылке objA, таким же, как и объект, доступный по ссылке objB
protected Finalize() Выполняет завершающие действия перед «сборкой мусора”. В C# метод Finalize() доступен посредством деструктора
public virtual int GetHashCode() Возвращает хеш-код, связанный с вызывающим объектом
public Type GetType() Получает тип объекта во время выполнения программы
protected object MemberwiseClone() Выполняет неполное копирование объекта, т.е. копируются только члены, но не объекты, на которые ссылаются эти члены
public static boolReferenceEquals(obj objA, object objB) Определяет, делаются ли ссылки objA и objB на один и тот же объект
public virtual string ToString() Возвращает строку, которая описывает объект

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

Как упоминалось в главе 9, если перегружается оператор ==, то обычно приходит­ ся переопределять методы Equals(object) и GetHashCode(), поскольку чаще всего требуется, чтобы метод Equals(object) и оператор == функционировали одинаково. Когда же переопределяется метод Equals(object), то следует переопределить и ме­ тод GetHashCode(), чтобы оба метода оказались совместимыми.

Метод ToString() возвращает символьную строку, содержащую описание того объекта, для которого он вызывается. Кроме того, метод ToString() автоматически вызывается при выводе содержимого объекта с помощью метода WriteLine(). Этот метод переопределяется во многих классах, что позволяет приспосабливать описание к конкретным типам объектов, создаваемых в этих классах. Ниже приведен пример применения данного метода.

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

Упаковка и распаковка

Как пояснялось выше, все типы в С#, включая и простые типы значений, являются производными от класса object. Следовательно, ссылкой типа object можно вос­ пользоваться для обращения к любому другому типу, в том числе и к типам значений. Когда ссылка на объект класса object используется для обращения к типу значения, то такой процесс называется упаковкой. Упаковка приводит к тому, что значение про­ стого типа сохраняется в экземпляре объекта, т.е. «упаковывается» в объекте, который затем используется как и любой другой объект. Но в любом случае упаковка происхо­ дит автоматически. Для этого достаточно присвоить значение переменной ссылочного типа object, а об остальном позаботится компилятор С#.

Распаковка представляет собой процесс извлечения упакованного значения из объекта. Это делается с помощью явного приведения типа ссылки на объект класса object к соответствующему типу значения. Попытка распаковать объект в другой тип может привести к ошибке во время выполнения.

Ниже приведен простой пример, демонстрирующий упаковку и распаковку.

В этом примере кода выводится значение 10. Обратите внимание на то, что значе­ ние переменной х упаковывается в объект простым его присваиванием переменной obj, ссылающейся на этот объект. А затем это значение извлекается из объекта, до­ ступного по его ссылке obj, и далее приводится к типу int.

Ниже приведен еще один, более интересный пример упаковки. В данном случае значение типа int передается в качестве аргумента методу Sqr(), который, в свою очередь, принимает параметр типа object.

Вот к какому результату приводит выполнение этого кода.

В данном примере значение переменной х автоматически упаковывается при пере­ даче методу Sqr().

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

В результате выполнения этой программы выводится значение 10. Дело в том, что метод ToString() возвращает строковое представление объекта, для которого он вы­ зывается. В данном случае строковым представлением значения 10 как вызывающего объекта является само значение 10!

Класс object как универсальный тип данных

Если object является базовым классом для всех остальных типов и упаковка зна­ чений простых типов происходит автоматически, то класс object можно вполне ис­ пользовать в качестве «универсального» типа данных. Для примера рассмотрим про­ грамму, в которой сначала создается массив типа object, элементам которого затем присваиваются значения различных типов данных.

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

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

Несмотря на то что универсальный характер класса object может быть довольно эффективно использован в некоторых ситуациях, было бы ошибкой думать, что с по­ мощью этого класса стоит пытаться обойти строго соблюдаемый в C# контроль типов. Вообще говоря, целое значение следует хранить в переменной типа int, строку — в пе­ ременной ссылочного типа string и т.д.

А самое главное, что начиная с версии 2.0 для программирования на C# стали до­ ступными подлинно обобщенные типы данных — обобщения (более подробно они рассматриваются в главе 18). Внедрение обобщений позволило без труда определять классы и алгоритмы, автоматически обрабатывающие данные разных типов, соблюдая типовую безопасность. Благодаря обобщениям отпала необходимость пользоваться классом object как универсальным типом данных при создании нового кода. Уни­ версальный характер этого класса лучше теперь оставить для применения в особых случаях.

C#. Доступ к элементам класса из экземпляров классов, образовывающих иерархию. Особенности применения ссылки на базовый класс.

Доступ к элементам класса из экземпляров классов, образовывающих иерархию. Особенности применения ссылки на базовый класс. Ключевые слова as , is . Способы вызова методов с одинаковыми именами в иерархиях наследования. Статический полиморфизм

В данной теме изучается:

  • доступ с помощью экземпляров к элементам классов, образовывающих иерархию;
  • доступ к с помощью ссылки на базовый класс к элементам классов, образовывающих иерархию;
  • изучение особенностей использования ключевых слов is , as ;
  • способы вызова методов с одинаковыит именами в иерархии классов.

Содержание

  • 1. Использование экземпляров классов, образовывающих иерархию. Особенности доступа к элементам базового класса из экземпляра унаследованного класса
  • 2. Пример, который демонстрирует доступ из экземпляров классов, образовывающих иерархию
  • 3. Ссылка на базовый класс в иерархии классов. Особенности применения
  • 4. Пример, который демонстрирует ограничения, которые могут возникнуть при использовании ссылки на базовый класс
  • 5. Способы организации доступа к элементам производного класса с помощью ссылки на базовый класс. Ключевые слова is , as
  • 6. Пример, который демонстрирует способы организации доступа по ссылке к элементам производного класса

Поиск на других ресурсах:

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

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

  • ко всем public-элементам данного класса;
  • к тем public-элементам базового класса, имена которых не «прячутся» (перекрываются) элементами данного класса.
    Из экземпляра класса невозможно получить доступ к элементам классов, унаследованных от данного.

Рисунок 1 демонстрирует доступ из экземпляров objA , objB , objC классов A , B , C которые образовывают иерархию наследования. Из любого экземпляра делается попытка вызвать методы его класса и методы других классов иерархии.

C#. Наследование. Доступ к элементам классов из экземпляров этих классов

Рисунок 1. Доступ к элементам классов из экземпляров этих классов

2. Пример, который демонстрирует доступ из экземпляров классов, образовывающих иерархию

Ниже приводится текст демонстрационной программы которая соответствует рисунку 1. Программа создана по шаблону Console Application.

Результат выполнения программы

3. Ссылка на базовый класс в иерархии классов. Особенности применения

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

Рисунок 2 демонстрирует правило 1 на примере иерархии из 3 классов A , B , C .

C#. Наследование. Присвоение ссылке на базовый класс значений экземпляров производных классов

Рисунок 2. Присвоение ссылке на базовый класс значений экземпляров производных классов

После того, как ссылке на базовый класс присвоены значения одного из экземпляров класса из иерархии, можно попробовать использовать элементы этого экземпляра (вызвать методы, свойства, и т.п.). В этом случае уместным есть правило 2: если ссылка на базовый класс получает значение объекта (экземпляра) производного класса, то доступ по этой ссылке есть только к элементам базового класса. Рисунок 3 демонстрирует использование правила 2.

C#. Наследование. Демонстрация доступа к элементам классов с помощью ссылки на базовый класс

Рисунок 3. Демонстрация доступа к элементам классов A , B с помощью ссылки rA на базовый класс A

Как видно из рисунка 2, с помощью ссылки на базовый класс можно доступиться к элементам базового класса A . К элементам унаследованного класса B нету доступа. Чтобы организовать доступ к элементам унаследованного класса B нужно использовать способы приведения типа A к типу B (смотрите пункт 5).

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

В примере продемонстрированы ограничения доступа к элементам экземпляра objB производного класса B из ссылки rA на базовый класс A . Невозможно вызвать элементы класса B без явного приведения типа и использования операторов is , as .

Результат выполнения программы

5. Способы организации доступа к элементам производного класса с помощью ссылки на базовый класс. Ключевые слова is , as

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

  • использовать явное приведение к типу унаследованного класса;
  • использовать ключевое слово as , которое осуществляет попытку приведения одного типа к другому;
  • использовать ключевое слово as в сочетании с проверкой на совместимость типов. Проверка на совместимость использует ключевое слово is .

На рисунке 4 изображены все 3 способа для заданных классов A, B которые образовывают иерархию.

C#. Наследование. Способы доступа к элементам производного класса с помощью ссылки на базовый класс

Рисунок 4. Способы доступа к элементам производного класса с помощью ссылки на базовый класс

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

В примере демонстрируется 3 способы организации доступа к методу Show() производного класса B из ссылки на базовый класс A . Реализованные способы демонстрируют статический полиморфизм, если централизованный вызов метода формируется на этапе компиляции (во время приведения типа).

Как обратиться к методу класса с

Как обратиться к определенной колонке ТЗ и к определенной строке из этой колонки?
Как обратиться к определенной колонке ТЗ и к определенной строке из этой колонки? Добавлено.

Как обратиться к методу класса из внешней функции?
Имеется простой класс Employee с методами GetAge() — возвращает возраст, и SetAge(int age). Как.

Как из другой функции обратиться к членам класса окна?
Здравствуйте! Вобщем в VC6 у меня есть класс окна, зовется CMyDlg. Так вот по нажатию на кнопку.

Как обратиться к функции класса, который находится в листе?
Как обратиться к функции класса, который находится в листе? В классе есть функция Move. .

Смотря из какого класса нужно обратиться к методу (функций и процедур в плане терминологии в шарпе нет).

Если из того же класса, в котором метод объявлен, то очень просто:

Сообщение от kolorotur

Вот в чем оказывается ошибка была.

Возник следующий вопросик (не стал создавать новую тему решил здесь спросить)

Есть есть публичный класс MvcApplication со статичным публичным методом GetUnicName

мне компилятор говорит следующее:
Error 4 The name ‘MvcApplication’ does not exist in the current context

Классы находятся в одном Solution (естественно))), но в разных проектах

Подскажите в чем проблема

jaguar7021, а у вас класс который вызываете и из которого вызываете в одном namespace?

turbanoff, опередил

Вот такой ошибкой:
Error 2 The type or namespace name ‘WebFront’ could not be found (are you missing a using directive or an assembly reference?)

И вызов на прямую тоже не работает:

Вот такая ошибка:
Error 4 The name ‘WebFront’ does not exist in the current context

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

Перенес в другой файл функцайку

Как обратиться к определенной кнопке
У меня имеется таблица, созданная циклом в PHP, и крайний столбец отведен под кнопку &quot;удалить&quot;. Как.

Как в таблице обратиться к определенной колонке
Есть табл fxml. Колонки: Firstname, Lastname, Phone, Email Я хочу сделать слушатель на колонку.

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

Как обратиться из конструктора базового абстрактного класса к свойству-массиву класса наследника
Добрый день. Подскажите как обратиться из конструктора базового абстрактного класса к.

Как обратиться к полю объекта класса Car, который находится в массиве класса Cars?
Помогите пожалуйста разобраться. Ниже три разных модуля. Как обратиться к полю объекта класса Car.

Как обратиться к свойству класса из метода этого же класса
Добрый день! function Tta_component.check: boolean; //âåðíåò true åñëè ïðè èíèöèàëèçàöèè.

Как обратиться к методу класса с

Эта статья даст базовое понимание терминов «класс», «метод», «наследование», «перегрузка метода»

Содержание

Методы

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

Взгляните на пример:

Методу Vec2f::getLength доступны все символы (т.е. переменные, функции, типы данных), которые были объявлены в одной из трёх областей видимости. При наличии символов с одинаковыми идентификаторами один символ перекрывает другой, т.к. поиск происходит от внутренней области видимости к внешней.

Понять идею проще на схеме. В ней область видимости названа по-английски: scope.

Схема

Поднимаясь по схеме от внутренней области видимости к внешней, легко понять, какие имена символов доступны в методе getLength:

  1. локальная переменная “lengthSquare”
  2. поля Vec2f под именами “x” и “y”
  3. всё, что есть в глобальной области видимости

К слову, в других методах структуры Vec2f переменная “lengthSquare” будет недоступна, а поля “x” и “y” будут доступны.

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

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

Посмотрите на простой пример. В нём есть проблема: и поля, и параметры конструктора названы одинаково. В результате в области видимости конструктора доступны только параметры, и своими именами они перекрывают поля!

Язык C++ предлагает два решения. Первый способ — использовать косвенное обращение к полям через привязанный к методу объект. Указатель на него доступен по ключевому слову this :

Второй путь считается более правильным: мы используем специальную возможность конструкторов — “списки инициализации конструктора” (англ. constructor initializer lists). Списки инициализации — это список, разделённый запятыми и начинающийся с “:”. Элемент списка инициализации выглядит как field(expression) , т.е. для каждого выбранного программистом поля можно указать выражение, инициализирующее его. Имя переменной является выражением. Поэтому мы инициализируем поле его параметром:

Объявление и определение методов

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

Классы и структуры

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

Основы инкапсуляции

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

  1. public — символ в этой области доступен извне
  2. private — символ из этой области доступен лишь собственных в методах
  3. protected — используется редко, о нём можете прочитать в документации

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

Запомните несколько хороших правил:

  • Используйте struct, если все поля публичные и не зависят друг от друга; используйте class, если между полями должны соблюдаться закономерности (например, поле “площадь” круга должно быть)

Основы наследования

В C++ новый тип может наследовать все поля и методы другого типа. Для этого достаточно указать структуру или класс в списке базовых типов. Такой приём используется в SFML при объявлении классов фигур:

Что означает public перед именем базового типа? Во-первых внешний код может передать RectangleShape в функцию, принимающую ссылку на Shape, то есть возможен так называемы upcast от более низкого (и более конкретного) типа RectangleShape к более высокому (и более абстрактному) типу Shape:

Во-вторых из-за public наследования все унаследованные поля и методы сохраняют свой уровень доступ: приватные остаются приватными, публичные остаются публичными. А если бы мы наследовали Shape с ключевым словом private, то уровень доступа стал бы ниже: все методы и поля стали бы приватными:

Контроль уровня доступа полей и методов — хитрый механизм, пройдёт немало времени, прежде чем вы научитесь пользоваться им правильно. В начале просто старайтесь сделать правильный выбор между private и public. Скорее всего поля будут private, а конструктор и все методы будут public. Это позволяет сохранять инвариант класса, то есть держать поля объекта в согласованном состоянии независимо от того, какие методы вызывают извне.

Основы полиморфизма: виртуальные методы и их перегрузка

SFML использует ещё одну идиому C++: виртуальные методы. Ключевые слова virtual , final , override относятся именно к этой идиоме. Например, в SFML определяется класс Drawable, который обозначает “сущность, которую можно нарисовать”. Все рисуемые классы SFML, включая sf::Sprite , sf::RectangleShape , sf::Text , прямо или косвенно наследуются от sf::Drawable .

Зачем это надо? Дело в том, что метод draw класса RenderWindow принимает параметр типа Drawable . Тем не менее, этот метод успешно рисует любые типы объектов: спрайты, фигуры, тексты. Он не выполняет проверок — он просто настраивает состояние рисования (RenderStates) и вызывает метод draw у сущности, которая является Drawable .

Виртуальный метод вызывается косвенно: если класс Shape , унаследованный от Drawable , переопределил метод, а потом был передан как параметр типа Drawable , то вызов метода draw всё равно приведёт к вызову переопределённого метода Shape::draw , а не метода Drawable::draw ! С обычными (не виртуальными) методами такого не происходит: если бы мы убрали слово virtual из объявления draw , то вызов метода draw у параметра типа Drawable всегда приводил бы к вызову Drawable::draw , даже если реальный тип объекта, скрытого за этим параметром, совсем другой.

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

Другими словами, RenderWindow и RectangleShape не знают, что они работают друг с другом, но тем не менее каждый вызывает правильный метод другого класса!

Иллюстрация

Когда вы просто вызываете window.draw(shape) , повышение класса происходит дважды: сначала конкретный класс фигуры повышается до более ограниченного класса Drawable, затем конкретный класс RenderWindow повышается до абстрактного RenderTarget. Всё это не требует времени при выполнении: просто компилятор выполняет проверки типов данных ещё при компиляции, не более того.

Как унаследовать Drawable: практический пример

Мы создадим класс, который рисует флаг России. Он будет унаследован от Drawable, чтобы использовать для рисования обычный метод draw у объекта окна.

Теперь мы можем реализовать конструктор и метод draw. В конструкторе мы должны вычислить и установить позиции и размеры трёх полос на флаге, а в методе draw мы должны их последовательно нарисовать.

Как обратиться к методу класса с

Строка: Form1.Metod("Этот текст из стороннего обьекта") — вызывает ошибку:

An object reference is required for the non-static field, method, or property ‘WindowsFormsApplication1.Form1.Metod(string)’

делать статическим — нежелательно. Этот вариант уже испробован, возможно какие нибудь есть другие варианты? Как обратиться к методам и полям Form1 из другого обьекта?

Ответы

1. передать делегат в конструктор Klass1 (от NIKkakNIK)

2. передать ссылку на Form1 целиком в конструктор Klass1.

3. добавить в Klass1 событие (event) и в коде Form1 подписаться на него.

4. вытащить ссылку на форму из System.Windows.Forms.Application.OpenForms[]

  • Помечено в качестве ответа Abolmasov Dmitry 14 марта 2011 г. 12:54

Спасибо за ответ. Но тут же возникает вопрос — как я понимаю, при запуске программы, уже создается экземпляр Form1. А если я создам новый объект, то строка: this.textBox1.Text = s; эта операция будет применена к новому обьекту и не будет иметь с существующей начальной Form1 — ничего общего — элемент textBox1 — не изменит своего значения в начальной Form1.

Если метод public void metod(string s) — сделать статическим, тут же возникает проблема к обращению элементам окна textBox1, так как в статическом методе по определению нельзя использовать this. А кроме как обращения через this, к элементам окна (editBox1) начальной формы — незнаю. Замкнутый круг. (((((

Возможно есть какая либо переменная, на которую ссылается построенная при запуске программы форма.

А как насчет механизма событий? Применимы ли они в данном случае? Если можно — напишите код — буду очень благодарен!

  • Предложено в качестве ответа NIKkakNIK 14 марта 2011 г. 8:50
  • Помечено в качестве ответа Abolmasov Dmitry 14 марта 2011 г. 12:54

Экземпляр Form1 создается при запуске, если это прописано в функции Main, которая вызывается при запуске всей программы.

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

Указатели на методы классов в C++

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

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

Давайте разберемся что и почему происходит.
Все манипуляции будут произведены для архитектуры x86-64.

Взглянем на код.

Размер указателя на метод больше 8 байт. В некоторых компиляторах это не так, например компилятор Microsoft ужимает до 8 байт указатель на метод в некоторых случаях. В последних версиях компиляторов clang и gcc для Linux принимал размер 16 байт.

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

Посмотрим такой код на C++. Это базовый пример вызова метода из указателя на метод.

Скомпилировав код такой командой:

Получаем вывод LLVM IR:

LLVM IR является промежуточным представлением между машинным кодом и C++ в компиляторе Clang. Он позволяет компилятору производить оптимизации не зависящие от конкретной архитектуры процессора, а нам он дает понять что происходит на тех или иных стадиях компиляции, и является более читаемым чем язык ассемблера.

Подробнее про LLVM IR можно узнать в Википедии, официальном сайте LLVM и Clang.

  • Взглянув на первую строчку, видно что указатель на метод является структурой ` `, а не обычным указателем. Эта структура содержит два i64 элемента, которые могут уместить в себя 2 обычных указателя. Видно почему мы не можем приводить указатели на методы в обычные. Мы не можем без потерь преобразовать 16 байт в 8 байт в общем случае.
  • В блоке `entry`, начинающимся с 5 строки, видно что происходит корректирование указателя `this`. Это значит, что компилятор прибавляет к указателю на `this` значение второго элемента этой структуры, и позже в блоке `memptr.end` передает его в вызов метода.
  • Нечто странное происходит происходит в блоке `entry` на 14 строке с первым элементом структуры. Компилятор вычисляет выражение аналогичное следующему: `bool isvirtual = val & 1`. Компилятор считает указатель на метод виртуальным, если число в нем нечетное, в противном случае невиртуальным.
  • Если указатель на метод указывает на невиртуальный метод, то значение первого элемента считается обычным указателем на функцию, который позже вызывается. Эти предположения происходят в блоке `memptr.nonvirtual`.
  • Если указатель на метод указывает на виртуальный метод, то тут сложнее. Вначале вычитается единица из первого элемента структуры, и вычисленное значение является отступом для виртуальной таблицы, указатель на которую берется из значения указателя на `this`. Это происходит в блоке `memptr.virtual`.
  • Информацию является ли он виртуальным
  • Указатель на адрес метода (если не виртуальный)
  • Смещение в vtable (если виртуальный)
  • корректирование `this`

Метод класса имеет невидимый первый параметр — указатель на `this`, который передается компилятором при вызове метода. Остальные аргументы передаются после в том же порядке, что и были.

Если бы мы писали этот код на C++, то он выглядел бы примерно так:

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

Как мы видим, тут представлены примеры указателей на методы и аналогичные функции, которые принимают указатель на класс как указатель на `this`. Однако компилятор не может преобразовать указатели a1 и b1 в связи с тем, что мы не можем бесплатно преобразовывать указатели дочернего типа в указатели родительского типа. Компилятору необходимо запомнить отступ (значение корректирования) внутри дочернего класса для родительского класса и сохранить его где-то.

Посмотрим такой код:

Скомпилируем код командой:

Видно, что указатель на метод указывает на одну и ту же функцию. Однако значение корректирования разное из-за того что класс B расположен по сути внутри класса C.
Компилятору C++ необходимо знать отступ от базового класса для того чтобы передать `this` в метод класса.

Что плохого в этой реализации:

  • Размер указателя относительно большой, даже если корректирование отсутствует каким либо образом в gcc и clang
  • Каждый раз идет проверка виртуальности метода, даже если мы знаем что он не виртуальный
  • Использовать статический метод, принимающий экземпляр класса
  • Забыть про существование указателей на методы, и решить проблему как-то иначе в прочих случаях
  • В интернете есть советы использовать std::bind, std::function и подобные библиотечные функции. Проверив их поведение, я не обнаружил существования каких либо оптимизаций для указателей на методы.
  • У меня нет технической возможности проверить что происходит в компиляторах Microsoft, поэтому не особо про них рассказал. Однако протестировав онлайн компиляторы, я заметил что MSVC умеет анализировать структуру классов и удалять поле значения корректирования, если оно не требуется.

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

Также я написал маленькую программку, что выводит данные об указателях на методы и помогает понять их внутренности, пока писал эту статью. Работает в clang и gcc под Linux. Код выложен тут.

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

Как обратиться к классу с

Строка: Form1.Metod("Этот текст из стороннего обьекта") — вызывает ошибку:

An object reference is required for the non-static field, method, or property ‘WindowsFormsApplication1.Form1.Metod(string)’

делать статическим — нежелательно. Этот вариант уже испробован, возможно какие нибудь есть другие варианты? Как обратиться к методам и полям Form1 из другого обьекта?

Ответы

1. передать делегат в конструктор Klass1 (от NIKkakNIK)

2. передать ссылку на Form1 целиком в конструктор Klass1.

3. добавить в Klass1 событие (event) и в коде Form1 подписаться на него.

4. вытащить ссылку на форму из System.Windows.Forms.Application.OpenForms[]

  • Помечено в качестве ответа Abolmasov Dmitry 14 марта 2011 г. 12:54

Спасибо за ответ. Но тут же возникает вопрос — как я понимаю, при запуске программы, уже создается экземпляр Form1. А если я создам новый объект, то строка: this.textBox1.Text = s; эта операция будет применена к новому обьекту и не будет иметь с существующей начальной Form1 — ничего общего — элемент textBox1 — не изменит своего значения в начальной Form1.

Если метод public void metod(string s) — сделать статическим, тут же возникает проблема к обращению элементам окна textBox1, так как в статическом методе по определению нельзя использовать this. А кроме как обращения через this, к элементам окна (editBox1) начальной формы — незнаю. Замкнутый круг. (((((

Возможно есть какая либо переменная, на которую ссылается построенная при запуске программы форма.

А как насчет механизма событий? Применимы ли они в данном случае? Если можно — напишите код — буду очень благодарен!

  • Предложено в качестве ответа NIKkakNIK 14 марта 2011 г. 8:50
  • Помечено в качестве ответа Abolmasov Dmitry 14 марта 2011 г. 12:54

Экземпляр Form1 создается при запуске, если это прописано в функции Main, которая вызывается при запуске всей программы.

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

Читать:
Система windows определила что наиболее подходящий драйвер уже установлен

Похожие статьи