Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ЧЛЕНЫ ТИПОВ И ДОСТУП К НИМ

.doc
Скачиваний:
4
Добавлен:
22.02.2015
Размер:
100.35 Кб
Скачать

ЧЛЕНЫ ТИПОВ И ДОСТУП К НИМ

Тип может определять произвольное число следующих членов.

• Константы

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

• Поля

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

• Конструкторы экземпляров

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

• Конструкторы типов

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

• Методы

Метод — это функция, выполняющая операции, которые изменяют или опрашивают состояние типа (статический метод) или объекта (экземплярный метод). Методы обычно осуществляют чтение и запись полей типов или объектов.

• Перегрузка операторов

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

• Операторы преобразования

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

• Свойства

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

• События

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

• Типы

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

Пример членов класса.

class SomeType {

// Вложенный класс

class SomeNestedType { }

// Константа, неизменяемое и изменяемое статическое поле

const Int32 SomeConstant =1;

readonly Int32 SomeReadOnlyField =2;

static Int32 SomeReadWriteField = 3 ;

// Конструктор типа

static SomeType() { }

// Конструкторы экземпляров

public SomeType() { }

public SomeType(Int32 x) { }

// Статический и экземплярный методы

string InstanceMethod() { return null; }

static void Main() {}

// Экземплярное свойство

Int32 SomeProp {

get { return 0; }

set { }

// Экземплярное индексируемое свойство

public Int32 this[String s] {

get { return 0; }

set { }

// Экземплярное событие

event EventHandler SomeEvent;

}

МОДИФИКАТОРЫ ДОСТУПА И ПРЕДОПРЕДЕЛЕННЫЕ АТРИБУТЫ

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

private

Доступен только методам в определяющем типе и вложенных в него типах

protected

Доступен только методам в этом типе (и вложенных в него типах) или одном из его производных типов безотносительно к сборке

internal

Доступен только методам в определяющей сборке

protected internal

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

public

Доступен всем методам во всех сборках

Предопределенные атрибуты поля

static - Поле частично определяет состояние типа, а не объекта.

readonly - В поле может осуществляться запись только кодом, находящимся в конструкторе. Для полей можно указывать оба атрибута (для констант – нет.)

Предопределенные атрибуты метода

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

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

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

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

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

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

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

ДОБАВЛЕНИЕ К ТИПУ КОНСТАНТ И ПОЛЕЙ

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

public class Component {

// ПРИМЕЧАНИЕ: С# не позволяет использовать для констант модификатор //static, поскольку всегда подразумевается, что константы являются //статическими

public const Int32 MaxEntriesInList = 50;

}

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

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

CLR поддерживает изменяемые и неизменяемые поля. Большинство полей -

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

public class Component {

// Модификатор static необходим, чтобы ассоциировать поле с его типом

public static readonly Int32 MaxEntriesInList = 50;

}

Теперь при исполнении метода Main этого приложения CLR извлекает значение поля MaxEntriesInList из динамической памяти, выделенной для его хранения. Это значение будет равно 50.

В предыдущем примере показано, как определить неизменяемое статическое

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

public class SomeType {

// Это статическое неизменяемое поле. Его значение рассчитывается и //сохраняется в памяти во время инициализации класса во время выполнения

public static readonly Random random = new Random();

// Это статическое изменяемое

static Int32 numberOfWrites = 0;

// Это неизменяемое экземплярное поле

public readonly String pathName = "Untitled";

// Это изменяемое экземплярное поле.

public FileStream fs;

public SomeType(String pathName) {

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

this.pathName = pathName;

public String DoSomethingOf (){

// Эта строка читает и записывает значение статического изменяемого поля.

numberOf Writes = numberOfWrites + 1;

// Эта строка читает значение неизменяемого экземплярного поля.

return pathName;}

МЕТОДЫ

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

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

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

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

class SomeType {

// По умолчанию С# автоматически определяет открытый конструктор без //параметров.

}

Это определение идентично определению типа:

class SomeType {

public SomeType() { }

}

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

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

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

class SomeType {

Int32 x = 5;

}

При создании объекта SomeType его поле х инициализируется значением 5. Как это происходит? Надо изучить IL-код метода-конструктора.

Конструктор объекта SomeType содержит код, записывающий в поле

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

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; }

}

Компилятор помещает в начало каждого из методов код, инициализирующий поля x, s и d. Затем он добавляет к методу код, расположенный внутри методов-конструкторов. Например, IL-код, сгенерированный для конструктора с параметром типа String, состоит из кода, инициализирующего поля x, s и d, и кода, перезаписывающего поле d значением 10. Отметим: поле b гарантированно инициализируется значением 0, даже если нет кода, инициализирующего это поле явно.

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

class SomeType {

// Здесь нет кода, явно инициализирующего поля.

Int32 x;

String s;

Double d;

Byte b;

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

// Этот конструктор содержит код, инициализирующий поля

public SomeType() {

x = 5;

s = "Hi Therel";

d = 3.14159;

}

// Этот конструктор сначала вызывает конструктор по умолчанию.

public SomeType(Int32 x) : this() {

this.x = x;}

// Этот конструктор сначала вызывает конструктор по умолчанию.

public SomeType(String s) : this() {

this.s = s; }

Конструкторы размерных типов работают иначе, чем конструкторы ссылочных типов. Во-первых, 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. у = у;

}}

class Rectangle {

public Point topLeft, bottomRight;

public Rectangle() {

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

topLeft = new Point(1, 2);

bottomRight = new Point(100, 200);

}}

Конструктор экземпляра размерного типа будет исполнен, только если вызвать его явно. Так что, если конструктор объекта Rectangle не инициализировал его поля topLeft и bottom Right вызовом конструктора Point оператором new, поля х и у у обеих структур Point будут содержать 0. Если размерный тип Point уже определен, то конструктор по умолчанию, не имеющий параметров, не определяется.

Рассмотрим пример

struct Point {

public Int32 x, у;

public Point() {

x = у = 5; }

}

class Rectangle {

public Point topLeft, bottomRight;

public Rectangle() {}

}

А теперь вопрос: какими значениями — 0 или 5 — будут инициализированы поля х и у, принадлежащие структурам Point (topLeft и bottomRight)?

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

По этой же причине ошибочен код

struct SomeValType {

Int32 x = 5;

}

Также ошибочен код

struct SomeValType {

Int32 x, у;

// С# допускает наличие у размерных типов конструкторов с параметрами

public SomeValType(Int32 x) {

this.x = x;}}

Обратите внимание: поле у здесь не инициализируется !

КОНСТРУКТОРЫ ТИПОВ

Помимо конструкторов экземпляров, CLR поддерживает конструкторы типов (также известные как статические конструкторы, конструкторы классов или инициализаторы типов). Конструкторы типа можно применять и к интерфейсам (хотя С# этого не допускает), ссылочным и размерным типам. Подобно тому, как конструкторы экземпляров используются для установки первоначального состояния экземпляра типа, конструкторы типов применяются для установки первоначального состояния типа. По умолчанию у типа не определен ни один конструктор. У типа может быть один и только один конструктор. Кроме того, у конструкторов типа никогда не бывает параметров. Вот как определяются ссылочные и размерные типы с конструкторами в программах на С#:

class SomeRefType {

static SomeRefType() {

// Исполняется при первом обращении к типу SomeRefType.

}

struct SomeValType {

// C# на самом деле допускает определять для размерных типов

// конструкторы, не имеющие параметров.

static SomeValType() {

// Исполняется при первом обращении к типу SomeValType.

}}

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

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

CLR вызывает конструктор типа в одном из следующих случаев.

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

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

Пример

class SomeType {

static Int32 x = 5;

static SomeType() {

x = 10;

}}

СВОЙСТВА БЕЗ ПАРАМЕТРОВ

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

public class Employee {

public String Name; // Имя служащего

public Int32 Age; // Возраст служащего

}

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

Employee е = new Employee();

e.Name = "Ivan Sidorov"; // Задать имя служащего

e.Age = 25; // Задать возраст служащего

Console.WriteLine(e.Name); // Вывести на экран " Ivan Sidorov "

Этот способ запроса и установки сведений о состоянии объекта очень распространен. Но предыдущий код ни в коем случае не следует писать так, как в примере. Одним из соглашений объектно-ориентированного программирования и разработки является инкапсуляция данных. Инкапсуляция данных – одна из основ ООП, означает, что поля типа ни в коем случае не следует открывать для общего пользования, так как в этом случае слишком просто написать код, способный испортить сведения о состоянии объекта путем неверного применения полей. Например, таким кодом разработчик может легко повредить объект Employee:

e.Age = -5; // Можете вообразить человека, которому -5 лет?

Есть и другие причины инкапсуляции доступа к полям данных типа.

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

public class Employee {

private String Name; // Поле стало закрытым

private Int32 Age; // Поле стало закрытым

public String GetName() {

return(Name);

}

public void SetName(String value) {

Name = value;

}

public Int32 SetAge() {

return(Age);

}

public void SetAge{Int32 value) {

if (value < 0)

throw new ArgumentOutOfRangeException(

"Age must be greater than or equal to 0");

Age = value; }

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

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

e.SetAge(35); // Обновить возраст

e.SetAge{-5); // Генерируется исключение ArgumentOutOfRangeException

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

Этот класс использует свойства и функционально идентичен предыдущему:

public class Employee {

private String _Name;

// Добавление символа '_' помогает избежать конфликта имен.

private Int32 _Age;

// Добавление символа '_' помогает избежать конфликта имен.

public String Name {

get { return(_Name); }

set { _Name = value; } // Ключевое слово 'value' всегда

// идентифицирует новое значение.

}

public Int32 Age {

get { return(_Age); }

set {

if (value < 0) // Ключевое слово 'value' всегда

// идентифицирует новое значение.

throw new ArgumentOutOfRangeException(

"Age must be greater than or equal to 0");

_Age = value;

}}}

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

е.Аде = 35; // Обновить возраст

е.Аде = -5; // Генерируется исключение ArgumentOutOfRangeException

Можно считать свойства «умными» полями, т. е. полями с дополнительной логикой. CLR поддерживает статические, экземплярные и виртуальные свойства.

Кроме того, свойства могут помечаться модификатором доступа и определяться в интерфейсах.

У каждого свойства есть имя и тип (который не может быть void). Нельзя перегружать свойства (т.е. определять пару свойств с одинаковыми именами, но с разным типом). Определяя свойство, обычно определяют пару методов: get и set. Однако, опустив set, можно определить свойство, доступное только для чтения, а если оставить только get, получится свойство, доступное только для записи.

Методы get и set свойства довольно часто манипулируют закрытым полем, определенным в типе. Это поле обычно называют полем поддержки (backing field).

Однако методам get и set не приходится обращаться к полю поддержки. Так, тип System.Threading.Thread поддерживает свойство Priority, взаимодействующее непосредственно с ОС, а объект Thread не поддерживает поле, хранящее приоритет потока. Другой пример свойств, не имеющих полей поддержки, — неизменяемые свойства, вычисляемые при выполнении: длина массива, заканчивающегося 0, или область прямоугольника, заданного шириной и высотой и т. д.

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

• метод-аксессор get этого свойства — генерируется, только если для свойства определен аксессор get;

• метод-аксессор set этого свойства — генерируется, только если для свойства определен аксессор set;

• определение свойства в метаданных управляемого модуля — генерируется всегда.

Вернемся к показанному выше типу Employee. При его компиляции компилятор обнаруживает свойства Name и Age. Поскольку у обоих свойств есть методы-аксессоры get и set, компилятор генерирует в типе Employee четыре определения метода. Результат получается такой, как если бы тип был первоначально написан так:

public class Employee {

private String _Name; // Добавление символа '_' помогает избежать конфликта имен.

private Int32 _Age; // Добавление символа '_' помогает избежать конфликта имен.

public String get_Name(){

return _Name;

}

public void set_Name(String value) {

_Name = value; // 'value' всегда идентифицирует новое значение.