close

Вход

Забыли?

вход по аккаунту

?

25 91 ООПиП Понкратов

код для вставкиСкачать
25. Конструкторы экземпляров. Конструкторы типов. Перегрузка операторов. Операторы преобразования. Передача методу параметра ссылкой. Передача переменного числа параметров. Виртуальные методы.
Конструкторы - это методы, позволяющие корректно инициализировать новый экземпляр типа. Чтобы сгенерировать верифицируемый код, общеязыковая исполняющая среда (CLR) требует определения в каждом классе (ссылочном типе) хотя бы одного конструктора (если нужно запретить коду, внешнему по отношению к этому классу, создавать его экземпляры, можно сделать конструктор закрытым). При создании объекта ссылочного типа выделяемая для него память всегда обнуляется до вызова конструктора экземпляра типа. Любые поля, не перезаписываемые конструктором явно, гарантированно содержат 0 или null.
По умолчанию многие компиляторы (в том числе С#) определяют для ссылочных типов открытые конструкторы без параметров (их часто называют конструкторами по умолчанию), если вы не определяете свой конструктор явно. class SomeType {// По умолчанию С# автоматически определяет открытый конструктор без параметров}
Это определение идентично определению типа:
class SomeType {public SomeType( ) { }}
Тип может определять несколько конструкторов, при этом сигнатуры конструкторов обязательно должны отличаться, доступ к разным конструкторам также может предоставляться на разных условиях. class SomeType { Int32 x = 5; String s = "Hi there";
Double d = 3.14159; Byte b;
// Это конструкторы:
public SomeType( ) {...}
public SomeType(Int32 x) {...}
public SomeType(String s) { ...; d = 10; }
}
Генерируя IL-код для трех методов-конструкторов из этого примера, компилятор помещает в начало каждого из методов код, инициализирующий поля х, s и d. Затем он добавляет к методу код, расположенный внутри методов-конструкторов. Поскольку в показанном выше классе определены три конструктора, компилятор трижды генерирует код, инициализирующий поля х, s и d: по разу для каждого из конструкторов. Конструкторы размерных типов работают иначе, чем конструкторы ссылочных типов. Во-первых, CLR не требует определять конструкторы у размерного типа. Фактически многие компиляторы (включая С#) не определяют для размерных типов конструкторы по умолчанию, не имеющие параметров. Причина в том, что размерные типы можно создавать неявно. Разберем такой код;
struct Point {public Int32 x, у;}
class Rectangle {public Point topLeft, bottomRight;}
Чтобы создать объект Rectangle, надо использовать оператор new, указав, конструктор. В этом случае вызывается конструктор, автоматически сгенерированный компилятором С#. Память, выделенная для объекта Rectangle, включает место для двух экземпляров размерного типа Point. Из соображений повышения производительности CLR не пытается вызвать конструктор для каждого экземпляра размерного типа, содержащегося внутри объекта ссылочного типа. Но, как сказано выше, поля размерного типа инициализируются нулевыми или пустыми значениями.
CLR действительно позволяет программистам определять конструкторы для размерных типов, но эти конструкторы будут исполнены лишь при наличии кода, явно вызывающего один из них. например, как в конструкторе объекта Rectangle:
struct Point {
public Int32 x, у;
public Point(Int32 x, Int32 y) { this.x = x; this.y = y;
} }
class Rectangle {
public Point topLeft, bottomRight;
public Rectangle ( ) {
// В С# оператор new, использованный для создания экземпляра размер-
// ного типа, просто позволяет конструктору инициализировать память,
// уже выделенную для этого экземпляра
topLeft = new Point(l, 2);
bottomRigtht = new Point(100, 200); } }
С# не позволяет определять для размерного типа конструкторы без параметров. Поэтому показанный выше код на самом деле даже не компилируется. Конструкторы типов
Помимо конструкторов экземпляров, CLR поддерживает конструкторы типов (также известные как статические конструкторы, конструкторы классов или инициализаторы типов). Конструкторы типа можно применять и к интерфейсам (хотя этого не допускает), ссылочным и размерным типам. Подобно тому, как конструкторы экземпляров используются для установки первоначального состояния экземпляра типа, конструкторы типов применяются для установки первоначального состояния типа. По умолчанию у типа не определен ни один конструктор. У типа может быть один и только один конструктор.
Кроме того, у конструкторов типа никогда не бывает параметров. Вот как определяются ссылочные и размерные типы с конструкторами в программах на С#:
class SomeRefType {
static SomeRefType( ) {
// Исполняется при первом обращении к типу SomeRefType.
} }
struct SomeValType {
// CS на самом деле допускает определять для размерных типов
// конструкторы, не имеющие параметров
static SomeValType( ) {
// Исполняется при первом обращении к типу SomeValType.
} }
Конструкторы типа всегда должны быть закрытыми, чтобы код разработчика не смог их вызвать - напротив, CLR всегда способна вызвать конструктор типа. Кроме того, CLR ведет себя довольно свободно, принимая решение о вызове конструктора типа. CLR вызывает конструктор типа в одном из следующих случаев.
• Прямо перед созданием первого экземпляра типа или перед первым обращением к полю или члену класса, не унаследованному от предка. Это называется точной семантикой, поскольку CLR вызывает конструктор типа именно в тот момент, когда он необходим.
• В любое время перед первым обращением к статическому полю, не унаследованному от предка. Это семантика с заблаговременной инициализацией поля, так как CLR гарантирует лишь то, что статический конструктор будет исполнен до обращения к статическому полю, возможно, задолго до него.
CLR гарантирует только начало исполнения конструктора типа, но не гарантирует его завершения. Это необходимо для избежания взаимоблокировок в тех редких случаях, когда два конструктора типа ссылаются друг на друга.
В некоторых языках тип может определять, как операторы должны манипулировать его экземплярами. В частности, многие типы (например, System.String) используют перегрузку операторов равенства (==) и неравенства (!=). CLR ничего не известно о перегрузке операторов - ведь она даже не знает, что такое оператор. Допустим, вы определяете (на С#) класс:
class Complex {
public static Complex operator+(Complex cl. Complex c2) { . . . }}
В этом случае компилятор генерирует определение метода opAddition и устанавливает в записи с определением этого метода флаг specialname, свидетельствующий о том, что это "особый" метод. Когда компилятор языка (в том числе компилятор С#) видит в исходном тексте оператор +, он исследует типы его операндов.
При этом компилятор пытается выяснить, не определен ли для одного из них метод opAddition с флагом specialname, параметры которого совместимы с типами операндов.
Если такой метод существует, компилятор генерирует код, вызывающий этот метод, иначе возникает ошибка компиляции.
Методы операторов преобразования
Время от времени требуется преобразовать объект одного типа в объект другого типа. Когда исходный и целевой типы являются элементарными, компилятор способен без посторонней помощи генерировать код, необходимый для преобразования объекта.
Однако если ни один из типов не является элементарным, компилятор не будет знать, как выполнить преобразование. Представьте, что в FCL включен тип данных Rational, в который удобно преобразовывать объекты типа Int32 или Single. Более того, обратное преобразование выполнять тоже удобно.
Чтобы выполнить эти преобразования, тип Rational должен определять открытые конструкторы, принимающие в качестве единственного параметра экземпляр преобразуемого типа. Кроме того, нужно определить открытый экземплярный метод То Ххх, не принимающий параметров (примером может служить популярный метод ToString). Каждый такой метод преобразует экземпляр типа, в котором определен этот метод, в экземпляр типа Ххх. Вот как правильно определить соответствующие конструкторы и методы для типа Rational:
class Rational {
// Создает Rational из Int32
public Rational(Int32 numerator) { ... }
// Создает Rational из Single public Rational(Single value) { ... }
// Преобразует Rational в Int32 public Int32 ToInt32( ) { ... }
// Преобразует Rational в Single public Single ToSingle( ) { ... }
}
Вызывая эти конструкторы и методы, разработчик, использующий любой язык, может преобразовать объект типа Int32 или Single в Rational и обратно. Подобно методам перегруженных операторов методы операторов преобразования должны быть помечены модификаторами public и static. При определении методов операторов преобразования также следует указать, должен ли компилятор генерировать код для неявного вызова метода оператора преобразования автоматически или он должен генерировать код, вызывающий этот метод, лишь при наличии явного указания в исходном тексте. Ключевое слово implicit указывает компилятору С#, что наличие в исходном тексте явного приведения типов не обязательно для генерации кода, вызывающего метод оператора преобразования. Ключевое слово explicit позволяет компилятору вызывать метод, лишь, когда в исходном тексте имеется явное приведение типов.
После ключевого слова implicit или explicit следует поместить указание (ключевое слово operator), сообщающее компилятору, что данный метод представляет оператор преобразования. После ключевого слова operator надо указать целевой тип, в который преобразуется объект, а в скобках - исходный тип объекта.
Определив в показанном выше типе Rational операторы преобразования, можно написать (на С#):
class App {
static void Main( ) {
Rational rl = 5;// Неявное приведение Int32 к Rational
Rational r2 = 2.5F;// Неявное приведение Single к Rational
Int32 x = (Int32) rl; // Явное приведение Rational к Int32 Single s = (Single) rl; // Явное приведение Rational к Single }
}
Компилятор С# полностью поддерживает операторы преобразования. Обнаружив код, в котором вместо ожидаемого типа используется объект совсем другого типа, компилятор ищет метод оператора неявного преобразования, способный выполнить нужное преобразование, и генерирует код, вызывающий этот метод. Если есть подходящий метод оператора неявного преобразования, компилятор вставляет в результирующий IL-код вызов этого метода.
Обнаружив в исходном тексте явное приведение типов, компилятор ищет метод оператора явного или неявного преобразования. Если он существует, компилятор генерирует вызывающий его код. Если компилятор не может найти подходящий метод оператора преобразования, он генерирует ошибку, и код не компилируется.
Виртуальный метод (виртуальная функция) - в объектно-ориентированном программировании метод (функция) класса, который может быть переопределён в классах-наследниках так, что конкретная реализация метода для вызова будет определяться во время исполнения. Таким образом, программисту необязательно знать точный тип объекта для работы с ним через виртуальные методы: достаточно лишь знать, что объект принадлежит классу или наследнику класса, в котором метод объявлен.
Базовый класс может и не предоставлять реализации виртуального метода, а только декларировать его существование. Такие методы без реализации называются "чисто виртуальными" или абстрактными. Класс, содержащий хотя бы один такой метод, тоже будет абстрактным
static void Main(string[] args){
Animal[] animals = new Animal[2];
animals[0] = new Cat();
animals[1] = new Dog();
animals[0].talk();
animals[1].talk();
}
public class Animal {
public virtual void Talk() {
WriteLine("Animal Talk");
}}
public class Cat : Animal {
public override void Talk() {
WriteLine(string.Format("talk ", "Cat", "МЯУ!!!!!!!"));
}}
public class Dog : Animal {
public override void Talk() {
WriteLine(string.Format(" talk ", "Dog", "ГАФ!!!!!!!"));
}}
CLR определяет, является ли нестатический метод виртуальным, путем изучения метаданных. Однако CLR не использует эту информацию при вызове метода - вместо этого она поддерживает две команды IL для вызова методов: call и callvirt. Метод, вызываемый командой IL call, зависит от типа переданной ссылки, а вызываемый командой callvirt - от типа объекта, на который указывает переданная ссылка. При компиляции исходного текста компилятор определяет, какой метод вызывается - виртуальный или нет. и генерирует соответствующую команду IL - call или callvirt. Это значит, что виртуальный метод можно вызвать как невиртуальный. Такой подход часто применяют, когда код вызывает виртуальный метод, определенный в базовом классе типа, как показано ниже:
class SomeClass {
// ToString - это виртуальный метод, определенный в базовом классе Object,
public override String ToString( ) {
// Компилятор использует команду IL 'call' для вызова
// виртуального метода ToString класса Object как невиртуального
// Если бы компилятор использовал команду 'callvirt' вместо 'call'
// то этот метод рекурсивно вызывал бы сам себя до переполнения стека
return base.ToString( );
}}
Кроме того, компиляторы обычно генерируют команду call при вызове виртуального метода по ссылке на изолированный тип. Здесь применение call вместо callvirt помогает повысить скорость, так как в этом случае CLR может не проверять реальный тип объекта, на который передана ссылка. Кроме того, команда call в случае размерных типов (которые всегда являются изолированными) предотвращает их упаковку, что снижает утилизацию памяти и процессора.
Независимо от того, какая команда используется для вызова экземплярного метода - call или callvirt, все методы экземпляра всегда получают в качестве первого параметра скрытый указатель this ссылающийся на объект, которым манипулирует метод.
Передача методу параметра ссылкой. Передача переменного числа параметров По умолчанию CLR предполагает, что все параметры методов передаются значением. При передаче объекта ссылочного типа методу передается (значением) ссылка (или указатель) на этот объект. Если параметром является экземпляр размерного типа, то методу передается его копия. CLR также позволяет передавать параметры ссылкой, а не значением. В С# это делается с помощью ключевых слов out и ref. Оба заставляют компилятор генерировать метаданные, которые описывают параметр как переданный ссылкой. Компилятор использует эти метаданные для генерации кода, передающего вместо самого параметра его адрес.
Разница между этими ключевыми словами касается метода инициализации объекта, на который указывает переданная ссылка. Если параметр метода помечен ключевым словом out, то вызывающий код может не инициализировать его, пока не будет вызван этот метод. В этом случае вызванный метод не может читать значение параметра и должен записать его, прежде чем вернуть управление. Если параметр метода помечен ключевым словом ref, то вызывающий код должен инициализировать его перед вызовом этого метода, а вызванный метод может, как читать, так и записывать значение параметра.
Использование ключевых слов out и ref с размерными и ссылочными типами существенно отличается. Сначала рассмотрим их использование с размерными типами:
class App {
static void Main( ) { Int32 x;
// Инициализировать параметр х не обязательно
SetVal(out x);
// Выводит на консоль "10" Console.WriteLine(x);
}
static void SetVal(out Int32 v) {
// Этот метод должен инициализировать v
v = 10; } }
А теперь взгляните на пример, в котором вместо out использовано ключевое слово ref:
class App {
static void Main( ) { Int32 x = 5;
// Параметр х должен быть инициализирован
AddVal(ref x);
// Выводит на консоль "15" Console. WriteLine(x);
}
static void AddVal(ref Int32 v) {
// Этот метод может использовать инициализированный параметр v
v += 10; } }
Использование ключевых слов out и ref с размерными типами дает тот же результат, что и передача ссылочного типа значением. Ключевые слова out и ref позволяют методу манипулировать единственным экземпляром размерного типа. Вызывающий код должен выделить память для этого экземпляра, а вызванный метод будет управлять выделенной памятью, В случае ссылочных типов вызывающий код выделяет память для указателя на передаваемый объект, а вызванный код манипулирует этим указателем. Передача методу переменного числа параметров
Иногда разработчику удобно определить метод, способный принимать переменное число параметров. Например, тип System.String предлагает методы, позволяющие выполнить конкатенацию произвольного числа строк, а также методы, при вызове которых можно задать набор строк, которые должны быть форматированы все вместе.
Метод, принимающий переменное число аргументов, объявляют так
static Int32 Add(params Int32[] values) {
// ПРИМЕЧАНИЕ: если нужно, этот массив // можно передать и другим методам.
Int32 sum = 0;
for (Int32 x = 0; x < values. Length; x++)
sum += values[x]; return sum;
}
Очевидно, этот метод может быть вызван так:
static void Main( ) {
// Выводит на консоль "15"
Console.WriteLine(Add(new Int32[] { 1, 2, 3, 4, 5 } ));
}
Следующий вызов метода Add прекрасно компилируется, отлично работает и дает в результате сумму, равную 0 (как и ожидалось):
static void Main( ) {
// Выводит на консоль "0" Console.WriteLine(Add( ));
}
Все показанные до сих пор примеры демонстрировали написание метода, принимающего произвольное число параметров типа Int32. Но как бы вы написали метод, принимающий произвольное число параметров любого типа? Ответ прост: достаточно модифицировать прототип метода так, чтобы вместо Int32[] он принимал Object[]. 
Документ
Категория
Разное
Просмотров
45
Размер файла
30 Кб
Теги
понкратов, оопип
1/--страниц
Пожаловаться на содержимое документа